This repository has no description
1<script lang="ts">
2 import type { KeyboardEventHandler } from "svelte/elements";
3 import Eye from "$icon/eye";
4 import Pencil from "$icon/pencil";
5 import Button from "./Button.svelte";
6 import ButtonGroup, { segmentProps } from "./ButtonGroup.svelte";
7 import Textarea, { textareaMinHeight } from "./Textarea.svelte";
8 import { renderMarkup, type MarkupContext } from "$lib/markup";
9
10 interface Props {
11 value?: string;
12 markup: MarkupContext;
13 tab?: "write" | "preview";
14 id?: string;
15 name?: string;
16 rows?: number;
17 placeholder?: string;
18 disabled?: boolean;
19 autofocus?: boolean;
20 // drop the textarea/preview background so the editor blends into its surroundings
21 transparent?: boolean;
22 }
23
24 let {
25 value = $bindable(""),
26 markup,
27 tab = $bindable("write"),
28 id,
29 name,
30 rows = 12,
31 placeholder,
32 disabled = false,
33 autofocus = false,
34 transparent = false
35 }: Props = $props();
36
37 const surface = $derived(transparent ? "bg-transparent" : "bg-background-default");
38 // the preview pane replaces the field in place, so it has to carry the field's own
39 // height and inset — otherwise the whole editor resizes every time you switch tabs.
40 const pane = `rounded border border-border-default px-2.5 py-2 ${textareaMinHeight}`;
41
42 let preview = $state<Promise<string> | null>(null);
43 let textareaEl = $state<HTMLTextAreaElement>();
44
45 // focus on mount (and when returning to the write tab) if requested
46 $effect(() => {
47 if (autofocus) textareaEl?.focus();
48 });
49
50 $effect(() => {
51 if (tab !== "preview") {
52 preview = null;
53 return;
54 }
55 const source = value;
56 if (!source.trim()) {
57 preview = null;
58 return;
59 }
60 preview = renderMarkup(source, markup).then((html) => html ?? "");
61 });
62 // ctrl/cmd+enter submits the enclosing form, mirroring the old htmx editor
63 const handleKeydown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
64 if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
65 e.preventDefault();
66 e.currentTarget.form?.requestSubmit();
67 }
68 };
69</script>
70
71<div class="flex flex-col gap-1.5">
72 <ButtonGroup class="self-start">
73 <Button
74 type="button"
75 size="sm"
76 icon={Pencil}
77 {...segmentProps(tab === "write")}
78 onclick={() => (tab = "write")}
79 >
80 Write
81 </Button>
82 <Button
83 type="button"
84 size="sm"
85 icon={Eye}
86 {...segmentProps(tab === "preview")}
87 onclick={() => (tab = "preview")}
88 >
89 Preview
90 </Button>
91 </ButtonGroup>
92
93 {#if tab === "write"}
94 <Textarea
95 {id}
96 {name}
97 {rows}
98 resizeable
99 {placeholder}
100 bind:value
101 bind:element={textareaEl}
102 {disabled}
103 onkeydown={handleKeydown}
104 class={transparent ? "bg-transparent" : undefined}
105 />
106 {:else}
107 {#await preview}
108 <div class={`${pane} ${surface} typography-paragraph-small text-foreground-subtle italic`}>Rendering…</div>
109 {:then html}
110 {#if html}
111 <div class={`markup ${pane} ${surface}`}>
112 <!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in $lib/markup -->
113 {@html html}
114 </div>
115 {:else}
116 <div class={`${pane} ${surface} typography-paragraph-small text-foreground-subtle italic`}>
117 Nothing to preview.
118 </div>
119 {/if}
120 {:catch}
121 <div class={`${pane} ${surface} typography-paragraph-small text-foreground-subtle italic`}>
122 Could not render preview.
123 </div>
124 {/await}
125 {/if}
126</div>