slexkit 0.2.0 → 0.3.1

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 (104) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +21 -21
  3. package/README.md +4 -3
  4. package/README.zh-CN.md +4 -3
  5. package/dist/ai/llms-authoring.txt +2 -0
  6. package/dist/ai/llms-capabilities.txt +126 -0
  7. package/dist/ai/llms-components.txt +29 -7
  8. package/dist/ai/llms-full.txt +1909 -153
  9. package/dist/ai/llms-runtime.txt +18 -13
  10. package/dist/ai/llms.txt +22 -1
  11. package/dist/ai/slexkit-ai-manifest.json +717 -62
  12. package/dist/base.css +359 -359
  13. package/dist/chunks/{accordion-cfjyxw93.js → button-53ccjq5p.js} +11 -11
  14. package/dist/chunks/{accordion-nw12ytps.js → button-cr1fhsa7.js} +48 -2
  15. package/dist/chunks/{accordion-5f0nvjjm.js → button-dsftwzvg.js} +4 -3
  16. package/dist/chunks/{accordion-hzyrngd6.js → button-faf563xf.js} +2 -2
  17. package/dist/chunks/{accordion-ehnhpeca.js → button-jxv4c65t.js} +2 -2
  18. package/dist/chunks/{accordion-cw5r75jm.js → button-xv2dz7vn.js} +1 -1
  19. package/dist/chunks/{accordion-830dw78f.js → button-z5yv24ks.js} +2 -2
  20. package/dist/components/accordion.js +2 -2
  21. package/dist/components/badge.js +2 -2
  22. package/dist/components/button.css +101 -101
  23. package/dist/components/button.js +3 -3
  24. package/dist/components/callout.js +4 -4
  25. package/dist/components/card.js +2 -2
  26. package/dist/components/checkbox.js +3 -2
  27. package/dist/components/choice.css +151 -151
  28. package/dist/components/code-block.js +2 -2
  29. package/dist/components/collapsible.js +2 -2
  30. package/dist/components/column.js +1 -1
  31. package/dist/components/content.css +273 -250
  32. package/dist/components/display.css +1 -1
  33. package/dist/components/divider.js +2 -2
  34. package/dist/components/grid.js +1 -1
  35. package/dist/components/index.js +13994 -172
  36. package/dist/components/input.css +786 -852
  37. package/dist/components/input.js +34 -144
  38. package/dist/components/link.js +2 -2
  39. package/dist/components/progress.js +2 -2
  40. package/dist/components/radio-group.js +3 -2
  41. package/dist/components/row.js +1 -1
  42. package/dist/components/section.js +2 -2
  43. package/dist/components/select.css +175 -181
  44. package/dist/components/select.js +3 -3
  45. package/dist/components/slider.css +125 -116
  46. package/dist/components/slider.js +2 -2
  47. package/dist/components/specs.js +34 -1
  48. package/dist/components/stat.js +2 -2
  49. package/dist/components/submit.css +8 -8
  50. package/dist/components/submit.js +1 -1
  51. package/dist/components/switch.css +105 -105
  52. package/dist/components/switch.js +4 -3
  53. package/dist/components/table.js +4 -4
  54. package/dist/components/tabs.css +95 -95
  55. package/dist/components/tabs.js +4 -4
  56. package/dist/components/text-input.css +26 -95
  57. package/dist/components/text.js +13 -1
  58. package/dist/components/toast.js +4 -4
  59. package/dist/components/tooling.css +0 -1
  60. package/dist/components/tooling.js +73 -8
  61. package/dist/runtime.cjs +1610 -17
  62. package/dist/runtime.js +1609 -16
  63. package/dist/slexkit.cjs +28191 -13865
  64. package/dist/slexkit.css +1525 -1569
  65. package/dist/slexkit.js +28190 -13864
  66. package/dist/tooling.js +117 -11
  67. package/dist/types/components/svelte/helpers.d.ts +8 -1
  68. package/dist/types/engine/capabilities.d.ts +54 -0
  69. package/dist/types/engine/index.d.ts +6 -0
  70. package/dist/types/engine/secure-runtime.d.ts +9 -1
  71. package/dist/types/engine/stdlib.d.ts +30 -0
  72. package/dist/types/engine/types.d.ts +1 -0
  73. package/dist/types/engine/validation.d.ts +28 -0
  74. package/dist/types/index.d.ts +6 -3
  75. package/dist/types/runtime.d.ts +6 -3
  76. package/dist/types/version.d.ts +2 -2
  77. package/dist/umd/slexkit.tooling.umd.js +45084 -44775
  78. package/dist/umd/slexkit.umd.js +28191 -13865
  79. package/package.json +5 -3
  80. package/skills/slexkit-host-integration/SKILL.md +1 -1
  81. package/src/components/svelte/content/Formula.svelte +27 -0
  82. package/src/components/svelte/content/Table.svelte +1 -1
  83. package/src/components/svelte/display/Text.svelte +14 -1
  84. package/src/components/svelte/helpers.ts +56 -1
  85. package/src/components/svelte/input/Checkbox.svelte +1 -1
  86. package/src/components/svelte/input/Input.svelte +0 -110
  87. package/src/components/svelte/input/RadioGroup.svelte +1 -1
  88. package/src/components/svelte/input/Select.svelte +2 -2
  89. package/src/components/svelte/input/Switch.svelte +2 -2
  90. package/src/components/svelte/input/Tabs.svelte +7 -7
  91. package/src/components/svelte/tooling/PlaygroundMarkdown.svelte +84 -2
  92. package/src/styles/components/button.css +101 -101
  93. package/src/styles/components/choice.css +152 -152
  94. package/src/styles/components/select.css +175 -181
  95. package/src/styles/components/slider.css +125 -116
  96. package/src/styles/components/submit.css +8 -8
  97. package/src/styles/components/switch.css +105 -105
  98. package/src/styles/components/tabs.css +95 -95
  99. package/src/styles/components/text-input.css +26 -95
  100. package/src/styles/content.css +274 -251
  101. package/src/styles/display.css +1 -1
  102. package/src/styles/input.css +8 -8
  103. package/src/styles/layout.css +360 -360
  104. package/src/styles/tooling.css +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slexkit",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Zero-build, Markdown-friendly reactive UI runtime for AI output.",
