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 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 previewClass?: string;
20 autofocus?: boolean;
21 // drop the textarea/preview background so the editor blends into its surroundings
22 transparent?: boolean;
23 }
24
25 let {
26 value = $bindable(""),
27 markup,
28 tab = $bindable("write"),
29 id,
30 name,
31 rows = 12,
32 placeholder,
33 disabled = false,
34 previewClass = "min-h-40",
35 autofocus = false,
36 transparent = false
37 }: Props = $props();
38
39 const surface = $derived(transparent ? "bg-transparent" : "bg-background-default");
40
41 let previewHtml = $state<string | null>(null);
42 let previewing = $state(false);
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") return;
52 const source = value;
53 if (!source.trim()) {
54 previewHtml = null;
55 previewing = false;
56 return;
57 }
58 let cancelled = false;
59 previewing = true;
60 renderMarkup(source, markup)
61 .then((html) => {
62 if (!cancelled) previewHtml = html;
63 })
64 .catch(() => {
65 if (!cancelled) previewHtml = null;
66 })
67 .finally(() => {
68 if (!cancelled) previewing = false;
69 });
70 return () => {
71 cancelled = true;
72 };
73 });
74 // ctrl/cmd+enter submits the enclosing form, mirroring the old htmx editor
75 const handleKeydown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
76 if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
77 e.preventDefault();
78 e.currentTarget.form?.requestSubmit();
79 }
80 };
81</script>
82
83<div class="flex flex-col gap-1.5">
84 <ButtonGroup class="self-start">
85 <Button
86 type="button"
87 size="sm"
88 icon={Pencil}
89 {...segmentProps(tab === "write")}
90 onclick={() => (tab = "write")}
91 >
92 Write
93 </Button>
94 <Button
95 type="button"
96 size="sm"
97 icon={Eye}
98 {...segmentProps(tab === "preview")}
99 onclick={() => (tab = "preview")}
100 >
101 Preview
102 </Button>
103 </ButtonGroup>
104
105 {#if tab === "write"}
106 <Textarea
107 {id}
108 {name}
109 {rows}
110 resizeable
111 {placeholder}
112 bind:value
113 bind:element={textareaEl}
114 {disabled}
115 onkeydown={handleKeydown}
116 class={transparent ? "max-h-none bg-transparent" : "max-h-none"}
117 />
118 {:else if previewHtml}
119 <div
120 class={`markup rounded border border-border-default ${surface} px-2.5 py-2 ${previewClass}`}
121 >
122 <!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in $lib/markup -->
123 {@html previewHtml}
124 </div>
125 {:else}
126 <div
127 class={`rounded border border-border-default ${surface} px-2.5 py-2 text-sm text-foreground-subtle italic ${previewClass}`}
128 >
129 {previewing ? "Rendering…" : "Nothing to preview."}
130 </div>
131 {/if}
132</div>