@marianmeres/stuic 3.191.0 → 3.192.0

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 (35) hide show
  1. package/AGENTS.md +2 -2
  2. package/API.md +29 -0
  3. package/dist/components/CodeBlock/CodeBlock.fixture.svelte +57 -0
  4. package/dist/components/CodeBlock/CodeBlock.fixture.svelte.d.ts +10 -0
  5. package/dist/components/CodeBlock/CodeBlock.svelte +441 -0
  6. package/dist/components/CodeBlock/CodeBlock.svelte.d.ts +113 -0
  7. package/dist/components/CodeBlock/README.md +336 -0
  8. package/dist/components/CodeBlock/_internal/lines.d.ts +13 -0
  9. package/dist/components/CodeBlock/_internal/lines.js +37 -0
  10. package/dist/components/CodeBlock/_internal/normalize-code.d.ts +9 -0
  11. package/dist/components/CodeBlock/_internal/normalize-code.js +39 -0
  12. package/dist/components/CodeBlock/_internal/paint.d.ts +20 -0
  13. package/dist/components/CodeBlock/_internal/paint.js +81 -0
  14. package/dist/components/CodeBlock/highlight/http.d.ts +6 -0
  15. package/dist/components/CodeBlock/highlight/http.js +62 -0
  16. package/dist/components/CodeBlock/highlight/index.d.ts +14 -0
  17. package/dist/components/CodeBlock/highlight/index.js +29 -0
  18. package/dist/components/CodeBlock/highlight/json.d.ts +9 -0
  19. package/dist/components/CodeBlock/highlight/json.js +74 -0
  20. package/dist/components/CodeBlock/highlight/shell.d.ts +8 -0
  21. package/dist/components/CodeBlock/highlight/shell.js +230 -0
  22. package/dist/components/CodeBlock/highlight/types.d.ts +17 -0
  23. package/dist/components/CodeBlock/highlight/types.js +1 -0
  24. package/dist/components/CodeBlock/i18n-sk.d.ts +17 -0
  25. package/dist/components/CodeBlock/i18n-sk.js +22 -0
  26. package/dist/components/CodeBlock/i18n.d.ts +37 -0
  27. package/dist/components/CodeBlock/i18n.js +34 -0
  28. package/dist/components/CodeBlock/index.css +470 -0
  29. package/dist/components/CodeBlock/index.d.ts +4 -0
  30. package/dist/components/CodeBlock/index.js +4 -0
  31. package/dist/index.css +1 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1 -0
  34. package/docs/domains/components.md +59 -1
  35. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  ```
25
25
  src/lib/
26
- ├── components/ # 82 component directories
26
+ ├── components/ # 83 component directories
27
27
  ├── actions/ # 16 Svelte actions (use: directives)
28
28
  ├── attachments/ # Svelte attachments ({@attach} — preferred for new DOM helpers)
29
29
  ├── utils/ # 55 utility modules (48 on the barrel)
@@ -146,7 +146,7 @@ so it is the only confusable pair — do not "fix" one into the other.
146
146
 
147
147
  ### Domain Docs
148
148
 
149
- - [Components](./docs/domains/components.md) — 82 component directories, Props pattern, snippets
149
+ - [Components](./docs/domains/components.md) — 83 component directories, Props pattern, snippets
150
150
  - [Theming](./docs/domains/theming.md) — CSS tokens, dark mode, themes
151
151
  - [CSS presets](./docs/domains/css-presets.md) — ratio-locked frame (letterbox), safe-area, scrollbar
152
152
  - [Actions](./docs/domains/actions.md) — 16 Svelte directives
package/API.md CHANGED
@@ -1273,6 +1273,35 @@ Horizontal schedule chart — project plans (task per row, progress, milestones)
1273
1273
 
1274
1274
  The geometry is exported separately for axis-aligned overlays: `buildGanttAxis`, `placeRange`, `placePoint`, `dayToFraction`, `boundsOf`.
1275
1275
 
1276
+ #### `CodeBlock`
1277
+
1278
+ Copyable code sample: a bordered box with a header (the language or a file name, or tabs for several samples, + a borderless `CopyButton`) over a `<pre><code>`. Rendered as text; copies exactly what is shown. JSON, HTTP and shell are syntax-highlighted by default with the CSS Custom Highlight API (no markup, no dependency; plain text where unsupported). The root carries `not-prose`.
1279
+
1280
+ | Prop | Type | Default | Description |
1281
+ | ----------------- | --------------------------------- | ------- | ------------------------------------------------------------------------------------- |
1282
+ | `code` | `string` | — | The sample (ignored with `samples`) |
1283
+ | `lang` | `string` | — | Header label, what is highlighted, `data-lang`, `language-{lang}` class |
1284
+ | `title` | `THC` | — | Header label in place of `lang`; with `samples`, shown before the tabs |
1285
+ | `samples` | `CodeBlockSample[]` | — | `{ code, lang?, label?, id?, highlightLines?, copyText? }[]` behind tabs |
1286
+ | `active` | `string` | — | Shown sample's id (bindable) — share one bound value to sync blocks |
1287
+ | `highlight` | `boolean \| CodeBlockHighlighter` | `true` | Built-in `highlightCode`, your own `(code, lang) => [start, end, type][]`, or off |
1288
+ | `lineNumbers` | `boolean` | `false` | Line numbers (not selectable, not copied); `lineNumbersStart` offsets them |
1289
+ | `highlightLines` | `number[] \| string` | — | Lines to mark, as positions in the sample (`"1, 3-5"`) |
1290
+ | `collapsedLines` | `number` | — | Collapse longer samples to N lines with a "Show all N lines" toggle (`bind:expanded`) |
1291
+ | `verbatim` | `boolean` | `false` | Keep `code` as given (default: drop blank end lines and the shared indent) |
1292
+ | `wrap` | `boolean` | `false` | Soft-wrap instead of scrolling horizontally |
1293
+ | `copy` | `boolean` | `true` | Render the copy button |
1294
+ | `copyButtonProps` | `Partial<CopyButtonProps>` | — | Pass-through to the `CopyButton`; its `text` overrides what is copied |
1295
+ | `t` | `TranslateFn` | English | `createCodeBlockT(CODE_BLOCK_MESSAGES_SK)` — also localizes the copy button |
1296
+
1297
+ Class slots: `class`, `classHeader`, `classTitle`, `classTabs`, `classTab`, `classPre`, `classCode`, `classLine`, `classFooter`, `classToggle`. Also exported: `highlightCode`, `highlightJson`, `highlightHttp`, `highlightShell`, `HIGHLIGHT_CODE_LANGS`, types `CodeBlockSample`, `CodeBlockToken`, `CodeBlockTokenType`, `CodeBlockHighlighter`.
1298
+
1299
+ ```svelte
1300
+ <CodeBlock lang="bash" code={quickstart} />
1301
+ <CodeBlock lang="json" title="deno.json" code={config} lineNumbers highlightLines="2-3" />
1302
+ <CodeBlock title="Create an item" {samples} bind:active={language} collapsedLines={15} />
1303
+ ```
1304
+
1276
1305
  #### `ListGroup`
1277
1306
 
1278
1307
  Bordered, rounded box of rows split by hairlines, with an optional header (title + aside) and footer. Each row is one wrapping flex line of parts; `data-grow` on the part that should absorb the slack. Generic over the item type.
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ // Harness for what `rerender()` can't drive: `rerender()` re-signals every prop (so it
3
+ // would re-run the measuring and painting effects on its own and hide a missing
4
+ // dependency on the code), and it can't observe a written `$bindable`.
5
+ import CodeBlock, { type CodeBlockSample } from "./CodeBlock.svelte";
6
+
7
+ let {
8
+ mode = "swap",
9
+ initial = "",
10
+ next = "",
11
+ lang,
12
+ }: {
13
+ mode?: "swap" | "sync" | "expanded";
14
+ initial?: string;
15
+ next?: string;
16
+ lang?: string;
17
+ } = $props();
18
+
19
+ // swap: `code` alone changes, from inside
20
+ // svelte-ignore state_referenced_locally
21
+ let code = $state(initial);
22
+ let lineNumbers = $state(false);
23
+
24
+ // sync: two tabbed blocks share one bound `active`
25
+ let active = $state<string | undefined>();
26
+ const first: CodeBlockSample[] = [
27
+ { label: "curl", lang: "bash", code: "curl https://x.test" },
28
+ { label: "Python", lang: "python", code: "requests.get('https://x.test')" },
29
+ ];
30
+ const second: CodeBlockSample[] = [
31
+ { label: "curl", lang: "bash", code: "curl -X POST https://x.test" },
32
+ { label: "fetch", lang: "js", code: "await fetch('https://x.test')" },
33
+ ];
34
+
35
+ // expanded: a written bindable, mirrored out
36
+ let expanded = $state(false);
37
+ </script>
38
+
39
+ {#if mode === "swap"}
40
+ <button type="button" onclick={() => (code = code === initial ? next : initial)}
41
+ >swap</button
42
+ >
43
+ <button type="button" onclick={() => (lineNumbers = !lineNumbers)}>numbers</button>
44
+ <CodeBlock {code} {lang} {lineNumbers} copy={false} style="width: 240px" />
45
+ {:else if mode === "sync"}
46
+ <output data-testid="active">{active ?? ""}</output>
47
+ <CodeBlock samples={first} bind:active copy={false} data-testid="first" />
48
+ <CodeBlock samples={second} bind:active copy={false} data-testid="second" />
49
+ {:else}
50
+ <output data-testid="expanded">{expanded}</output>
51
+ <CodeBlock
52
+ code={Array.from({ length: 12 }, (_, i) => `line ${i + 1}`).join("\n")}
53
+ collapsedLines={4}
54
+ bind:expanded
55
+ copy={false}
56
+ />
57
+ {/if}
@@ -0,0 +1,10 @@
1
+ import CodeBlock from "./CodeBlock.svelte";
2
+ type $$ComponentProps = {
3
+ mode?: "swap" | "sync" | "expanded";
4
+ initial?: string;
5
+ next?: string;
6
+ lang?: string;
7
+ };
8
+ declare const CodeBlock: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type CodeBlock = ReturnType<typeof CodeBlock>;
10
+ export default CodeBlock;
@@ -0,0 +1,441 @@
1
+ <script lang="ts" module>
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import type { THC } from "../Thc/Thc.svelte";
4
+ import type { Props as CopyButtonProps } from "../CopyButton/CopyButton.svelte";
5
+ import type { TranslateFn } from "../../types.js";
6
+ import type { CodeBlockHighlighter, CodeBlockToken } from "./highlight/types.js";
7
+
8
+ /** One sample of a tabbed block — the same thing in another language or tool. */
9
+ export interface CodeBlockSample {
10
+ /** The sample (rendered as text) */
11
+ code: string;
12
+ /** What it is written in — the tab label (unless `label`), and what is highlighted */
13
+ lang?: string;
14
+ /** The tab label, in place of `lang` */
15
+ label?: THC;
16
+ /**
17
+ * Identity for `active`. Defaults to a string `label`, else `lang`, else the index —
18
+ * so blocks sharing one bound `active` stay in sync by what the reader sees.
19
+ */
20
+ id?: string;
21
+ /** Lines to highlight in this sample (overrides the block's `highlightLines`) */
22
+ highlightLines?: number[] | string;
23
+ /** What the copy button copies for this sample, in place of the displayed text */
24
+ copyText?: string;
25
+ }
26
+
27
+ export interface Props extends Omit<
28
+ HTMLAttributes<HTMLDivElement>,
29
+ "children" | "title"
30
+ > {
31
+ /** The sample. Always rendered as text, never as HTML. Ignored with `samples`. */
32
+ code?: string;
33
+ /**
34
+ * What the sample is written in. A label, and the highlighter's hint: it is the header
35
+ * label (unless `title` is set), `data-lang` on the root, and a `language-*` class on
36
+ * the `<code>`. Ignored with `samples` (each has its own).
37
+ */
38
+ lang?: string;
39
+ /**
40
+ * Header label in place of `lang` — e.g. a file name. `""` hides the label. With
41
+ * `samples` it is shown before the tabs and names the tab list.
42
+ */
43
+ title?: THC;
44
+ /** Several samples of the same thing (curl / fetch / Python…), switched by tabs */
45
+ samples?: CodeBlockSample[];
46
+ /**
47
+ * The id of the shown sample (see `CodeBlockSample.id`); bindable. Unset → the first
48
+ * sample. An id this block doesn't have leaves it on the sample it showed, and the
49
+ * value is never rewritten — so several blocks can share one bound `active`.
50
+ */
51
+ active?: string;
52
+ /**
53
+ * Render the code exactly as given. By default the blank lines at both ends and the
54
+ * indentation all lines share are dropped (from the display and the copy alike).
55
+ */
56
+ verbatim?: boolean;
57
+ /** Soft-wrap long lines instead of scrolling horizontally */
58
+ wrap?: boolean;
59
+ /**
60
+ * Syntax highlighting. `true` (default): the built-in `highlightCode` (JSON, HTTP,
61
+ * shell — other languages stay plain). A function: your own tokenizer. `false`: off.
62
+ * Painted with the CSS Custom Highlight API — no markup; browsers without it show
63
+ * plain text.
64
+ */
65
+ highlight?: boolean | CodeBlockHighlighter;
66
+ /** Show line numbers (not selectable, not copied) */
67
+ lineNumbers?: boolean;
68
+ /** The first line's number (default `1`) */
69
+ lineNumbersStart?: number;
70
+ /**
71
+ * Lines to highlight: 1-based positions in the sample (not the displayed numbers),
72
+ * as an array or a string like `"1, 3-5"`.
73
+ */
74
+ highlightLines?: number[] | string;
75
+ /**
76
+ * Collapse samples longer than this many lines to this many, with a "Show all N
77
+ * lines" toggle.
78
+ */
79
+ collapsedLines?: number;
80
+ /** Whether a collapsible sample is expanded; bindable */
81
+ expanded?: boolean;
82
+ /** Render the copy button (default `true`) */
83
+ copy?: boolean;
84
+ /**
85
+ * Props for the copy button (a `CopyButton`: `label`, `variant`, `onCopied`, …). A
86
+ * `text` here overrides what gets copied — e.g. a shell sample without its `$ `
87
+ * prompts (a sample's own `copyText` wins over it).
88
+ */
89
+ copyButtonProps?: Partial<CopyButtonProps>;
90
+ /** i18n translate function (see `createCodeBlockT`); also passed to the copy button */
91
+ t?: TranslateFn;
92
+ /** Skip all default styling */
93
+ unstyled?: boolean;
94
+ /** Additional CSS classes for the root */
95
+ class?: string;
96
+ /** Class for the header row */
97
+ classHeader?: string;
98
+ /** Class for the header label */
99
+ classTitle?: string;
100
+ /** Class for the tab list */
101
+ classTabs?: string;
102
+ /** Class for every tab */
103
+ classTab?: string;
104
+ /** Class for the `<pre>` */
105
+ classPre?: string;
106
+ /** Class for the `<code>` */
107
+ classCode?: string;
108
+ /** Class for every line (only rendered with `lineNumbers` / `highlightLines`) */
109
+ classLine?: string;
110
+ /** Class for the footer holding the collapse toggle */
111
+ classFooter?: string;
112
+ /** Class for the collapse toggle */
113
+ classToggle?: string;
114
+ /** Bindable root element reference */
115
+ el?: HTMLDivElement;
116
+ }
117
+ </script>
118
+
119
+ <script lang="ts">
120
+ import { tick } from "svelte";
121
+ import { twMerge } from "../../utils/tw-merge.js";
122
+ import { getId } from "../../utils/get-id.js";
123
+ import { iconChevronDown } from "../../icons/index.js";
124
+ import Thc, { isTHCNotEmpty } from "../Thc/Thc.svelte";
125
+ import Button from "../Button/Button.svelte";
126
+ import CopyButton from "../CopyButton/CopyButton.svelte";
127
+ import { highlightCode } from "./highlight/index.js";
128
+ import { normalizeCode } from "./_internal/normalize-code.js";
129
+ import { parseLineSet, splitLines } from "./_internal/lines.js";
130
+ import { paintTokens } from "./_internal/paint.js";
131
+ import { t_default } from "./i18n.js";
132
+
133
+ let {
134
+ code,
135
+ lang,
136
+ title,
137
+ samples,
138
+ active = $bindable(),
139
+ verbatim = false,
140
+ wrap = false,
141
+ highlight = true,
142
+ lineNumbers = false,
143
+ lineNumbersStart = 1,
144
+ highlightLines,
145
+ collapsedLines,
146
+ expanded = $bindable(false),
147
+ copy = true,
148
+ copyButtonProps,
149
+ t = t_default,
150
+ unstyled = false,
151
+ class: classProp,
152
+ classHeader: classHeaderProp,
153
+ classTitle: classTitleProp,
154
+ classTabs: classTabsProp,
155
+ classTab: classTabProp,
156
+ classPre: classPreProp,
157
+ classCode: classCodeProp,
158
+ classLine: classLineProp,
159
+ classFooter: classFooterProp,
160
+ classToggle: classToggleProp,
161
+ el = $bindable(),
162
+ ...rest
163
+ }: Props = $props();
164
+
165
+ const uid = getId("stuic-code-block-");
166
+ const preId = `${uid}-code`;
167
+ const titleId = `${uid}-title`;
168
+ const tabId = (i: number) => `${uid}-tab-${i}`;
169
+
170
+ // --- samples (tabs) ---------------------------------------------------------------
171
+
172
+ let _samples = $derived(samples?.length ? samples : undefined);
173
+
174
+ // Ids as documented, deduplicated so every tab stays selectable.
175
+ let sampleIds = $derived.by(() => {
176
+ const seen = new Set<string>();
177
+ return (_samples ?? []).map((s, i) => {
178
+ let id =
179
+ s.id ?? (typeof s.label === "string" && s.label ? s.label : s.lang) ?? `${i}`;
180
+ if (seen.has(id)) id = `${id}-${i}`;
181
+ seen.add(id);
182
+ return id;
183
+ });
184
+ });
185
+
186
+ // An `active` this block doesn't have (another block's language, when several share
187
+ // one bound value) keeps showing what it showed — initially the first sample.
188
+ let lastIndex = 0;
189
+ let activeIndex = $derived.by(() => {
190
+ if (!_samples) return -1;
191
+ const i = sampleIds.indexOf(active as string);
192
+ if (i >= 0) lastIndex = i;
193
+ return i >= 0 ? i : Math.min(lastIndex, _samples.length - 1);
194
+ });
195
+ let current = $derived(_samples?.[activeIndex]);
196
+
197
+ // Keyed by index, written through `bind:this`; only read in handlers.
198
+ const tabEls: HTMLButtonElement[] = [];
199
+
200
+ function selectTab(i: number, focus = false) {
201
+ active = sampleIds[i];
202
+ if (focus) tabEls[i]?.focus();
203
+ }
204
+
205
+ function onTabKeydown(e: KeyboardEvent, i: number) {
206
+ const n = sampleIds.length;
207
+ const next =
208
+ e.key === "ArrowRight"
209
+ ? (i + 1) % n
210
+ : e.key === "ArrowLeft"
211
+ ? (i - 1 + n) % n
212
+ : e.key === "Home"
213
+ ? 0
214
+ : e.key === "End"
215
+ ? n - 1
216
+ : -1;
217
+ if (next < 0) return;
218
+ e.preventDefault();
219
+ selectTab(next, true);
220
+ }
221
+
222
+ // --- the displayed text -------------------------------------------------------------
223
+
224
+ let _code = $derived(current ? current.code : (code ?? ""));
225
+ let _lang = $derived(current ? current.lang : lang);
226
+ let text = $derived(verbatim ? (_code ?? "") : normalizeCode(_code));
227
+ let lines = $derived(splitLines(text));
228
+
229
+ let _highlighted = $derived(
230
+ parseLineSet(current?.highlightLines ?? highlightLines, lines.length)
231
+ );
232
+ // one <span> per line only when something needs it; otherwise a single text node
233
+ let lined = $derived(lineNumbers || _highlighted.size > 0);
234
+ let digits = $derived(String(lineNumbersStart + Math.max(lines.length, 1) - 1).length);
235
+
236
+ let _title = $derived(_samples ? title : (title ?? lang));
237
+ let hasTitle = $derived(isTHCNotEmpty(_title));
238
+
239
+ let copyText = $derived(current?.copyText ?? copyButtonProps?.text ?? text);
240
+
241
+ // --- collapse ---------------------------------------------------------------------
242
+
243
+ let collapsible = $derived(
244
+ !!collapsedLines && collapsedLines > 0 && lines.length > collapsedLines
245
+ );
246
+ let collapsed = $derived(collapsible && !expanded);
247
+
248
+ async function toggle() {
249
+ expanded = !expanded;
250
+ if (!expanded) {
251
+ // collapsing a long sample can leave the reader far below the block
252
+ await tick();
253
+ if (el && el.getBoundingClientRect().top < 0)
254
+ el.scrollIntoView({ block: "nearest" });
255
+ }
256
+ }
257
+
258
+ // --- keyboard reachability ----------------------------------------------------------
259
+
260
+ // A scroll container must be reachable by keyboard, or its overflow can't be read
261
+ // without a mouse. Measured rather than always on, so a sample that fits costs no tab
262
+ // stop. Re-measured on resize and whenever the content or the layout changes (a longer
263
+ // single line does not resize the box). A collapsed sample's vertical overflow is
264
+ // hidden, not scrollable — the toggle is its way in. A tab panel is always a tab stop.
265
+ let preEl = $state<HTMLPreElement>();
266
+ let scrollable = $state(false);
267
+ $effect(() => {
268
+ const pre = preEl;
269
+ if (!pre) return;
270
+ void text;
271
+ void wrap;
272
+ void lined;
273
+ const isCollapsed = collapsed;
274
+ const measure = () => {
275
+ scrollable =
276
+ pre.scrollWidth > pre.clientWidth ||
277
+ (!isCollapsed && pre.scrollHeight > pre.clientHeight);
278
+ };
279
+ measure();
280
+ const ro = new ResizeObserver(measure);
281
+ ro.observe(pre);
282
+ return () => ro.disconnect();
283
+ });
284
+
285
+ // --- syntax highlighting ------------------------------------------------------------
286
+
287
+ let codeEl = $state<HTMLElement>();
288
+ $effect(() => {
289
+ const root = codeEl;
290
+ const fn = highlight === true ? highlightCode : highlight || undefined;
291
+ if (!root || !fn) return;
292
+ void lined; // the DOM structure the ranges point into
293
+ // the ranges index the rendered text — never paint a DOM that disagrees with it
294
+ if (root.textContent !== text) return;
295
+ let tokens: CodeBlockToken[];
296
+ try {
297
+ tokens = fn(text, _lang);
298
+ } catch (e) {
299
+ // a failing highlighter must not take the sample down with it
300
+ console.error(e);
301
+ return;
302
+ }
303
+ return paintTokens(root, tokens ?? []);
304
+ });
305
+
306
+ // --- classes ------------------------------------------------------------------------
307
+
308
+ // Under `unstyled` a part keeps only the consumer's classes — `undefined` rather than
309
+ // `class=""` when there are none.
310
+ const _cls = (base: string, ...extra: (string | undefined)[]) =>
311
+ (unstyled ? twMerge(...extra) : twMerge(base, ...extra)) || undefined;
312
+
313
+ // `not-prose`: a code block is the component most likely to sit in a typography
314
+ // column, whose plugin would otherwise restyle the <pre>, the <code> (adding
315
+ // backticks) and the button. Inert where the plugin isn't installed.
316
+ let _class = $derived(
317
+ unstyled ? classProp : twMerge("stuic-code-block not-prose", classProp)
318
+ );
319
+ let _classHeader = $derived(_cls("stuic-code-block-header", classHeaderProp));
320
+ let _classTitle = $derived(_cls("stuic-code-block-title", classTitleProp));
321
+ let _classTabs = $derived(_cls("stuic-code-block-tabs", classTabsProp));
322
+ let _classTab = $derived(_cls("stuic-code-block-tab", classTabProp));
323
+ let _classPre = $derived(_cls("stuic-code-block-pre", classPreProp));
324
+ // the `language-*` class is semantics, not styling — it survives `unstyled`
325
+ let _classCode = $derived(
326
+ _cls(
327
+ "stuic-code-block-code",
328
+ _lang ? `language-${_lang.trim().replace(/\s+/g, "-")}` : undefined,
329
+ classCodeProp
330
+ )
331
+ );
332
+ let _classLine = $derived(_cls("stuic-code-block-line", classLineProp));
333
+ let _classFooter = $derived(_cls("stuic-code-block-footer", classFooterProp));
334
+ let _classToggle = $derived(_cls("stuic-code-block-toggle", classToggleProp));
335
+ let _classCopy = $derived(_cls("stuic-code-block-copy", copyButtonProps?.class));
336
+ </script>
337
+
338
+ <div
339
+ bind:this={el}
340
+ class={_class}
341
+ data-lang={_lang || undefined}
342
+ data-wrap={wrap ? "true" : undefined}
343
+ data-samples={_samples ? "true" : undefined}
344
+ data-copy={copy ? "true" : undefined}
345
+ data-lines={lined ? "true" : undefined}
346
+ data-line-numbers={lined && lineNumbers ? "true" : undefined}
347
+ data-collapsed={collapsed ? "true" : undefined}
348
+ {...rest}
349
+ >
350
+ <!-- A <div>, not a <header>: outside <main>/sectioning content a <header> is a
351
+ `banner` landmark, one per block. -->
352
+ {#if hasTitle || _samples || copy}
353
+ <div class={_classHeader}>
354
+ {#if hasTitle}
355
+ <span id={_samples ? titleId : undefined} class={_classTitle}
356
+ ><Thc thc={_title!} /></span
357
+ >
358
+ {/if}
359
+ {#if _samples}
360
+ <div
361
+ role="tablist"
362
+ aria-labelledby={hasTitle ? titleId : undefined}
363
+ class={_classTabs}
364
+ >
365
+ {#each _samples as sample, i (sampleIds[i])}
366
+ <button
367
+ bind:this={tabEls[i]}
368
+ type="button"
369
+ role="tab"
370
+ id={tabId(i)}
371
+ class={_classTab}
372
+ aria-selected={i === activeIndex}
373
+ aria-controls={preId}
374
+ tabindex={i === activeIndex ? 0 : -1}
375
+ data-active={i === activeIndex ? "true" : undefined}
376
+ onclick={() => selectTab(i)}
377
+ onkeydown={(e) => onTabKeydown(e, i)}
378
+ >
379
+ <Thc thc={sample.label ?? sample.lang ?? `${i + 1}`} />
380
+ </button>
381
+ {/each}
382
+ </div>
383
+ {/if}
384
+ {#if copy}
385
+ <CopyButton
386
+ label
387
+ variant="ghost"
388
+ size="sm"
389
+ {t}
390
+ {unstyled}
391
+ {...copyButtonProps}
392
+ text={copyText}
393
+ class={_classCopy}
394
+ />
395
+ {/if}
396
+ </div>
397
+ {/if}
398
+ <!-- tabindex: while it scrolls (axe `scrollable-region-focusable`), or as a tab panel -->
399
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
400
+ <pre
401
+ bind:this={preEl}
402
+ id={preId}
403
+ class={_classPre}
404
+ role={_samples ? "tabpanel" : undefined}
405
+ aria-labelledby={_samples ? tabId(activeIndex) : undefined}
406
+ tabindex={_samples || scrollable ? 0 : undefined}
407
+ style:--_collapsed-lines={collapsed ? collapsedLines : undefined}
408
+ style:--_digits={lined && lineNumbers ? digits : undefined}><code
409
+ bind:this={codeEl}
410
+ class={_classCode}
411
+ >{#if lined}{#each lines as line, i}<span
412
+ class={_classLine}
413
+ data-line={lineNumbers ? lineNumbersStart + i : undefined}
414
+ data-highlighted={_highlighted.has(i + 1) ? "true" : undefined}>{line}</span
415
+ >{/each}{:else}{text}{/if}</code
416
+ ></pre>
417
+ {#if collapsible}
418
+ <div class={_classFooter}>
419
+ <Button
420
+ type="button"
421
+ variant="ghost"
422
+ size="sm"
423
+ class={_classToggle}
424
+ aria-expanded={expanded}
425
+ aria-controls={preId}
426
+ {unstyled}
427
+ onclick={toggle}
428
+ >
429
+ <span
430
+ class={unstyled ? undefined : "stuic-code-block-toggle-icon"}
431
+ aria-hidden="true">{@html iconChevronDown()}</span
432
+ >
433
+ <span
434
+ >{expanded
435
+ ? t("show_less")
436
+ : t("show_all_lines", { count: lines.length })}</span
437
+ >
438
+ </Button>
439
+ </div>
440
+ {/if}
441
+ </div>
@@ -0,0 +1,113 @@
1
+ import type { HTMLAttributes } from "svelte/elements";
2
+ import type { THC } from "../Thc/Thc.svelte";
3
+ import type { Props as CopyButtonProps } from "../CopyButton/CopyButton.svelte";
4
+ import type { TranslateFn } from "../../types.js";
5
+ import type { CodeBlockHighlighter } from "./highlight/types.js";
6
+ /** One sample of a tabbed block — the same thing in another language or tool. */
7
+ export interface CodeBlockSample {
8
+ /** The sample (rendered as text) */
9
+ code: string;
10
+ /** What it is written in — the tab label (unless `label`), and what is highlighted */
11
+ lang?: string;
12
+ /** The tab label, in place of `lang` */
13
+ label?: THC;
14
+ /**
15
+ * Identity for `active`. Defaults to a string `label`, else `lang`, else the index —
16
+ * so blocks sharing one bound `active` stay in sync by what the reader sees.
17
+ */
18
+ id?: string;
19
+ /** Lines to highlight in this sample (overrides the block's `highlightLines`) */
20
+ highlightLines?: number[] | string;
21
+ /** What the copy button copies for this sample, in place of the displayed text */
22
+ copyText?: string;
23
+ }
24
+ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "title"> {
25
+ /** The sample. Always rendered as text, never as HTML. Ignored with `samples`. */
26
+ code?: string;
27
+ /**
28
+ * What the sample is written in. A label, and the highlighter's hint: it is the header
29
+ * label (unless `title` is set), `data-lang` on the root, and a `language-*` class on
30
+ * the `<code>`. Ignored with `samples` (each has its own).
31
+ */
32
+ lang?: string;
33
+ /**
34
+ * Header label in place of `lang` — e.g. a file name. `""` hides the label. With
35
+ * `samples` it is shown before the tabs and names the tab list.
36
+ */
37
+ title?: THC;
38
+ /** Several samples of the same thing (curl / fetch / Python…), switched by tabs */
39
+ samples?: CodeBlockSample[];
40
+ /**
41
+ * The id of the shown sample (see `CodeBlockSample.id`); bindable. Unset → the first
42
+ * sample. An id this block doesn't have leaves it on the sample it showed, and the
43
+ * value is never rewritten — so several blocks can share one bound `active`.
44
+ */
45
+ active?: string;
46
+ /**
47
+ * Render the code exactly as given. By default the blank lines at both ends and the
48
+ * indentation all lines share are dropped (from the display and the copy alike).
49
+ */
50
+ verbatim?: boolean;
51
+ /** Soft-wrap long lines instead of scrolling horizontally */
52
+ wrap?: boolean;
53
+ /**
54
+ * Syntax highlighting. `true` (default): the built-in `highlightCode` (JSON, HTTP,
55
+ * shell — other languages stay plain). A function: your own tokenizer. `false`: off.
56
+ * Painted with the CSS Custom Highlight API — no markup; browsers without it show
57
+ * plain text.
58
+ */
59
+ highlight?: boolean | CodeBlockHighlighter;
60
+ /** Show line numbers (not selectable, not copied) */
61
+ lineNumbers?: boolean;
62
+ /** The first line's number (default `1`) */
63
+ lineNumbersStart?: number;
64
+ /**
65
+ * Lines to highlight: 1-based positions in the sample (not the displayed numbers),
66
+ * as an array or a string like `"1, 3-5"`.
67
+ */
68
+ highlightLines?: number[] | string;
69
+ /**
70
+ * Collapse samples longer than this many lines to this many, with a "Show all N
71
+ * lines" toggle.
72
+ */
73
+ collapsedLines?: number;
74
+ /** Whether a collapsible sample is expanded; bindable */
75
+ expanded?: boolean;
76
+ /** Render the copy button (default `true`) */
77
+ copy?: boolean;
78
+ /**
79
+ * Props for the copy button (a `CopyButton`: `label`, `variant`, `onCopied`, …). A
80
+ * `text` here overrides what gets copied — e.g. a shell sample without its `$ `
81
+ * prompts (a sample's own `copyText` wins over it).
82
+ */
83
+ copyButtonProps?: Partial<CopyButtonProps>;
84
+ /** i18n translate function (see `createCodeBlockT`); also passed to the copy button */
85
+ t?: TranslateFn;
86
+ /** Skip all default styling */
87
+ unstyled?: boolean;
88
+ /** Additional CSS classes for the root */
89
+ class?: string;
90
+ /** Class for the header row */
91
+ classHeader?: string;
92
+ /** Class for the header label */
93
+ classTitle?: string;
94
+ /** Class for the tab list */
95
+ classTabs?: string;
96
+ /** Class for every tab */
97
+ classTab?: string;
98
+ /** Class for the `<pre>` */
99
+ classPre?: string;
100
+ /** Class for the `<code>` */
101
+ classCode?: string;
102
+ /** Class for every line (only rendered with `lineNumbers` / `highlightLines`) */
103
+ classLine?: string;
104
+ /** Class for the footer holding the collapse toggle */
105
+ classFooter?: string;
106
+ /** Class for the collapse toggle */
107
+ classToggle?: string;
108
+ /** Bindable root element reference */
109
+ el?: HTMLDivElement;
110
+ }
111
+ declare const CodeBlock: import("svelte").Component<Props, {}, "el" | "active" | "expanded">;
112
+ type CodeBlock = ReturnType<typeof CodeBlock>;
113
+ export default CodeBlock;