5
5
  "author": "SlexKit contributors",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  "module": "./dist/slexkit.js",
17
17
  "types": "./dist/types/index.d.ts",
18
18
  "bin": {
19
- "slex": "./scripts/cli.mjs"
19
+ "slex": "scripts/cli.mjs"
20
20
  },
21
21
  "workspaces": [
22
22
  "packages/*"
@@ -81,7 +81,7 @@
81
81
  ],
82
82
  "scripts": {
83
83
  "dev": "bun run site/server.ts",
84
- "build": "bun run build:core && bun run --filter @slexkit/streamdown build && bun run --filter @slexkit/obsidian build && bun run --filter @slexkit/mcp build",
84
+ "build": "bun run build:core && bun run --filter @slexkit/streamdown build && bun run --filter @slexkit/mcp build",
85
85
  "build:core": "bun run scripts/build-core.ts",
86
86
  "ai:docs": "bun run scripts/generate-ai-docs.ts",
87
87
  "version:sync": "bun run scripts/sync-version.ts",
@@ -92,6 +92,7 @@
92
92
  "copy-runtime": "node scripts/cli.mjs copy-runtime",
93
93
  "preview": "bun run site/server.ts",
94
94
  "smoke:release": "node scripts/release-smoke.mjs",
95
+ "smoke:registry": "node scripts/registry-smoke.mjs",
95
96
  "test": "bun test --conditions browser --isolate --preload ./tests/setup.ts tests packages",
96
97
  "test:watch": "bun test --conditions browser --isolate --watch --preload ./tests/setup.ts tests packages",
97
98
  "lint": "eslint src/",
@@ -119,6 +120,7 @@
119
120
  "flowbite": "^4.0.2",
120
121
  "flowbite-svelte": "^1.33.1",
121
122
  "katex": "^0.17.0",
123
+ "marked": "^18.0.5",
122
124
  "svelte": "^5.55.9",
123
125
  "svelte-highlight": "^7.9.0"
124
126
  },
