svelte-comp 1.3.3 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE.md +21 -21
  2. package/README.md +101 -100
  3. package/dist/App.svelte +507 -507
  4. package/dist/Container.svelte +59 -59
  5. package/dist/app.css +234 -235
  6. package/dist/app.d.ts +10 -0
  7. package/dist/lib/Accordion.svelte +155 -155
  8. package/dist/lib/Badge.svelte +44 -44
  9. package/dist/lib/Button.svelte +185 -170
  10. package/dist/lib/Calendar.svelte +384 -384
  11. package/dist/lib/Card.svelte +103 -103
  12. package/dist/lib/Carousel.svelte +293 -293
  13. package/dist/lib/Carousel.svelte.d.ts +1 -1
  14. package/dist/lib/CheckBox.svelte +210 -210
  15. package/dist/lib/CodeView.svelte +308 -307
  16. package/dist/lib/ColorPicker.svelte +159 -159
  17. package/dist/lib/ContextMenu.svelte +328 -322
  18. package/dist/lib/DatePicker.svelte +246 -246
  19. package/dist/lib/Dialog.svelte +233 -233
  20. package/dist/lib/Field.svelte +299 -299
  21. package/dist/lib/FilePicker.svelte +295 -240
  22. package/dist/lib/FilePicker.svelte.d.ts +6 -1
  23. package/dist/lib/Form.svelte +438 -438
  24. package/dist/lib/Hamburger.svelte +217 -217
  25. package/dist/lib/InstallPWA.svelte +94 -94
  26. package/dist/lib/Menu.svelte +623 -623
  27. package/dist/lib/NoticeBase.svelte +140 -140
  28. package/dist/lib/PaginatedCard.svelte +73 -73
  29. package/dist/lib/Pagination.svelte +119 -119
  30. package/dist/lib/PrimaryColorSelect.svelte +111 -111
  31. package/dist/lib/ProgressBar.svelte +141 -141
  32. package/dist/lib/ProgressCircle.svelte +190 -190
  33. package/dist/lib/Radio.svelte +189 -189
  34. package/dist/lib/SearchInput.svelte +104 -104
  35. package/dist/lib/Select.svelte +524 -524
  36. package/dist/lib/Slider.svelte +253 -253
  37. package/dist/lib/Splitter.svelte +159 -150
  38. package/dist/lib/Switch.svelte +168 -167
  39. package/dist/lib/Table.svelte +299 -299
  40. package/dist/lib/Tabs.svelte +213 -213
  41. package/dist/lib/ThemeToggle.svelte +128 -127
  42. package/dist/lib/TimePicker.svelte +312 -312
  43. package/dist/lib/TimePickerNew.svelte +634 -0
  44. package/dist/lib/TimePickerNew.svelte.d.ts +49 -0
  45. package/dist/lib/Toast.svelte +123 -123
  46. package/dist/lib/Tooltip.svelte +110 -110
  47. package/dist/lib/Topbar.svelte +107 -107
  48. package/dist/lib/__tests__/Accordion.test.d.ts +1 -0
  49. package/dist/lib/__tests__/Accordion.test.js +171 -0
  50. package/dist/lib/__tests__/Badge.test.d.ts +1 -0
  51. package/dist/lib/__tests__/Badge.test.js +41 -0
  52. package/dist/lib/__tests__/Button.test.d.ts +1 -0
  53. package/dist/lib/__tests__/Button.test.js +269 -0
  54. package/dist/lib/__tests__/Calendar.test.d.ts +1 -0
  55. package/dist/lib/__tests__/Calendar.test.js +171 -0
  56. package/dist/lib/__tests__/Card.test.d.ts +1 -0
  57. package/dist/lib/__tests__/Card.test.js +148 -0
  58. package/dist/lib/__tests__/Carousel.test.d.ts +1 -0
  59. package/dist/lib/__tests__/Carousel.test.js +439 -0
  60. package/dist/lib/__tests__/CheckBox.test.d.ts +1 -0
  61. package/dist/lib/__tests__/CheckBox.test.js +152 -0
  62. package/dist/lib/__tests__/CodeView.test.d.ts +1 -0
  63. package/dist/lib/__tests__/CodeView.test.js +157 -0
  64. package/dist/lib/__tests__/ColorPicker.test.d.ts +1 -0
  65. package/dist/lib/__tests__/ColorPicker.test.js +93 -0
  66. package/dist/lib/__tests__/ContextMenu.test.d.ts +1 -0
  67. package/dist/lib/__tests__/ContextMenu.test.js +67 -0
  68. package/dist/lib/__tests__/DatePicker.test.d.ts +1 -0
  69. package/dist/lib/__tests__/DatePicker.test.js +108 -0
  70. package/dist/lib/__tests__/Dialog.test.d.ts +1 -0
  71. package/dist/lib/__tests__/Dialog.test.js +183 -0
  72. package/dist/lib/__tests__/Field.test.d.ts +1 -0
  73. package/dist/lib/__tests__/Field.test.js +190 -0
  74. package/dist/lib/__tests__/FilePicker.test.d.ts +1 -0
  75. package/dist/lib/__tests__/FilePicker.test.js +179 -0
  76. package/dist/lib/__tests__/Form.integration.test.d.ts +1 -0
  77. package/dist/lib/__tests__/Form.integration.test.js +158 -0
  78. package/dist/lib/__tests__/Form.test.d.ts +1 -0
  79. package/dist/lib/__tests__/Form.test.js +463 -0
  80. package/dist/lib/__tests__/Hamburger.test.d.ts +1 -0
  81. package/dist/lib/__tests__/Hamburger.test.js +161 -0
  82. package/dist/lib/__tests__/InstallPWA.test.d.ts +1 -0
  83. package/dist/lib/__tests__/InstallPWA.test.js +15 -0
  84. package/dist/lib/__tests__/Menu.test.d.ts +1 -0
  85. package/dist/lib/__tests__/Menu.test.js +285 -0
  86. package/dist/lib/__tests__/NoticeBase.test.d.ts +1 -0
  87. package/dist/lib/__tests__/NoticeBase.test.js +60 -0
  88. package/dist/lib/__tests__/PaginatedCard.test.d.ts +1 -0
  89. package/dist/lib/__tests__/PaginatedCard.test.js +89 -0
  90. package/dist/lib/__tests__/Pagination.test.d.ts +1 -0
  91. package/dist/lib/__tests__/Pagination.test.js +168 -0
  92. package/dist/lib/__tests__/PrimaryColorSelect.test.d.ts +1 -0
  93. package/dist/lib/__tests__/PrimaryColorSelect.test.js +92 -0
  94. package/dist/lib/__tests__/ProgressBar.test.d.ts +1 -0
  95. package/dist/lib/__tests__/ProgressBar.test.js +69 -0
  96. package/dist/lib/__tests__/ProgressCircle.test.d.ts +1 -0
  97. package/dist/lib/__tests__/ProgressCircle.test.js +71 -0
  98. package/dist/lib/__tests__/Radio.test.d.ts +1 -0
  99. package/dist/lib/__tests__/Radio.test.js +127 -0
  100. package/dist/lib/__tests__/SearchInput.test.d.ts +1 -0
  101. package/dist/lib/__tests__/SearchInput.test.js +80 -0
  102. package/dist/lib/__tests__/Select.test.d.ts +1 -0
  103. package/dist/lib/__tests__/Select.test.js +408 -0
  104. package/dist/lib/__tests__/Slider.test.d.ts +1 -0
  105. package/dist/lib/__tests__/Slider.test.js +213 -0
  106. package/dist/lib/__tests__/Splitter.test.d.ts +1 -0
  107. package/dist/lib/__tests__/Splitter.test.js +87 -0
  108. package/dist/lib/__tests__/Switch.test.d.ts +1 -0
  109. package/dist/lib/__tests__/Switch.test.js +97 -0
  110. package/dist/lib/__tests__/Table.test.d.ts +1 -0
  111. package/dist/lib/__tests__/Table.test.js +349 -0
  112. package/dist/lib/__tests__/Tabs.test.d.ts +1 -0
  113. package/dist/lib/__tests__/Tabs.test.js +262 -0
  114. package/dist/lib/__tests__/ThemeToggle.test.d.ts +1 -0
  115. package/dist/lib/__tests__/ThemeToggle.test.js +84 -0
  116. package/dist/lib/__tests__/TimePicker.test.d.ts +1 -0
  117. package/dist/lib/__tests__/TimePicker.test.js +146 -0
  118. package/dist/lib/__tests__/TimePickerNew.test.d.ts +1 -0
  119. package/dist/lib/__tests__/TimePickerNew.test.js +322 -0
  120. package/dist/lib/__tests__/Toast.test.d.ts +1 -0
  121. package/dist/lib/__tests__/Toast.test.js +135 -0
  122. package/dist/lib/__tests__/Tooltip.test.d.ts +1 -0
  123. package/dist/lib/__tests__/Tooltip.test.js +171 -0
  124. package/dist/lib/__tests__/Topbar.test.d.ts +1 -0
  125. package/dist/lib/__tests__/Topbar.test.js +25 -0
  126. package/dist/lib/__tests__/setupLangContext.d.ts +1 -0
  127. package/dist/lib/__tests__/setupLangContext.js +65 -0
  128. package/dist/lib/__tests__/storage.test.d.ts +1 -0
  129. package/dist/lib/__tests__/storage.test.js +124 -0
  130. package/dist/lib/__tests__/utils.test.d.ts +1 -0
  131. package/dist/lib/__tests__/utils.test.js +11 -0
  132. package/dist/lib/index.d.ts +1 -0
  133. package/dist/lib/index.js +1 -0
  134. package/dist/lib/lang.d.ts +4 -0
  135. package/dist/lib/lang.js +4 -0
  136. package/dist/styles.css +234 -232
  137. package/dist/utils/index.js +15 -4
  138. package/package.json +52 -52
