svelte-comp 1.3.5 → 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.
- package/LICENSE.md +21 -21
- package/README.md +101 -101
- package/dist/App.svelte +1046 -1046
- package/dist/Container.svelte +59 -59
- package/dist/app.css +234 -234
- package/dist/app.d.ts +10 -10
- package/dist/lib/Accordion.svelte +155 -155
- package/dist/lib/Badge.svelte +44 -44
- package/dist/lib/Button.svelte +185 -185
- package/dist/lib/Calendar.svelte +384 -384
- package/dist/lib/Card.svelte +103 -103
- package/dist/lib/Carousel.svelte +293 -293
- package/dist/lib/CheckBox.svelte +210 -210
- package/dist/lib/CodeView.svelte +308 -308
- package/dist/lib/ColorPicker.svelte +159 -159
- package/dist/lib/ContextMenu.svelte +328 -328
- package/dist/lib/DatePicker.svelte +246 -246
- package/dist/lib/Dialog.svelte +233 -233
- package/dist/lib/Field.svelte +299 -299
- package/dist/lib/FilePicker.svelte +295 -295
- package/dist/lib/Form.svelte +438 -438
- package/dist/lib/Hamburger.svelte +217 -217
- package/dist/lib/InstallPWA.svelte +94 -94
- package/dist/lib/Menu.svelte +623 -623
- package/dist/lib/NoticeBase.svelte +140 -140
- package/dist/lib/PaginatedCard.svelte +73 -73
- package/dist/lib/Pagination.svelte +119 -119
- package/dist/lib/PrimaryColorSelect.svelte +111 -111
- package/dist/lib/ProgressBar.svelte +141 -141
- package/dist/lib/ProgressCircle.svelte +190 -190
- package/dist/lib/Radio.svelte +189 -189
- package/dist/lib/SearchInput.svelte +104 -104
- package/dist/lib/Select.svelte +524 -524
- package/dist/lib/Slider.svelte +253 -253
- package/dist/lib/Splitter.svelte +159 -159
- package/dist/lib/Switch.svelte +168 -168
- package/dist/lib/Table.svelte +299 -299
- package/dist/lib/Tabs.svelte +213 -213
- package/dist/lib/ThemeToggle.svelte +128 -128
- package/dist/lib/TimePicker.svelte +312 -312
- package/dist/lib/TimePickerNew.svelte +634 -634
- package/dist/lib/Toast.svelte +123 -123
- package/dist/lib/Tooltip.svelte +110 -110
- package/dist/lib/Topbar.svelte +112 -112
- package/dist/styles.css +234 -234
- package/package.json +52 -52
package/dist/lib/CodeView.svelte
CHANGED
|
@@ -1,308 +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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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>
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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>
|