@@ -21,7 +21,7 @@ Use this skill as `/host` when adding SlexKit to a host application or documenta
21
21
 
22
22
  - Vanilla host: `createSlexKitMarkdownRuntimeHost`.
23
23
  - React/Streamdown: `@slexkit/streamdown`.
24
- - Obsidian: `@slexkit/obsidian`, readonly fenced block rendering.
24
+ - Obsidian: install the official SlexKit plugin from Obsidian Community Plugins; release repo `slexkit/obsidian-slexkit`.
25
25
  - Custom host: detect the `slex` code fence, pass source plus artifact id to the runtime host, dispose when the block is removed.
26
26
 
27
27
  ## Rules
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import katex from "katex";
3
+ import { bindPropStore } from "../bindProps";
4
+ import { bool, text } from "../helpers";
5
+ import type { PropValues, SvelteComponentProps } from "../types";
6
+
7
+ let { props }: SvelteComponentProps = $props();
8
+ let p = $state<PropValues>({});
9
+ $effect(() => bindPropStore(props, (next) => (p = next)));
10
+
11
+ const tex = $derived(text(p.tex ?? p.formula ?? p.value));
12
+ const displayMode = $derived(p.displayMode === undefined && p.display === undefined && p.block === undefined
13
+ ? true
14
+ : bool(p.displayMode ?? p.display ?? p.block));
15
+ const rendered = $derived(renderFormula(tex, displayMode));
16
+
17
+ function renderFormula(source: string, display: boolean): string {
18
+ return katex.renderToString(source || "\\,", {
19
+ displayMode: display,
20
+ throwOnError: false,
21
+ strict: "ignore",
22
+ output: "htmlAndMathml",
23
+ });
24
+ }
25
+ </script>
26
+
27
+ <div class="slex-formula" data-display={displayMode ? "block" : "inline"}>{@html rendered}</div>
@@ -31,7 +31,7 @@
31
31
  {#each rows(p.rows ?? p.items) as row}
32
32
  <tr>
33
33
  {#if readColumns(p.columns).length}
34
- {#each readColumns(p.columns) as column}<td>{readCell(row, column)}</td>{/each}
34
+ {#each readColumns(p.columns) as column, index}<td>{readCell(row, column, index)}</td>{/each}
35
35
  {:else if Array.isArray(row)}
36
36
  {#each row as cell}<td>{text(cell)}</td>{/each}
37
37
  {:else}
@@ -6,6 +6,19 @@
6
6
  let { props }: SvelteComponentProps = $props();
7
7
  let p = $state<PropValues>({});
8
8
  $effect(() => bindPropStore(props, (next) => (p = next)));
9
+
10
+ const color = $derived(p.color == null || p.color === "" ? undefined : text(p.color));
11
+ const size = $derived(cssLength(p.size));
12
+
13
+ function cssLength(value: unknown): string | undefined {
14
+ if (value == null || value === "") return undefined;
15
+ const rendered = text(value);
16
+ return /^-?\d+(\.\d+)?$/.test(rendered) ? `${rendered}px` : rendered;
17
+ }
9
18
  </script>
10
19
 
11
- <div class={`slex-text${p.variant ? ` slex-text--${text(p.variant)}` : ""}${p.class ? ` ${text(p.class)}` : ""}`}>{text(p.content ?? p.text ?? p.label)}</div>
20
+ <div
21
+ class={`slex-text${p.variant ? ` slex-text--${text(p.variant)}` : ""}${p.class ? ` ${text(p.class)}` : ""}`}
22
+ style:color={color}
23
+ style:font-size={size}
24
+ >{text(p.content ?? p.text ?? p.label)}</div>
@@ -76,7 +76,8 @@ export function readColumnLabel(column: unknown, fallback: string): string {
76
76
  return text(column, fallback);
77
77
  }
78
78
 
79
- export function readCell(row: unknown, column: string): string {
79
+ export function readCell(row: unknown, column: string, index?: number): string {
80
+ if (Array.isArray(row)) return text(typeof index === "number" ? row[index] : undefined);
80
81
  if (row && typeof row === "object") return text((row as Record<string, unknown>)[column]);
81
82
  return text(row);
82
83
  }
@@ -105,6 +106,7 @@ export function catalogGroups(value: unknown): Array<{ label: string; items: Rec
105
106
 
106
107
  export function renderChildren(node: HTMLElement, ctx: RenderContext) {
107
108
  if (ctx.children && Object.keys(ctx.children).length > 0) {
109
+ node.replaceChildren();
108
110
  ctx.renderTree(ctx.children, node, ctx.forCtx);
109
111
  }
110
112
  return {
@@ -118,6 +120,59 @@ export function emit(ctx: RenderContext, event: string, data?: unknown): void {
118
120
  ctx.emit(event, data);
119
121
  }
120
122
 
123
+ export type ScheduledFrame = {
124
+ kind: "api" | "native" | "microtask";
125
+ id?: unknown;
126
+ canceled: boolean;
127
+ };
128
+
129
+ export function scheduleFrame(ctx: RenderContext, fn: (time?: number) => void): ScheduledFrame {
130
+ const handle: ScheduledFrame = { kind: "microtask", canceled: false };
131
+ const run = (time?: number) => {
132
+ if (!handle.canceled) fn(time);
133
+ };
134
+ const apiRaf = ctx.api?.raf;
135
+ if (typeof apiRaf === "function") {
136
+ try {
137
+ handle.kind = "api";
138
+ handle.id = apiRaf(run);
139
+ return handle;
140
+ } catch {
141
+ handle.kind = "microtask";
142
+ handle.id = undefined;
143
+ }
144
+ }
145
+
146
+ const ownerWindow = ctx.document.defaultView;
147
+ if (ownerWindow && typeof ownerWindow.requestAnimationFrame === "function") {
148
+ try {
149
+ handle.kind = "native";
150
+ handle.id = ownerWindow.requestAnimationFrame(run);
151
+ return handle;
152
+ } catch {
153
+ handle.kind = "microtask";
154
+ handle.id = undefined;
155
+ }
156
+ }
157
+
158
+ queueMicrotask(() => run());
159
+ return handle;
160
+ }
161
+
162
+ export function cancelScheduledFrame(ctx: RenderContext, handle: ScheduledFrame | undefined): void {
163
+ if (!handle || handle.canceled) return;
164
+ handle.canceled = true;
165
+ if (handle.kind === "api" && typeof ctx.api?.cancelRaf === "function") {
166
+ try {
167
+ ctx.api.cancelRaf(handle.id);
168
+ } catch {
169
+ // The runtime may already be disposed; cancellation is best effort.
170
+ }
171
+ } else if (handle.kind === "native" && typeof handle.id === "number") {
172
+ ctx.document.defaultView?.cancelAnimationFrame(handle.id);
173
+ }
174
+ }
175
+
121
176
  function formatScriptKey(key: string): string {
122
177
  return /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
123
178
  }
@@ -34,7 +34,7 @@
34
34
  </script>
35
35
 
36
36
  <span class="slex-choice-event-layer" onpointerdown={() => haptic(8)} onclick={() => haptic(8)}>
37
- <label class="slex-checkbox-field">
37
+ <label class="slex-checkbox-field" data-disabled={p.disabled ? "true" : undefined}>
38
38
  <input
39
39
  type="checkbox"
40
40
  class="slex-checkbox"
@@ -8,21 +8,6 @@
8
8
  import { parseEngineeringNumber } from "../../../engine/engineering";
9
9
  import type { PropValues, SvelteComponentProps } from "../types";
10
10
 
11
- const engineeringPrefixFactors: Record<string, number> = {
12
- p: 1e-12,
13
- n: 1e-9,
14
- u: 1e-6,
15
- "\u00b5": 1e-6,
16
- "\u788c": 1e-6,
17
- m: 1e-3,
18
- k: 1e3,
19
- K: 1e3,
20
- M: 1e6,
21
- meg: 1e6,
22
- G: 1e9,
23
- T: 1e12,
24
- };
25
-
26
11
  let { componentName, props, ctx }: SvelteComponentProps = $props();
27
12
  let p = $state<PropValues>({});
28
13
  let value = $state("");
@@ -35,8 +20,6 @@
35
20
  const readonly = $derived(bool(p.readonly) || bool(p.readOnly));
36
21
  const required = $derived(bool(p.required));
37
22
  const invalid = $derived(bool(p.invalid) || !!errorText);
38
- const steppable = $derived(isSteppableInput());
39
- const controls = $derived(steppable && p.controls !== false && p.controls !== "false");
40
23
  const componentId = $derived(safeId(componentName));
41
24
  const inputId = $derived(text(p.id) || (componentId ? `slex-input-${componentId}` : fallbackId));
42
25
  const descriptionId = $derived(`${inputId}-description`);
@@ -44,10 +27,6 @@
44
27
  const explicitAriaLabel = $derived(text(p["aria-label"] ?? p.ariaLabel));
45
28
  const computedAriaLabel = $derived(explicitAriaLabel || (labelText ? "" : text(p.placeholder)));
46
29
  const describedBy = $derived([descriptionText ? descriptionId : "", errorText ? errorId : ""].filter(Boolean).join(" "));
47
- const numericValue = $derived(readNumericValue());
48
- const controlLabel = $derived(labelText || text(p.placeholder) || componentName || "input");
49
- const decrementDisabled = $derived(!canStep(-1));
50
- const incrementDisabled = $derived(!canStep(1));
51
30
  $effect(() => bindPropStore(props, (next) => {
52
31
  p = next;
53
32
  value = text(next.value);
@@ -61,76 +40,6 @@
61
40
  return text(p.type, "text") === "engineering" ? "text" : text(p.type, "text");
62
41
  }
63
42
 
64
- function isSteppableInput(): boolean {
65
- const kind = text(p.type, "text");
66
- return kind === "number" ||
67
- kind === "engineering" ||
68
- p.min !== undefined ||
69
- p.max !== undefined ||
70
- p.step !== undefined;
71
- }
72
-
73
- function numericProp(input: unknown): number | undefined {
74
- if (input === undefined || input === null || input === "") return undefined;
75
- const next = Number(input);
76
- return Number.isFinite(next) ? next : undefined;
77
- }
78
-
79
- function stepSize(): number {
80
- const next = numericProp(p.step);
81
- if (next !== undefined && next > 0) return next;
82
- if (text(p.type, "text") !== "engineering") return 1;
83
- const parsed = parseEngineeringNumber(value);
84
- if (!parsed.valid || !parsed.prefix) return 1;
85
- return engineeringPrefixFactors[parsed.prefix] ?? 1;
86
- }
87
-
88
- function readNumericValue(): number | null {
89
- if (text(p.type, "text") === "engineering") {
90
- const parsed = parseEngineeringNumber(value);
91
- return parsed.valid && parsed.number !== null ? parsed.number : null;
92
- }
93
- const next = Number(value);
94
- return Number.isFinite(next) ? next : null;
95
- }
96
-
97
- function clamp(next: number): number {
98
- const min = numericProp(p.min);
99
- const max = numericProp(p.max);
100
- let clamped = next;
101
- if (min !== undefined) clamped = Math.max(min, clamped);
102
- if (max !== undefined) clamped = Math.min(max, clamped);
103
- return clamped;
104
- }
105
-
106
- function canStep(direction: -1 | 1): boolean {
107
- if (!controls || disabled || readonly || numericValue === null) return false;
108
- return clamp(numericValue + direction * stepSize()) !== numericValue;
109
- }
110
-
111
- function formatSteppedValue(next: number): string {
112
- if (text(p.type, "text") !== "engineering") return text(next);
113
- const parsed = parseEngineeringNumber(value);
114
- if (!parsed.valid) return text(next);
115
- const factor = parsed.prefix ? engineeringPrefixFactors[parsed.prefix] : undefined;
116
- const visibleNumber = factor ? next / factor : next;
117
- return `${formatNumber(visibleNumber)}${parsed.prefix}${parsed.unit}`;
118
- }
119
-
120
- function formatNumber(next: number): string {
121
- return text(Number(next.toPrecision(12)));
122
- }
123
-
124
- function emitValue(nextValue: string): void {
125
- value = nextValue;
126
- emit(ctx, "change", text(p.type, "text") === "engineering" ? parseEngineeringNumber(value) : value);
127
- }
128
-
129
- function stepBy(direction: -1 | 1): void {
130
- if (!canStep(direction) || numericValue === null) return;
131
- emitValue(formatSteppedValue(clamp(numericValue + direction * stepSize())));
132
- }
133
-
134
43
  function update(event: Event): void {
135
44
  if (disabled || readonly) return;
136
45
  value = (event.target as HTMLInputElement).value;
@@ -150,7 +59,6 @@
150
59
  <div
151
60
  class="slex-input-control"
152
61
  data-has-unit={unitText ? "true" : undefined}
153
- data-has-controls={controls ? "true" : undefined}
154
62
  >
155
63
  <input
156
64
  id={inputId}
@@ -174,24 +82,6 @@
174
82
  {#if unitText}
175
83
  <span class="slex-input-unit" aria-hidden="true">{unitText}</span>
176
84
  {/if}
177
- {#if controls}
178
- <span class="slex-input-controls">
179
- <button
180
- class="slex-input-step"
181
- type="button"
182
- aria-label={`Decrease ${controlLabel}`}
183
- disabled={decrementDisabled}
184
- onclick={() => stepBy(-1)}
185
- >-</button>
186
- <button
187
- class="slex-input-step"
188
- type="button"
189
- aria-label={`Increase ${controlLabel}`}
190
- disabled={incrementDisabled}
191
- onclick={() => stepBy(1)}
192
- >+</button>
193
- </span>
194
- {/if}
195
85
  </div>
196
86
  {#if descriptionText}
197
87
  <div id={descriptionId} class="slex-input-description">{descriptionText}</div>
@@ -49,7 +49,7 @@
49
49
  {@const disabled = !!item.disabled || !!p.disabled}
50
50
  {@const hapticKey = text(itemValue)}
51
51
  <span class="slex-choice-event-layer" onpointerdown={() => haptic(hapticKey, disabled, 8)} onclick={() => haptic(hapticKey, disabled, 8)}>
52
- <label class="slex-radio-field">
52
+ <label class="slex-radio-field" data-disabled={disabled ? "true" : undefined}>
53
53
  <input
54
54
  type="radio"
55
55
  class="slex-radio"
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from "svelte";
3
3
  import { bindPropStore } from "../bindProps";
4
- import { emit, label, list, text } from "../helpers";
4
+ import { emit, label, list, scheduleFrame, text } from "../helpers";
5
5
  import InlineIcon from "../InlineIcon.svelte";
6
6
  import type { PropValues, SvelteComponentProps } from "../types";
7
7
 
@@ -97,7 +97,7 @@
97
97
  function close(focusTrigger = true): void {
98
98
  open = false;
99
99
  activeIndex = selectedIndex();
100
- if (focusTrigger) requestAnimationFrame(() => triggerEl?.focus());
100
+ if (focusTrigger) scheduleFrame(ctx, () => triggerEl?.focus());
101
101
  }
102
102
 
103
103
  function toggle(): void {
@@ -10,7 +10,7 @@
10
10
  let lastHapticAt = 0;
11
11
  $effect(() => bindPropStore(props, (next) => {
12
12
  p = next;
13
- enabled = !!next.enabled;
13
+ enabled = !!(next.enabled ?? next.checked ?? next.value);
14
14
  }));
15
15
 
16
16
  function toggle(event: Event): void {
@@ -34,7 +34,7 @@
34
34
  </script>
35
35
 
36
36
  <span class="slex-switch-event-layer" onpointerdown={() => haptic(8)} onclick={() => haptic(8)}>
37
- <label class="slex-switch" data-state={enabled ? "on" : "off"}>
37
+ <label class="slex-switch" data-state={enabled ? "on" : "off"} data-disabled={p.disabled ? "true" : undefined}>
38
38
  <input
39
39
  type="checkbox"
40
40
  role="switch"
@@ -2,7 +2,7 @@
2
2
  import FlowbiteTabs from "../../../../node_modules/flowbite-svelte/dist/tabs/Tabs.svelte";
3
3
  import FlowbiteTabItem from "../../../../node_modules/flowbite-svelte/dist/tabs/TabItem.svelte";
4
4
  import { bindPropStore } from "../bindProps";
5
- import { emit, list, text } from "../helpers";
5
+ import { cancelScheduledFrame, emit, list, scheduleFrame, text, type ScheduledFrame } from "../helpers";
6
6
  import InlineIcon from "../InlineIcon.svelte";
7
7
  import type { PropValues, SvelteComponentProps } from "../types";
8
8
 
@@ -55,14 +55,14 @@
55
55
  }
56
56
 
57
57
  function annotateTabs(node: HTMLElement) {
58
- let raf: number | undefined;
58
+ let frame: ScheduledFrame | undefined;
59
59
  let resizeObserver: ResizeObserver | undefined;
60
60
  let indicatorReady = false;
61
61
 
62
62
  function scheduleIndicatorUpdate(list: HTMLElement, selectedTrigger: HTMLElement | undefined) {
63
- if (raf !== undefined) cancelAnimationFrame(raf);
64
- raf = requestAnimationFrame(() => {
65
- raf = undefined;
63
+ cancelScheduledFrame(ctx, frame);
64
+ frame = scheduleFrame(ctx, () => {
65
+ frame = undefined;
66
66
  if (!selectedTrigger) {
67
67
  list.style.setProperty("--slex-tabs-indicator-opacity", "0");
68
68
  return;
@@ -96,7 +96,7 @@
96
96
  list.style.setProperty("--slex-tabs-indicator-opacity", "1");
97
97
  if (!indicatorReady) {
98
98
  indicatorReady = true;
99
- requestAnimationFrame(() => {
99
+ scheduleFrame(ctx, () => {
100
100
  list.dataset.indicatorReady = "true";
101
101
  });
102
102
  }
@@ -137,7 +137,7 @@
137
137
  apply();
138
138
  },
139
139
  destroy() {
140
- if (raf !== undefined) cancelAnimationFrame(raf);
140
+ cancelScheduledFrame(ctx, frame);
141
141
  resizeObserver?.disconnect();
142
142
  },
143
143
  };
@@ -15,10 +15,92 @@
15
15
  inlineKatex: KatexRenderer,
16
16
  blockKatex: KatexRenderer,
17
17
  };
18
+ const options = { headerIds: false };
19
+
20
+ function stripFrontmatter(value: string) {
21
+ const raw = String(value ?? "").replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
22
+ if (!raw.startsWith("---")) return raw;
23
+
24
+ const end = raw.indexOf("\n---", 3);
25
+ if (end === -1) return raw;
26
+
27
+ const closeEnd = raw.indexOf("\n", end + 1);
28
+ return raw.slice(closeEnd === -1 ? raw.length : closeEnd + 1).trimStart();
29
+ }
30
+
31
+ function escapeHtml(value: string) {
32
+ return String(value)
33
+ .replace(/&/g, "&amp;")
34
+ .replace(/</g, "&lt;")
35
+ .replace(/>/g, "&gt;")
36
+ .replace(/"/g, "&quot;");
37
+ }
38
+
39
+ function stripInlineMarkdown(value: string) {
40
+ return String(value)
41
+ .replace(/\s+\{#[A-Za-z0-9_-]+\}\s*$/, "")
42
+ .replace(/`([^`]+)`/g, "$1")
43
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
44
+ .replace(/\*([^*]+)\*/g, "$1")
45
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
46
+ .replace(/<[^>]+>/g, "")
47
+ .replace(/\s+#+\s*$/, "")
48
+ .trim();
49
+ }
50
+
51
+ function slugText(value: string) {
52
+ const slug = String(value)
53
+ .toLowerCase()
54
+ .replace(/[`"'\u2018\u2019\u201c\u201d]/g, "")
55
+ .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
56
+ .replace(/^-+|-+$/g, "");
57
+ return slug || "section";
58
+ }
59
+
60
+ function createHeadingIdGenerator() {
61
+ const counts = new Map<string, number>();
62
+ return (rawTitle: string) => {
63
+ const explicit = rawTitle.match(/\s+\{#([A-Za-z0-9_-]+)\}\s*$/)?.[1] ?? "";
64
+ const base = explicit || slugText(stripInlineMarkdown(rawTitle));
65
+ const count = counts.get(base) ?? 0;
66
+ counts.set(base, count + 1);
67
+ return count ? `${base}-${count + 1}` : base;
68
+ };
69
+ }
70
+
71
+ function normalizeHeadingAnchors(markdown: string) {
72
+ const nextId = createHeadingIdGenerator();
73
+ let fence: { char: string; length: number } | null = null;
74
+
75
+ return String(markdown ?? "")
76
+ .split(/\n/)
77
+ .map((line) => {
78
+ const marker = String(line).match(/^[ \t]{0,3}(`{3,}|~{3,})/);
79
+ if (fence) {
80
+ if (marker && marker[1][0] === fence.char && marker[1].length >= fence.length) fence = null;
81
+ return line;
82
+ }
83
+ if (marker) {
84
+ fence = { char: marker[1][0], length: marker[1].length };
85
+ return line;
86
+ }
87
+
88
+ const heading = String(line).match(/^(#{1,6})[ \t]+(.+)$/);
89
+ if (!heading) return line;
90
+ const rawTitle = heading[2].replace(/\s+#+\s*$/, "");
91
+ const renderedTitle = rawTitle.replace(/\s+\{#[A-Za-z0-9_-]+\}\s*$/, "").trim();
92
+ return `<span id="${escapeHtml(nextId(rawTitle))}" class="slex-doc-heading-anchor"></span>\n${heading[1]} ${renderedTitle}`;
93
+ })
94
+ .join("\n");
95
+ }
96
+
97
+ function previewMarkdown(value: string) {
98
+ return normalizeHeadingAnchors(stripFrontmatter(value));
99
+ }
18
100
  </script>
19
101
 
20
- <div class="slex-doc-streamdown">
21
- <SvelteMarkdown source={content} {extensions} {renderers}>
102
+ <div class="slex-doc-prose slex-doc-streamdown">
103
+ <SvelteMarkdown source={previewMarkdown(content)} {extensions} {renderers} {options}>
22
104
  {#snippet code({ lang, text })}
23
105
  <PlaygroundSlexCode {lang} {text} {domain} />
24
106
  {/snippet}