@@ -1,307 +1,308 @@
1
- <!-- src/lib/CodeView.svelte -->
2
- <script lang="ts">
3
- /**
4
- * @component CodeView
5
- * @description CodeView is a small prism.js powered code block that supports syntax highlighting, optional editing, line numbers and active-line highlighting.
6
- *
7
- * @prop code {string} - Code content to render
8
- * @default ""
9
- *
10
- * @prop language {Language} - Syntax highlighting language
11
- * @options txt|html|css|js|json|python
12
- * @default "txt"
13
- *
14
- * @prop title {string} - Title displayed above the code block
15
- * @default "Code"
16
- *
17
- * @prop showCopyButton {boolean} - Shows the copy-to-clipboard button
18
- * @default true
19
- *
20
- * @prop showLineNumbers {boolean} - Displays line numbers alongside the code
21
- * @default false
22
- *
23
- * @prop editable {boolean} - Enables editable mode with a textarea overlay
24
- * @default false
25
- *
26
- * @prop activeLine {boolean} - Highlights the current cursor line in editable mode
27
- * @default false
28
- *
29
- * @prop sz {SizeKey} - Size preset affecting spacing and typography
30
- * @options xs|sm|md|lg|xl
31
- * @default md
32
- *
33
- * @prop class {string} - Extra classes applied to the root container
34
- * @default ""
35
- *
36
- * @note Uses Prism for syntax highlighting; HTML/CSS/TXT grammars are bundled by default.
37
- * @note Editable mode renders a transparent textarea above a highlighted code layer, mirroring real editors.
38
- * @note Cursor-line highlight is overlaid using CSS variables and scroll-position tracking.
39
- * @note Line numbers are fully scroll-synchronized with the editor content.
40
- * @note Readonly mode shows static highlighted code, without textarea.
41
- * @note Copy button writes the full code string to the clipboard.
42
- * @note All sizing and spacing scale automatically with the shared `sz` token.
43
- * @note Supports dark/light themes via existing design-token colors.
44
- * @note Designed as a low-level editor component, not a full IDE replacement.
45
- */
46
- import { type SizeKey, type Language, TEXT } from "./types";
47
- import * as Prism from "prismjs";
48
- import "prismjs/components/prism-markup";
49
- import "prismjs/components/prism-css";
50
- import "prismjs/components/prism-javascript";
51
- import "prismjs/components/prism-json";
52
- import "prismjs/components/prism-python";
53
- import "prismjs/themes/prism.css";
54
- import { cx } from "../utils";
55
-
56
- type Props = {
57
- code?: string;
58
- language?: Language;
59
- title?: string;
60
- showCopyButton?: boolean;
61
- showLineNumbers?: boolean;
62
- editable?: boolean;
63
- activeLine?: boolean;
64
- sz?: SizeKey;
65
- class?: string;
66
- };
67
-
68
- let {
69
- code = $bindable(""),
70
- language = "txt",
71
- title = "Code",
72
- showCopyButton = true,
73
- showLineNumbers = false,
74
- editable = false,
75
- activeLine = false,
76
- sz = "md",
77
- class: externalClass = "",
78
- }: Props = $props();
79
-
80
- let textareaEl = $state<HTMLTextAreaElement | null>(null);
81
- let gutterEl = $state<HTMLDivElement | null>(null);
82
- let highlightEl = $state<HTMLDivElement | null>(null);
83
- let copied = $state(false);
84
- let activeLineIndex = $state(0);
85
- let highlightScroll = $state(0);
86
- let padTopPx = $state(12);
87
-
88
- const lines = $derived(code.split("\n"));
89
-
90
- const LINE_HEIGHT: Record<SizeKey, string> = {
91
- xs: "leading-4",
92
- sm: "leading-[1.1rem]",
93
- md: "leading-[1.3rem]",
94
- lg: "leading-[1.45rem]",
95
- xl: "leading-7",
96
- };
97
-
98
- let lineHeightPx = $state(20);
99
-
100
- function escapeHtml(x: string) {
101
- return x.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
102
- }
103
-
104
- function highlight(src: string, lang: Language) {
105
- if (src === "") return "";
106
- if (lang === "txt") return escapeHtml(src);
107
- const key = lang === "html" ? "markup" : lang;
108
- const grammar = Prism.languages[key];
109
- return Prism.highlight(src, grammar, key);
110
- }
111
-
112
- const highlighted = $derived(highlight(code, language));
113
-
114
- function updateActiveLine() {
115
- if (!activeLine || !textareaEl) return;
116
- const pos = textareaEl.selectionStart ?? 0;
117
- const before = code.slice(0, pos);
118
- activeLineIndex = before.split("\n").length - 1;
119
- }
120
-
121
- function syncScroll(event: Event) {
122
- const el = event.currentTarget as HTMLElement;
123
- if (gutterEl) gutterEl.scrollTop = el.scrollTop;
124
- if (highlightEl) {
125
- highlightEl.scrollTop = el.scrollTop;
126
- highlightEl.scrollLeft = el.scrollLeft;
127
- highlightScroll = el.scrollTop;
128
- }
129
- }
130
-
131
- $effect(() => {
132
- void sz;
133
- if (!textareaEl) return;
134
- const styles = getComputedStyle(textareaEl);
135
- const lh = Number.parseFloat(styles.lineHeight);
136
- if (!Number.isNaN(lh)) lineHeightPx = lh;
137
- const pt = Number.parseFloat(styles.paddingTop);
138
- if (!Number.isNaN(pt)) padTopPx = pt;
139
- });
140
-
141
- async function copyToClipboard() {
142
- await navigator.clipboard.writeText(code);
143
- copied = true;
144
- setTimeout(() => (copied = false), 1200);
145
- }
146
- </script>
147
-
148
- <div
149
- class={cx(
150
- "cv-root w-full h-full min-h-0 flex flex-col border border-[var(--border-color-default)] bg-[var(--color-bg-surface)]",
151
- "text-[var(--color-text-default)]",
152
- externalClass
153
- )}
154
- >
155
- {#if title}
156
- <div
157
- class={cx(
158
- "px-3 py-1 bg-[var(--color-bg-muted)] font-semibold uppercase flex items-center justify-between",
159
- TEXT[sz]
160
- )}
161
- >
162
- <div>{title}</div>
163
-
164
- {#if showCopyButton}
165
- <button
166
- onclick={copyToClipboard}
167
- class={cx(
168
- "px-3 py-0.5 text-xs rounded bg-[var(--color-primary)] text-white hover:opacity-[var(--opacity-hover)]",
169
- "transition focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] focus:outline-none"
170
- )}
171
- class:!bg-green-600={copied}
172
- >
173
- {copied ? "Copied" : "Copy"}
174
- </button>
175
- {/if}
176
- </div>
177
- {/if}
178
-
179
- <div
180
- class={cx(
181
- "cv-body flex flex-1 min-h-0 font-mono",
182
- TEXT[sz],
183
- LINE_HEIGHT[sz]
184
- )}
185
- >
186
- {#if showLineNumbers}
187
- <div
188
- bind:this={gutterEl}
189
- class={cx(
190
- "select-none px-3 py-[12px] border-r border-[var(--border-color-default)]",
191
- "text-[var(--color-text-muted)] text-right overflow-hidden",
192
- "cv-gutter bg-[var(--color-bg-surface)] tabular-nums h-full min-h-0"
193
- )}
194
- >
195
- {#each lines as _, i (i)}
196
- <div class={LINE_HEIGHT[sz]}>{i + 1}</div>
197
- {/each}
198
- </div>
199
- {/if}
200
-
201
- <div class="cv-editor relative flex-1 min-h-0">
202
- <div
203
- bind:this={highlightEl}
204
- class={cx("cv-highlight cv-layer", TEXT[sz], LINE_HEIGHT[sz])}
205
- class:cv-active-line={activeLine && editable}
206
- style={activeLine && editable
207
- ? `--cv-line-height: ${lineHeightPx}px; --cv-active-line-top: ${padTopPx + activeLineIndex * lineHeightPx - highlightScroll}px;`
208
- : undefined}
209
- aria-hidden="true"
210
- >
211
- <!-- eslint-disable-next-line svelte/no-at-html-tags -->
212
- {@html highlighted}
213
- </div>
214
-
215
- <textarea
216
- bind:this={textareaEl}
217
- bind:value={code}
218
- onscroll={syncScroll}
219
- oninput={editable ? updateActiveLine : undefined}
220
- onkeyup={editable ? updateActiveLine : undefined}
221
- onclick={editable ? updateActiveLine : undefined}
222
- onmouseup={editable ? updateActiveLine : undefined}
223
- onfocus={editable ? updateActiveLine : undefined}
224
- spellcheck="false"
225
- readonly={!editable}
226
- class={cx("cv-input cv-layer", TEXT[sz], LINE_HEIGHT[sz])}
227
- ></textarea>
228
- </div>
229
- </div>
230
- </div>
231
-
232
- <style>
233
- .cv-layer {
234
- position: absolute;
235
- padding: 12px;
236
- white-space: pre;
237
- box-sizing: border-box;
238
- font: inherit;
239
- line-height: inherit;
240
- }
241
-
242
- .cv-highlight {
243
- --cv-active-color: color-mix(
244
- in oklab,
245
- var(--color-text-default) 16%,
246
- transparent
247
- );
248
- inset: 0;
249
- overflow: auto;
250
- pointer-events: none;
251
- color: var(--color-text-default);
252
- background: transparent;
253
- padding-bottom: 100px;
254
- }
255
-
256
- .cv-active-line {
257
- background-image: linear-gradient(
258
- var(--cv-active-color),
259
- var(--cv-active-color)
260
- );
261
- background-repeat: no-repeat;
262
- background-size: 100% var(--cv-line-height);
263
- background-position: 0 var(--cv-active-line-top);
264
- }
265
-
266
- .cv-input {
267
- inset: 0;
268
- color: transparent;
269
- caret-color: var(--color-text-default);
270
- outline: none;
271
- resize: none;
272
- overflow: auto;
273
- border: none;
274
- box-sizing: border-box;
275
- }
276
-
277
- .cv-input:focus {
278
- outline: none;
279
- }
280
-
281
- .cv-input:focus-visible {
282
- outline: none !important;
283
- }
284
-
285
- /* Prism */
286
- .token.comment {
287
- color: oklch(0.937 0.019 256 / 0.45);
288
- }
289
- .token.punctuation {
290
- color: oklch(0.726 0.051 239);
291
- }
292
- .token.tag {
293
- color: oklch(0.725 0.192 338);
294
- }
295
- .token.attr-name {
296
- color: oklch(0.747 0.157 254);
297
- }
298
- .token.attr-value {
299
- color: oklch(0.835 0.181 139);
300
- }
301
- .token.string {
302
- color: oklch(0.835 0.181 139);
303
- }
304
- .token.keyword {
305
- color: oklch(0.701 0.206 27);
306
- }
307
- </style>
1
+ <!-- src/lib/CodeView.svelte -->
2
+ <script lang="ts">
3
+ /**
4
+ * @component CodeView
5
+ * @description CodeView is a small prism.js powered code block that supports syntax highlighting, optional editing, line numbers and active-line highlighting.
6
+ *
7
+ * @prop code {string} - Code content to render
8
+ * @default ""
9
+ *
10
+ * @prop language {Language} - Syntax highlighting language
11
+ * @options txt|html|css|js|json|python
12
+ * @default "txt"
13
+ *
14
+ * @prop title {string} - Title displayed above the code block
15
+ * @default "Code"
16
+ *
17
+ * @prop showCopyButton {boolean} - Shows the copy-to-clipboard button
18
+ * @default true
19
+ *
20
+ * @prop showLineNumbers {boolean} - Displays line numbers alongside the code
21
+ * @default false
22
+ *
23
+ * @prop editable {boolean} - Enables editable mode with a textarea overlay
24
+ * @default false
25
+ *
26
+ * @prop activeLine {boolean} - Highlights the current cursor line in editable mode
27
+ * @default false
28
+ *
29
+ * @prop sz {SizeKey} - Size preset affecting spacing and typography
30
+ * @options xs|sm|md|lg|xl
31
+ * @default md
32
+ *
33
+ * @prop class {string} - Extra classes applied to the root container
34
+ * @default ""
35
+ *
36
+ * @note Uses Prism for syntax highlighting; HTML/CSS/TXT grammars are bundled by default.
37
+ * @note Editable mode renders a transparent textarea above a highlighted code layer, mirroring real editors.
38
+ * @note Cursor-line highlight is overlaid using CSS variables and scroll-position tracking.
39
+ * @note Line numbers are fully scroll-synchronized with the editor content.
40
+ * @note Readonly mode shows static highlighted code, without textarea.
41
+ * @note Copy button writes the full code string to the clipboard.
42
+ * @note All sizing and spacing scale automatically with the shared `sz` token.
43
+ * @note Supports dark/light themes via existing design-token colors.
44
+ * @note Designed as a low-level editor component, not a full IDE replacement.
45
+ */
46
+ import { type SizeKey, type Language, TEXT } from "./types";
47
+ import * as Prism from "prismjs";
48
+ import "prismjs/components/prism-markup";
49
+ import "prismjs/components/prism-css";
50
+ import "prismjs/components/prism-javascript";
51
+ import "prismjs/components/prism-json";
52
+ import "prismjs/components/prism-python";
53
+ import "prismjs/themes/prism.css";
54
+ import { cx } from "../utils";
55
+
56
+ type Props = {
57
+ code?: string;
58
+ language?: Language;
59
+ title?: string;
60
+ showCopyButton?: boolean;
61
+ showLineNumbers?: boolean;
62
+ editable?: boolean;
63
+ activeLine?: boolean;
64
+ sz?: SizeKey;
65
+ class?: string;
66
+ };
67
+
68
+ let {
69
+ code = $bindable(""),
70
+ language = "txt",
71
+ title = "Code",
72
+ showCopyButton = true,
73
+ showLineNumbers = false,
74
+ editable = false,
75
+ activeLine = false,
76
+ sz = "md",
77
+ class: externalClass = "",
78
+ }: Props = $props();
79
+
80
+ let textareaEl = $state<HTMLTextAreaElement | null>(null);
81
+ let gutterEl = $state<HTMLDivElement | null>(null);
82
+ let highlightEl = $state<HTMLDivElement | null>(null);
83
+ let copied = $state(false);
84
+ let activeLineIndex = $state(0);
85
+ let highlightScroll = $state(0);
86
+ let padTopPx = $state(12);
87
+
88
+ const lines = $derived(code.split("\n"));
89
+
90
+ const LINE_HEIGHT: Record<SizeKey, string> = {
91
+ xs: "leading-4",
92
+ sm: "leading-[1.1rem]",
93
+ md: "leading-[1.3rem]",
94
+ lg: "leading-[1.45rem]",
95
+ xl: "leading-7",
96
+ };
97
+
98
+ let lineHeightPx = $state(20);
99
+
100
+ function escapeHtml(x: string) {
101
+ return x.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
102
+ }
103
+
104
+ function highlight(src: string, lang: Language) {
105
+ if (src === "") return "";
106
+ if (lang === "txt") return escapeHtml(src);
107
+ const key = lang === "html" ? "markup" : lang;
108
+ const grammar = Prism.languages[key];
109
+ return Prism.highlight(src, grammar, key);
110
+ }
111
+
112
+ const highlighted = $derived(highlight(code, language));
113
+
114
+ function updateActiveLine() {
115
+ if (!activeLine || !textareaEl) return;
116
+ const pos = textareaEl.selectionStart ?? 0;
117
+ const before = code.slice(0, pos);
118
+ activeLineIndex = before.split("\n").length - 1;
119
+ }
120
+
121
+ function syncScroll(event: Event) {
122
+ const el = event.currentTarget as HTMLElement;
123
+ if (gutterEl) gutterEl.scrollTop = el.scrollTop;
124
+ if (highlightEl) {
125
+ highlightEl.scrollTop = el.scrollTop;
126
+ highlightEl.scrollLeft = el.scrollLeft;
127
+ highlightScroll = el.scrollTop;
128
+ }
129
+ }
130
+
131
+ $effect(() => {
132
+ void sz;
133
+ if (!textareaEl) return;
134
+ const styles = getComputedStyle(textareaEl);
135
+ const lh = Number.parseFloat(styles.lineHeight);
136
+ if (!Number.isNaN(lh)) lineHeightPx = lh;
137
+ const pt = Number.parseFloat(styles.paddingTop);
138
+ if (!Number.isNaN(pt)) padTopPx = pt;
139
+ });
140
+
141
+ async function copyToClipboard() {
142
+ if (typeof navigator === "undefined" || !navigator.clipboard) return;
143
+ await navigator.clipboard.writeText(code);
144
+ copied = true;
145
+ setTimeout(() => (copied = false), 1200);
146
+ }
147
+ </script>
148
+
149
+ <div
150
+ class={cx(
151
+ "cv-root w-full h-full min-h-0 flex flex-col border border-[var(--border-color-default)] bg-[var(--color-bg-surface)]",
152
+ "text-[var(--color-text-default)]",
153
+ externalClass
154
+ )}
155
+ >
156
+ {#if title}
157
+ <div
158
+ class={cx(
159
+ "px-[calc(var(--spacing-sm)+var(--spacing-xs))] py-[var(--spacing-xs)] bg-[var(--color-bg-muted)] font-semibold uppercase flex items-center justify-between",
160
+ TEXT[sz]
161
+ )}
162
+ >
163
+ <div>{title}</div>
164
+
165
+ {#if showCopyButton}
166
+ <button
167
+ onclick={copyToClipboard}
168
+ class={cx(
169
+ "px-[calc(var(--spacing-sm)+var(--spacing-xs))] py-[calc(var(--spacing-xs)/2)] [font-size:var(--text-xs)] rounded-[var(--radius-sm)] bg-[var(--color-primary)] text-[var(--color-text-inverse,#fff)] hover:opacity-[var(--opacity-hover)]",
170
+ "transition focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] focus:outline-none"
171
+ )}
172
+ class:!bg-green-600={copied}
173
+ >
174
+ {copied ? "Copied" : "Copy"}
175
+ </button>
176
+ {/if}
177
+ </div>
178
+ {/if}
179
+
180
+ <div
181
+ class={cx(
182
+ "cv-body flex flex-1 min-h-0 font-mono",
183
+ TEXT[sz],
184
+ LINE_HEIGHT[sz]
185
+ )}
186
+ >
187
+ {#if showLineNumbers}
188
+ <div
189
+ bind:this={gutterEl}
190
+ class={cx(
191
+ "select-none px-[calc(var(--spacing-sm)+var(--spacing-xs))] py-[calc(var(--spacing-sm)+var(--spacing-xs))] border-r border-[var(--border-color-default)]",
192
+ "text-[var(--color-text-muted)] text-right overflow-hidden",
193
+ "cv-gutter bg-[var(--color-bg-surface)] tabular-nums h-full min-h-0"
194
+ )}
195
+ >
196
+ {#each lines as _, i (i)}
197
+ <div class={LINE_HEIGHT[sz]}>{i + 1}</div>
198
+ {/each}
199
+ </div>
200
+ {/if}
201
+
202
+ <div class="cv-editor relative flex-1 min-h-0">
203
+ <div
204
+ bind:this={highlightEl}
205
+ class={cx("cv-highlight cv-layer", TEXT[sz], LINE_HEIGHT[sz])}
206
+ class:cv-active-line={activeLine && editable}
207
+ style={activeLine && editable
208
+ ? `--cv-line-height: ${lineHeightPx}px; --cv-active-line-top: ${padTopPx + activeLineIndex * lineHeightPx - highlightScroll}px;`
209
+ : undefined}
210
+ aria-hidden="true"
211
+ >
212
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
213
+ {@html highlighted}
214
+ </div>
215
+
216
+ <textarea
217
+ bind:this={textareaEl}
218
+ bind:value={code}
219
+ onscroll={syncScroll}
220
+ oninput={editable ? updateActiveLine : undefined}
221
+ onkeyup={editable ? updateActiveLine : undefined}
222
+ onclick={editable ? updateActiveLine : undefined}
223
+ onmouseup={editable ? updateActiveLine : undefined}
224
+ onfocus={editable ? updateActiveLine : undefined}
225
+ spellcheck="false"
226
+ readonly={!editable}
227
+ class={cx("cv-input cv-layer", TEXT[sz], LINE_HEIGHT[sz])}
228
+ ></textarea>
229
+ </div>
230
+ </div>
231
+ </div>
232
+
233
+ <style>
234
+ .cv-layer {
235
+ position: absolute;
236
+ padding: calc(var(--spacing-sm) + var(--spacing-xs));
237
+ white-space: var(--code-white-space, pre);
238
+ box-sizing: border-box;
239
+ font: inherit;
240
+ line-height: inherit;
241
+ }
242
+
243
+ .cv-highlight {
244
+ --cv-active-color: color-mix(
245
+ in oklab,
246
+ var(--color-text-default) 16%,
247
+ transparent
248
+ );
249
+ inset: 0;
250
+ overflow: auto;
251
+ pointer-events: none;
252
+ color: var(--color-text-default);
253
+ background: transparent;
254
+ padding-bottom: 100px;
255
+ }
256
+
257
+ .cv-active-line {
258
+ background-image: linear-gradient(
259
+ var(--cv-active-color),
260
+ var(--cv-active-color)
261
+ );
262
+ background-repeat: no-repeat;
263
+ background-size: 100% var(--cv-line-height);
264
+ background-position: 0 var(--cv-active-line-top);
265
+ }
266
+
267
+ .cv-input {
268
+ inset: 0;
269
+ color: transparent;
270
+ caret-color: var(--color-text-default);
271
+ outline: none;
272
+ resize: none;
273
+ overflow: auto;
274
+ border: none;
275
+ box-sizing: border-box;
276
+ }
277
+
278
+ .cv-input:focus {
279
+ outline: none;
280
+ }
281
+
282
+ .cv-input:focus-visible {
283
+ outline: none !important;
284
+ }
285
+
286
+ /* Prism */
287
+ .token.comment {
288
+ color: oklch(0.937 0.019 256 / 0.45);
289
+ }
290
+ .token.punctuation {
291
+ color: oklch(0.726 0.051 239);
292
+ }
293
+ .token.tag {
294
+ color: oklch(0.725 0.192 338);
295
+ }
296
+ .token.attr-name {
297
+ color: oklch(0.747 0.157 254);
298
+ }
299
+ .token.attr-value {
300
+ color: oklch(0.835 0.181 139);
301
+ }
302
+ .token.string {
303
+ color: oklch(0.835 0.181 139);
304
+ }
305
+ .token.keyword {
306
+ color: oklch(0.701 0.206 27);
307
+ }
308
+ </style>