slexkit 0.2.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 (221) hide show
  1. package/AGENTS.slexkit.md +29 -0
  2. package/CHANGELOG.md +90 -0
  3. package/LICENSE +21 -0
  4. package/README.md +165 -0
  5. package/README.zh-CN.md +165 -0
  6. package/dist/ai/llms-authoring.txt +44 -0
  7. package/dist/ai/llms-components.txt +669 -0
  8. package/dist/ai/llms-full.txt +6586 -0
  9. package/dist/ai/llms-runtime.txt +1475 -0
  10. package/dist/ai/llms-toolhost.txt +295 -0
  11. package/dist/ai/llms.txt +69 -0
  12. package/dist/ai/slexkit-ai-manifest.json +2922 -0
  13. package/dist/base.css +621 -0
  14. package/dist/chunks/accordion-5f0nvjjm.js +376 -0
  15. package/dist/chunks/accordion-830dw78f.js +221 -0
  16. package/dist/chunks/accordion-cfjyxw93.js +630 -0
  17. package/dist/chunks/accordion-cw5r75jm.js +424 -0
  18. package/dist/chunks/accordion-ehnhpeca.js +492 -0
  19. package/dist/chunks/accordion-hzyrngd6.js +2377 -0
  20. package/dist/chunks/accordion-nw12ytps.js +6823 -0
  21. package/dist/components/accordion.js +163 -0
  22. package/dist/components/badge.js +80 -0
  23. package/dist/components/button.css +114 -0
  24. package/dist/components/button.js +16 -0
  25. package/dist/components/callout.js +154 -0
  26. package/dist/components/card.js +95 -0
  27. package/dist/components/checkbox.js +114 -0
  28. package/dist/components/choice.css +165 -0
  29. package/dist/components/code-block.js +264 -0
  30. package/dist/components/collapsible.js +111 -0
  31. package/dist/components/column.js +49 -0
  32. package/dist/components/content.css +474 -0
  33. package/dist/components/disclosure.css +162 -0
  34. package/dist/components/display.css +259 -0
  35. package/dist/components/divider.js +98 -0
  36. package/dist/components/feedback.css +219 -0
  37. package/dist/components/grid.js +67 -0
  38. package/dist/components/index.js +13364 -0
  39. package/dist/components/input.css +1247 -0
  40. package/dist/components/input.js +384 -0
  41. package/dist/components/link.js +77 -0
  42. package/dist/components/progress.js +111 -0
  43. package/dist/components/radio-group.js +189 -0
  44. package/dist/components/row.js +200 -0
  45. package/dist/components/section.js +161 -0
  46. package/dist/components/select.css +260 -0
  47. package/dist/components/select.js +16 -0
  48. package/dist/components/slider.css +125 -0
  49. package/dist/components/slider.js +175 -0
  50. package/dist/components/specs.js +1090 -0
  51. package/dist/components/stat.js +178 -0
  52. package/dist/components/submit.css +9 -0
  53. package/dist/components/submit.js +77 -0
  54. package/dist/components/switch.css +114 -0
  55. package/dist/components/switch.js +114 -0
  56. package/dist/components/table.js +157 -0
  57. package/dist/components/tabs.css +192 -0
  58. package/dist/components/tabs.js +17 -0
  59. package/dist/components/text-input.css +245 -0
  60. package/dist/components/text.js +50 -0
  61. package/dist/components/toast.js +240 -0
  62. package/dist/components/tooling.css +1009 -0
  63. package/dist/components/tooling.js +48951 -0
  64. package/dist/runtime.cjs +3728 -0
  65. package/dist/runtime.js +3686 -0
  66. package/dist/slexkit.cjs +18539 -0
  67. package/dist/slexkit.css +4776 -0
  68. package/dist/slexkit.js +18497 -0
  69. package/dist/tooling.js +59141 -0
  70. package/dist/types/components/accordion.d.ts +2 -0
  71. package/dist/types/components/badge.d.ts +2 -0
  72. package/dist/types/components/button.d.ts +2 -0
  73. package/dist/types/components/callout.d.ts +2 -0
  74. package/dist/types/components/card.d.ts +2 -0
  75. package/dist/types/components/checkbox.d.ts +2 -0
  76. package/dist/types/components/code-block.d.ts +2 -0
  77. package/dist/types/components/collapsible.d.ts +2 -0
  78. package/dist/types/components/column.d.ts +2 -0
  79. package/dist/types/components/divider.d.ts +2 -0
  80. package/dist/types/components/entries/accordion.d.ts +3 -0
  81. package/dist/types/components/entries/badge.d.ts +3 -0
  82. package/dist/types/components/entries/button.d.ts +3 -0
  83. package/dist/types/components/entries/callout.d.ts +3 -0
  84. package/dist/types/components/entries/card.d.ts +3 -0
  85. package/dist/types/components/entries/checkbox.d.ts +3 -0
  86. package/dist/types/components/entries/code-block.d.ts +3 -0
  87. package/dist/types/components/entries/collapsible.d.ts +3 -0
  88. package/dist/types/components/entries/column.d.ts +3 -0
  89. package/dist/types/components/entries/divider.d.ts +3 -0
  90. package/dist/types/components/entries/grid.d.ts +3 -0
  91. package/dist/types/components/entries/input.d.ts +3 -0
  92. package/dist/types/components/entries/link.d.ts +3 -0
  93. package/dist/types/components/entries/progress.d.ts +3 -0
  94. package/dist/types/components/entries/radio-group.d.ts +3 -0
  95. package/dist/types/components/entries/row.d.ts +3 -0
  96. package/dist/types/components/entries/section.d.ts +3 -0
  97. package/dist/types/components/entries/select.d.ts +3 -0
  98. package/dist/types/components/entries/slider.d.ts +3 -0
  99. package/dist/types/components/entries/specs.d.ts +1 -0
  100. package/dist/types/components/entries/stat.d.ts +3 -0
  101. package/dist/types/components/entries/submit.d.ts +3 -0
  102. package/dist/types/components/entries/switch.d.ts +3 -0
  103. package/dist/types/components/entries/table.d.ts +3 -0
  104. package/dist/types/components/entries/tabs.d.ts +3 -0
  105. package/dist/types/components/entries/text.d.ts +3 -0
  106. package/dist/types/components/entries/toast.d.ts +3 -0
  107. package/dist/types/components/entries/tooling.d.ts +1 -0
  108. package/dist/types/components/grid.d.ts +2 -0
  109. package/dist/types/components/index.d.ts +6 -0
  110. package/dist/types/components/input.d.ts +2 -0
  111. package/dist/types/components/link.d.ts +2 -0
  112. package/dist/types/components/progress.d.ts +2 -0
  113. package/dist/types/components/radio-group.d.ts +2 -0
  114. package/dist/types/components/row.d.ts +2 -0
  115. package/dist/types/components/section.d.ts +2 -0
  116. package/dist/types/components/select.d.ts +2 -0
  117. package/dist/types/components/slider.d.ts +2 -0
  118. package/dist/types/components/spec-helpers.d.ts +23 -0
  119. package/dist/types/components/spec-registry.d.ts +12 -0
  120. package/dist/types/components/spec-schema.d.ts +74 -0
  121. package/dist/types/components/specs.d.ts +2 -0
  122. package/dist/types/components/stat.d.ts +2 -0
  123. package/dist/types/components/submit.d.ts +2 -0
  124. package/dist/types/components/svelte/adapter.d.ts +3 -0
  125. package/dist/types/components/svelte/bindProps.d.ts +2 -0
  126. package/dist/types/components/svelte/helpers.d.ts +33 -0
  127. package/dist/types/components/svelte/layout/balancedTiles.d.ts +14 -0
  128. package/dist/types/components/svelte/types.d.ts +12 -0
  129. package/dist/types/components/switch.d.ts +2 -0
  130. package/dist/types/components/table.d.ts +2 -0
  131. package/dist/types/components/tabs.d.ts +2 -0
  132. package/dist/types/components/text.d.ts +2 -0
  133. package/dist/types/components/toast.d.ts +2 -0
  134. package/dist/types/components/tooling.d.ts +2 -0
  135. package/dist/types/components-svelte.d.ts +5 -0
  136. package/dist/types/engine/component-scope.d.ts +14 -0
  137. package/dist/types/engine/component-state.d.ts +9 -0
  138. package/dist/types/engine/diagnostics.d.ts +24 -0
  139. package/dist/types/engine/engineering.d.ts +11 -0
  140. package/dist/types/engine/eval.d.ts +5 -0
  141. package/dist/types/engine/index.d.ts +26 -0
  142. package/dist/types/engine/markdown-runtime.d.ts +33 -0
  143. package/dist/types/engine/merge.d.ts +1 -0
  144. package/dist/types/engine/reactive.d.ts +11 -0
  145. package/dist/types/engine/registry.d.ts +4 -0
  146. package/dist/types/engine/renderer.d.ts +6 -0
  147. package/dist/types/engine/sandbox-runner.d.ts +2 -0
  148. package/dist/types/engine/secure-runtime.d.ts +214 -0
  149. package/dist/types/engine/store.d.ts +12 -0
  150. package/dist/types/engine/types.d.ts +58 -0
  151. package/dist/types/icons/manager.d.ts +17 -0
  152. package/dist/types/icons/phosphor.d.ts +45 -0
  153. package/dist/types/index.d.ts +61 -0
  154. package/dist/types/runtime.d.ts +32 -0
  155. package/dist/types/toolhost/index.d.ts +78 -0
  156. package/dist/types/tooling-umd.d.ts +47 -0
  157. package/dist/types/version.d.ts +8 -0
  158. package/dist/umd/slexkit.tooling.umd.js +66553 -0
  159. package/dist/umd/slexkit.umd.js +18552 -0
  160. package/package.json +136 -0
  161. package/scripts/cli.mjs +47 -0
  162. package/skills/slexkit/SKILL.md +27 -0
  163. package/skills/slexkit-author/SKILL.md +50 -0
  164. package/skills/slexkit-host-integration/SKILL.md +33 -0
  165. package/skills/slexkit-secure-runtime/SKILL.md +31 -0
  166. package/skills/slexkit-toolhost/SKILL.md +38 -0
  167. package/skills/slexkit-update/SKILL.md +23 -0
  168. package/src/components/svelte/InlineIcon.svelte +66 -0
  169. package/src/components/svelte/adapter.ts +76 -0
  170. package/src/components/svelte/bindProps.ts +9 -0
  171. package/src/components/svelte/content/Badge.svelte +19 -0
  172. package/src/components/svelte/content/Callout.svelte +57 -0
  173. package/src/components/svelte/content/CodeBlock.svelte +130 -0
  174. package/src/components/svelte/content/Divider.svelte +21 -0
  175. package/src/components/svelte/content/Link.svelte +21 -0
  176. package/src/components/svelte/content/Section.svelte +24 -0
  177. package/src/components/svelte/content/Table.svelte +44 -0
  178. package/src/components/svelte/disclosure/Accordion.svelte +100 -0
  179. package/src/components/svelte/disclosure/Collapsible.svelte +45 -0
  180. package/src/components/svelte/display/Stat.svelte +102 -0
  181. package/src/components/svelte/display/Text.svelte +11 -0
  182. package/src/components/svelte/feedback/Progress.svelte +34 -0
  183. package/src/components/svelte/feedback/Toast.svelte +105 -0
  184. package/src/components/svelte/helpers.ts +148 -0
  185. package/src/components/svelte/input/Button.svelte +78 -0
  186. package/src/components/svelte/input/Checkbox.svelte +52 -0
  187. package/src/components/svelte/input/Input.svelte +202 -0
  188. package/src/components/svelte/input/RadioGroup.svelte +71 -0
  189. package/src/components/svelte/input/Select.svelte +220 -0
  190. package/src/components/svelte/input/Slider.svelte +96 -0
  191. package/src/components/svelte/input/Submit.svelte +32 -0
  192. package/src/components/svelte/input/Switch.svelte +53 -0
  193. package/src/components/svelte/input/Tabs.svelte +188 -0
  194. package/src/components/svelte/layout/Card.svelte +17 -0
  195. package/src/components/svelte/layout/Column.svelte +15 -0
  196. package/src/components/svelte/layout/Grid.svelte +26 -0
  197. package/src/components/svelte/layout/Row.svelte +105 -0
  198. package/src/components/svelte/layout/balancedTiles.ts +85 -0
  199. package/src/components/svelte/tooling/CodeMirror.svelte +91 -0
  200. package/src/components/svelte/tooling/Playground.svelte +765 -0
  201. package/src/components/svelte/tooling/PlaygroundMarkdown.svelte +26 -0
  202. package/src/components/svelte/tooling/PlaygroundSlexCode.svelte +76 -0
  203. package/src/components/svelte/types.ts +17 -0
  204. package/src/styles/animation.css +98 -0
  205. package/src/styles/components/button.css +114 -0
  206. package/src/styles/components/choice.css +165 -0
  207. package/src/styles/components/select.css +260 -0
  208. package/src/styles/components/slider.css +125 -0
  209. package/src/styles/components/submit.css +9 -0
  210. package/src/styles/components/switch.css +114 -0
  211. package/src/styles/components/tabs.css +192 -0
  212. package/src/styles/components/text-input.css +245 -0
  213. package/src/styles/content.css +474 -0
  214. package/src/styles/disclosure.css +162 -0
  215. package/src/styles/display.css +259 -0
  216. package/src/styles/entry.css +34 -0
  217. package/src/styles/feedback.css +219 -0
  218. package/src/styles/input.css +8 -0
  219. package/src/styles/layout.css +365 -0
  220. package/src/styles/theme.css +31 -0
  221. package/src/styles/tooling.css +1009 -0
@@ -0,0 +1,765 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import { mount as mountSvelte, unmount } from "svelte";
4
+ import { basicSetup } from "codemirror";
5
+ import { javascript, javascriptLanguage } from "@codemirror/lang-javascript";
6
+ import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
7
+ import type { EditorView } from "@codemirror/view";
8
+ import type { SlexExpression, RenderContext } from "../../../engine/types";
9
+ import { mount as mountSlexKit, parseSlexSource } from "../../../engine/index";
10
+ import { bindPropStore } from "../bindProps";
11
+ import { stringifySource, text } from "../helpers";
12
+ import type { PropValues, SvelteComponentProps } from "../types";
13
+ import Button from "../input/Button.svelte";
14
+ import Select from "../input/Select.svelte";
15
+ import Tabs from "../input/Tabs.svelte";
16
+ import CodeMirror from "./CodeMirror.svelte";
17
+ import PlaygroundMarkdown from "./PlaygroundMarkdown.svelte";
18
+
19
+ type PlaygroundMode = "code" | "live" | "render";
20
+ type SourceKind = "markdown" | "slex";
21
+ type Diagnostic =
22
+ | { ok: true }
23
+ | {
24
+ ok: false;
25
+ block: number;
26
+ message: string;
27
+ editorLine: number;
28
+ column: number;
29
+ detail?: string;
30
+ excerpt?: string;
31
+ };
32
+
33
+ let { props, ctx }: SvelteComponentProps = $props();
34
+ let p = $state<PropValues>({});
35
+ let mode = $state<PlaygroundMode>("render");
36
+ let source = $state("");
37
+ let previewSource = $state("");
38
+ let sourceType = $state<SourceKind>("markdown");
39
+ let lastSource = "";
40
+ let editorView = $state<EditorView | null>(null);
41
+ let playgroundNode = $state<HTMLElement | null>(null);
42
+ let previewNode = $state<HTMLElement | null>(null);
43
+ let previewOverflow = $state(false);
44
+ let previewAnchorFixed = $state(false);
45
+ let previewAnchorOffset = $state(0);
46
+ let compact = $state(false);
47
+ let splitPercent = $state(48);
48
+ let configuredMode = "";
49
+ let configuredSplit = "";
50
+ let configuredSourceType = "";
51
+ let dragging = $state(false);
52
+ let currentTheme = $state<"light" | "dark">("light");
53
+ let splitSurface: HTMLElement | null = null;
54
+ let activeDragPointerId: number | null = null;
55
+ let previewOverflowFrame = 0;
56
+ const themeStorageKey = "slexkit:theme";
57
+
58
+ const modeItems: Array<{ id: PlaygroundMode; label: string; icon: string }> = [
59
+ { id: "render", label: "Render", icon: "eye" },
60
+ { id: "live", label: "Live", icon: "square-split-horizontal" },
61
+ { id: "code", label: "Code", icon: "code" },
62
+ ];
63
+ const sourceTypeItems = [
64
+ { label: "Markdown", value: "markdown" },
65
+ { label: "SLEX", value: "slex" },
66
+ ];
67
+
68
+ const slexLanguage = javascript();
69
+ const javascriptEditorExtensions = [
70
+ basicSetup,
71
+ slexLanguage,
72
+ ];
73
+ const markdownEditorExtensions = [
74
+ basicSetup,
75
+ markdown({
76
+ base: markdownLanguage,
77
+ codeLanguages: (info) => {
78
+ const language = info.trim().toLowerCase().split(/\s+/, 1)[0];
79
+ return isSlexLanguage(language)
80
+ ? javascriptLanguage
81
+ : null;
82
+ },
83
+ defaultCodeLanguage: slexLanguage,
84
+ }),
85
+ ];
86
+ const editorExtensions = $derived(sourceType === "slex" ? javascriptEditorExtensions : markdownEditorExtensions);
87
+
88
+ $effect(() => bindPropStore(props, (next) => {
89
+ p = next;
90
+ const nextSource = stringifySource(next.source);
91
+ const sourceChanged = nextSource !== lastSource;
92
+ if (sourceChanged) {
93
+ source = nextSource;
94
+ previewSource = nextSource;
95
+ lastSource = nextSource;
96
+ resetPreviewAnchor();
97
+ }
98
+ const nextSourceType = text(next.sourceType ?? next.type, "");
99
+ if (nextSourceType !== configuredSourceType || (!nextSourceType && sourceChanged)) {
100
+ configuredSourceType = nextSourceType;
101
+ sourceType = normalizeSourceKind(nextSourceType, nextSource);
102
+ }
103
+ const nextMode = text(next.mode ?? next.webMode, "");
104
+ if (nextMode !== configuredMode) {
105
+ configuredMode = nextMode;
106
+ if (nextMode) mode = normalizeMode(nextMode);
107
+ }
108
+ const nextSplit = text(next.splitPercent ?? next.split, "");
109
+ if (nextSplit !== configuredSplit) {
110
+ configuredSplit = nextSplit;
111
+ const parsed = Number(nextSplit);
112
+ if (Number.isFinite(parsed)) {
113
+ splitPercent = Math.min(72, Math.max(18, parsed));
114
+ }
115
+ }
116
+ }));
117
+
118
+ $effect(() => {
119
+ const next = source;
120
+ const timer = window.setTimeout(() => {
121
+ previewSource = next;
122
+ }, 300);
123
+ return () => window.clearTimeout(timer);
124
+ });
125
+
126
+ $effect(() => {
127
+ if (mode !== "live" || !editorView || !previewNode) return;
128
+ const codeScroller = editorView.scrollDOM;
129
+ const previewScroller = previewNode;
130
+ let syncing = false;
131
+
132
+ const sync = (sourceNode: HTMLElement, targetNode: HTMLElement) => {
133
+ if (syncing) return;
134
+ const sourceMax = Math.max(0, sourceNode.scrollHeight - sourceNode.clientHeight);
135
+ const targetMax = Math.max(0, targetNode.scrollHeight - targetNode.clientHeight);
136
+ if (!sourceMax || !targetMax) return;
137
+ syncing = true;
138
+ targetNode.scrollTop = (sourceNode.scrollTop / sourceMax) * targetMax;
139
+ window.requestAnimationFrame(() => {
140
+ syncing = false;
141
+ });
142
+ };
143
+
144
+ const syncPreview = () => sync(codeScroller, previewScroller);
145
+ const syncCode = () => sync(previewScroller, codeScroller);
146
+ codeScroller.addEventListener("scroll", syncPreview, { passive: true });
147
+ previewScroller.addEventListener("scroll", syncCode, { passive: true });
148
+ return () => {
149
+ codeScroller.removeEventListener("scroll", syncPreview);
150
+ previewScroller.removeEventListener("scroll", syncCode);
151
+ };
152
+ });
153
+
154
+ $effect(() => {
155
+ const node = previewNode;
156
+ if (!node) {
157
+ previewOverflow = false;
158
+ return;
159
+ }
160
+
161
+ schedulePreviewOverflowMeasure();
162
+ if (typeof ResizeObserver === "undefined") return;
163
+
164
+ const observer = new ResizeObserver(schedulePreviewOverflowMeasure);
165
+ observer.observe(node);
166
+ const container = previewScrollContainer();
167
+ if (container && container !== node) observer.observe(container);
168
+ const mutationObserver = new MutationObserver(schedulePreviewOverflowMeasure);
169
+ mutationObserver.observe(node, { childList: true, subtree: true });
170
+
171
+ return () => {
172
+ observer.disconnect();
173
+ mutationObserver.disconnect();
174
+ if (previewOverflowFrame) {
175
+ window.cancelAnimationFrame(previewOverflowFrame);
176
+ previewOverflowFrame = 0;
177
+ }
178
+ };
179
+ });
180
+
181
+ onMount(() => {
182
+ currentTheme = document.documentElement.classList.contains("dark") ? "dark" : "light";
183
+ const media = window.matchMedia("(max-width: 640px)");
184
+ const updateCompact = () => {
185
+ const width = playgroundNode?.getBoundingClientRect().width ?? 0;
186
+ compact = media.matches || (width > 0 && width <= 640);
187
+ };
188
+ const cancelDrag = () => {
189
+ endDrag();
190
+ };
191
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(updateCompact);
192
+ updateCompact();
193
+ media.addEventListener("change", updateCompact);
194
+ window.addEventListener("resize", updateCompact);
195
+ if (playgroundNode) observer?.observe(playgroundNode);
196
+ window.addEventListener("pointerup", cancelDrag);
197
+ window.addEventListener("pointercancel", cancelDrag);
198
+ window.addEventListener("blur", cancelDrag);
199
+ document.addEventListener("visibilitychange", cancelDrag);
200
+
201
+ return () => {
202
+ media.removeEventListener("change", updateCompact);
203
+ window.removeEventListener("resize", updateCompact);
204
+ observer?.disconnect();
205
+ window.removeEventListener("pointerup", cancelDrag);
206
+ window.removeEventListener("pointercancel", cancelDrag);
207
+ window.removeEventListener("blur", cancelDrag);
208
+ document.removeEventListener("visibilitychange", cancelDrag);
209
+ };
210
+ });
211
+
212
+ function themeToggleEnabled(): boolean {
213
+ const value = p.themeToggle ?? p.showThemeToggle ?? p.enableThemeToggle;
214
+ return value === true || value === "true" || value === 1 || value === "1";
215
+ }
216
+
217
+ function normalizeMode(value: string): PlaygroundMode {
218
+ const raw = value.trim().toLowerCase();
219
+ if (raw === "code" || raw === "script" || raw === "editor") return "code";
220
+ if (raw === "render" || raw === "preview") return "render";
221
+ if (raw === "live" || raw === "split" || raw === "both") return "live";
222
+ return "render";
223
+ }
224
+
225
+ function normalizePreviewAlign(value: string): "center" | "start" {
226
+ const raw = value.trim().toLowerCase();
227
+ return raw === "start" || raw === "top" ? "start" : "center";
228
+ }
229
+
230
+ function resolvedPreviewAlign(): "center" | "start" {
231
+ const explicit = text(p.previewAlign ?? p.alignPreview ?? p.previewPlacement, "");
232
+ if (explicit) return normalizePreviewAlign(explicit);
233
+ return sourceType === "markdown" ? "start" : "center";
234
+ }
235
+
236
+ function sourceSelectProps() {
237
+ return {
238
+ subscribe(run: (value: PropValues) => void) {
239
+ run({
240
+ value: sourceType,
241
+ variant: "toolbar",
242
+ options: sourceTypeItems,
243
+ "aria-label": text(p.sourceTypeLabel, "Source type"),
244
+ });
245
+ return () => {};
246
+ },
247
+ };
248
+ }
249
+
250
+ function viewTabsProps() {
251
+ return {
252
+ subscribe(run: (value: PropValues) => void) {
253
+ run({
254
+ value: mode,
255
+ tabs: modeItems.map((item) => ({
256
+ value: item.id,
257
+ label: item.label,
258
+ icon: item.icon,
259
+ iconOnly: true,
260
+ title: item.label,
261
+ })),
262
+ });
263
+ return () => {};
264
+ },
265
+ };
266
+ }
267
+
268
+ function viewTabsCtx(): RenderContext {
269
+ return {
270
+ ...ctx,
271
+ emit(eventName, data) {
272
+ if (eventName === "change") {
273
+ const nextMode = normalizeMode(text(data));
274
+ if (nextMode !== mode) resetPreviewAnchor();
275
+ mode = nextMode;
276
+ return;
277
+ }
278
+ ctx.emit(eventName, data);
279
+ },
280
+ };
281
+ }
282
+
283
+ function actionButtonProps(action: "theme" | "web" | "copy") {
284
+ return {
285
+ subscribe(run: (value: PropValues) => void) {
286
+ run(action === "theme"
287
+ ? {
288
+ variant: "ghost",
289
+ icon: currentTheme === "dark" ? "sun" : "moon",
290
+ iconOnly: true,
291
+ pressed: currentTheme === "dark",
292
+ title: text(p.themeLabel ?? p.themeToggleLabel, "Toggle theme"),
293
+ "aria-label": text(p.themeLabel ?? p.themeToggleLabel, "Toggle theme"),
294
+ }
295
+ : action === "web"
296
+ ? {
297
+ variant: "ghost",
298
+ icon: "arrow-square-out",
299
+ iconOnly: true,
300
+ title: text(p.openWebLabel, "Open in playground"),
301
+ "aria-label": text(p.openWebLabel, "Open in playground"),
302
+ }
303
+ : {
304
+ variant: "ghost",
305
+ icon: "copy",
306
+ iconOnly: true,
307
+ title: text(p.copyLabel, "Copy source"),
308
+ "aria-label": text(p.copyLabel, "Copy source"),
309
+ });
310
+ return () => {};
311
+ },
312
+ };
313
+ }
314
+
315
+ function actionButtonCtx(action: "theme" | "web" | "copy"): RenderContext {
316
+ return {
317
+ ...ctx,
318
+ emit(eventName, data) {
319
+ if (eventName === "click") {
320
+ if (action === "theme") toggleTheme();
321
+ else if (action === "web") openWeb();
322
+ else void copySource();
323
+ return;
324
+ }
325
+ ctx.emit(eventName, data);
326
+ },
327
+ };
328
+ }
329
+
330
+ function toggleTheme() {
331
+ const next = document.documentElement.classList.contains("dark") ? "light" : "dark";
332
+ currentTheme = next;
333
+ document.documentElement.classList.toggle("dark", next === "dark");
334
+ document.documentElement.classList.toggle("light", next === "light");
335
+ document.documentElement.dataset.theme = next;
336
+ try {
337
+ window.localStorage?.setItem(themeStorageKey, next);
338
+ } catch {
339
+ // Storage can be unavailable in privacy-restricted contexts.
340
+ }
341
+ }
342
+
343
+ function sourceSelectCtx(): RenderContext {
344
+ return {
345
+ ...ctx,
346
+ emit(eventName, data) {
347
+ if (eventName === "change" || eventName === "select") {
348
+ const nextSourceType = normalizeSourceKind(text(data), source);
349
+ if (nextSourceType !== sourceType) resetPreviewAnchor();
350
+ sourceType = nextSourceType;
351
+ return;
352
+ }
353
+ ctx.emit(eventName, data);
354
+ },
355
+ };
356
+ }
357
+
358
+ function openWeb() {
359
+ const base = text(p.webUrl ?? p.playgroundUrl, "/playground.html");
360
+ const url = new URL(base, window.location.href);
361
+ url.searchParams.set("srcs", source);
362
+ url.searchParams.set("type", sourceType);
363
+ url.searchParams.set("mode", mode);
364
+ const domain = text(p.domain);
365
+ const pluginVersion = text(p.pluginVersion ?? p.version);
366
+ if (domain) url.searchParams.set("domain", domain);
367
+ if (pluginVersion) url.searchParams.set("pluginVersion", pluginVersion);
368
+ window.open(url.toString(), "_blank", "noopener,noreferrer");
369
+ }
370
+
371
+ async function copySource() {
372
+ if (navigator.clipboard?.writeText) {
373
+ try {
374
+ await navigator.clipboard.writeText(source);
375
+ return;
376
+ } catch {
377
+ // Fall back for non-secure origins or denied clipboard permissions.
378
+ }
379
+ }
380
+
381
+ const textarea = document.createElement("textarea");
382
+ textarea.value = source;
383
+ textarea.setAttribute("readonly", "");
384
+ textarea.style.position = "fixed";
385
+ textarea.style.opacity = "0";
386
+ document.body.appendChild(textarea);
387
+ textarea.select();
388
+ document.execCommand("copy");
389
+ textarea.remove();
390
+ }
391
+
392
+ function startDrag(event: PointerEvent) {
393
+ if (!splitSurface) return;
394
+ event.preventDefault();
395
+ activeDragPointerId = typeof event.pointerId === "number" ? event.pointerId : null;
396
+ dragging = true;
397
+ if (typeof splitSurface.setPointerCapture !== "function") return;
398
+ try {
399
+ splitSurface.setPointerCapture(event.pointerId);
400
+ } catch {
401
+ // Pointer capture can fail if the browser has already cancelled the pointer.
402
+ }
403
+ }
404
+
405
+ function updateDrag(event: PointerEvent) {
406
+ if (!dragging || !splitSurface) return;
407
+ const rect = splitSurface.getBoundingClientRect();
408
+ const raw = compact
409
+ ? ((event.clientY - rect.top) / rect.height) * 100
410
+ : ((event.clientX - rect.left) / rect.width) * 100;
411
+ splitPercent = Math.min(72, Math.max(18, raw));
412
+ }
413
+
414
+ function endDrag(event?: PointerEvent) {
415
+ const pointerId = event?.pointerId ?? activeDragPointerId;
416
+ dragging = false;
417
+ activeDragPointerId = null;
418
+ if (
419
+ splitSurface
420
+ && pointerId !== null
421
+ && typeof splitSurface.hasPointerCapture === "function"
422
+ && typeof splitSurface.releasePointerCapture === "function"
423
+ && splitSurface.hasPointerCapture(pointerId)
424
+ ) {
425
+ splitSurface.releasePointerCapture(pointerId);
426
+ }
427
+ }
428
+
429
+ function splitStyle() {
430
+ return compact
431
+ ? `grid-template-rows:${splitPercent}% 8px minmax(0, 1fr);`
432
+ : `grid-template-columns:${splitPercent}% 8px minmax(0, 1fr);`;
433
+ }
434
+
435
+ function previewScrollContainer() {
436
+ if (!previewNode) return null;
437
+ return mode === "render" ? previewNode.parentElement as HTMLElement | null : previewNode;
438
+ }
439
+
440
+ function resetPreviewAnchor() {
441
+ previewAnchorFixed = false;
442
+ previewAnchorOffset = 0;
443
+ }
444
+
445
+ function capturePreviewAnchor() {
446
+ if (!previewNode || previewOverflow || resolvedPreviewAlign() !== "center") return;
447
+ const container = previewScrollContainer();
448
+ if (!container) return;
449
+ const target = (mode === "render" ? previewNode : previewNode.firstElementChild) as HTMLElement | null;
450
+ if (!target) return;
451
+
452
+ const containerRect = container.getBoundingClientRect();
453
+ const targetRect = target.getBoundingClientRect();
454
+ const styles = window.getComputedStyle(container);
455
+ const paddingTop = Number.parseFloat(styles.paddingTop || "0");
456
+ previewAnchorOffset = Math.max(0, targetRect.top - containerRect.top - paddingTop + container.scrollTop);
457
+ previewAnchorFixed = true;
458
+ }
459
+
460
+ function measurePreviewOverflow() {
461
+ if (!previewNode) {
462
+ previewOverflow = false;
463
+ return;
464
+ }
465
+
466
+ const container = previewScrollContainer();
467
+ if (!container) {
468
+ previewOverflow = false;
469
+ return;
470
+ }
471
+
472
+ const content = previewNode.firstElementChild instanceof HTMLElement
473
+ ? previewNode.firstElementChild
474
+ : previewNode;
475
+ const styles = window.getComputedStyle(container);
476
+ const availableHeight = Math.max(
477
+ 0,
478
+ container.clientHeight - Number.parseFloat(styles.paddingTop || "0") - Number.parseFloat(styles.paddingBottom || "0"),
479
+ );
480
+ const availableWidth = Math.max(
481
+ 0,
482
+ container.clientWidth - Number.parseFloat(styles.paddingLeft || "0") - Number.parseFloat(styles.paddingRight || "0"),
483
+ );
484
+
485
+ previewOverflow = content.scrollHeight > availableHeight + 1 || content.scrollWidth > availableWidth + 1;
486
+ }
487
+
488
+ function schedulePreviewOverflowMeasure() {
489
+ if (previewOverflowFrame) return;
490
+ previewOverflowFrame = window.requestAnimationFrame(() => {
491
+ previewOverflowFrame = 0;
492
+ measurePreviewOverflow();
493
+ });
494
+ }
495
+
496
+ function isSlexLanguage(language: string): boolean {
497
+ return language === "slex";
498
+ }
499
+
500
+ function resolveSourceKind(value: string): "slex" | "markdown" {
501
+ return sourceType;
502
+ }
503
+
504
+ function normalizeSourceKind(value: string, valueSource = source): SourceKind {
505
+ const raw = value.trim().toLowerCase();
506
+ if (raw === "markdown" || raw === "site-markdown" || raw === "md") return "markdown";
507
+ if (isSlexLanguage(raw)) return "slex";
508
+ return looksLikeSlexSource(valueSource) ? "slex" : "markdown";
509
+ }
510
+
511
+ function looksLikeSlexSource(value: string) {
512
+ const raw = String(value ?? "").trim();
513
+ if (!raw || /^(```|~~~)/.test(raw)) return false;
514
+ if (!/^(?:export\s+default\s+)?\(?\s*\{/.test(raw)) return false;
515
+ return /["']?(slex|namespace|layout|g)["']?\s*:/.test(raw);
516
+ }
517
+
518
+ function parseSlexDocuments(value: string) {
519
+ const raw = String(value ?? "").trim();
520
+ if (!raw) return { ok: true as const, value: [] as SlexExpression[] };
521
+ const wrapped = raw.startsWith("[") ? raw : `[${raw}]`;
522
+ const parsed = parseSlexSource(wrapped);
523
+ if (!parsed.ok) return parsed;
524
+ const valueList = Array.isArray(parsed.value) ? parsed.value : [parsed.value];
525
+ return {
526
+ ok: true as const,
527
+ value: valueList.filter((item): item is SlexExpression => !!item && typeof item === "object") as SlexExpression[],
528
+ };
529
+ }
530
+
531
+ function analyze(value: string): Diagnostic {
532
+ const blocks = Array.from(value.matchAll(/(```|~~~)slex\s*\n([\s\S]*?)\n\1/g), (match, index) => {
533
+ const code = match[2];
534
+ const startIndex = (match.index ?? 0) + match[0].indexOf(code);
535
+ return {
536
+ code,
537
+ index: index + 1,
538
+ startLine: value.slice(0, startIndex).split("\n").length,
539
+ };
540
+ });
541
+ const candidates = blocks.length
542
+ ? blocks
543
+ : looksLikeSlexSource(value)
544
+ ? [{ code: value, index: 1, startLine: 1 }]
545
+ : [];
546
+
547
+ for (const block of candidates) {
548
+ if (!block.code.trim()) continue;
549
+ const parsed = parseSlexDocuments(block.code);
550
+ if (!parsed.ok) {
551
+ const diagnostic = parsed.diagnostic;
552
+ return {
553
+ ok: false,
554
+ block: block.index,
555
+ message: diagnostic.message,
556
+ editorLine: block.startLine + diagnostic.line - 1,
557
+ column: diagnostic.column,
558
+ detail: diagnostic.detail ?? "",
559
+ excerpt: diagnostic.excerpt,
560
+ };
561
+ }
562
+ }
563
+ return { ok: true };
564
+ }
565
+
566
+ function preview(node: HTMLElement) {
567
+ let cleanup: (() => void) | undefined;
568
+ let markdownApp: ReturnType<typeof mountSvelte> | undefined;
569
+ let renderToken = 0;
570
+
571
+ const renderNow = () => {
572
+ cleanup?.();
573
+ cleanup = undefined;
574
+ if (markdownApp) {
575
+ void unmount(markdownApp);
576
+ markdownApp = undefined;
577
+ }
578
+ node.replaceChildren();
579
+
580
+ const diagnostic = analyze(previewSource);
581
+ if (!diagnostic.ok) {
582
+ node.appendChild(syntaxErrorNode(diagnostic));
583
+ schedulePreviewOverflowMeasure();
584
+ return;
585
+ }
586
+
587
+ if (resolveSourceKind(previewSource) === "slex") {
588
+ const host = previewInnerNode();
589
+ node.appendChild(host);
590
+ const parsed = parseSlexDocuments(previewSource);
591
+ if (!parsed.ok) {
592
+ node.replaceChildren();
593
+ node.appendChild(syntaxErrorNode({
594
+ ok: false,
595
+ block: 1,
596
+ message: parsed.diagnostic.message,
597
+ editorLine: parsed.diagnostic.line,
598
+ column: parsed.diagnostic.column,
599
+ detail: parsed.diagnostic.detail ?? "",
600
+ excerpt: parsed.diagnostic.excerpt,
601
+ }));
602
+ schedulePreviewOverflowMeasure();
603
+ return;
604
+ }
605
+ const cleanups = parsed.value.map((script) => {
606
+ const block = document.createElement("div");
607
+ block.className = "slex-playground-document";
608
+ host.appendChild(block);
609
+ return mountSlexKit(script, block);
610
+ });
611
+ cleanup = () => {
612
+ for (const dispose of cleanups) dispose();
613
+ };
614
+ } else {
615
+ const host = previewInnerNode();
616
+ node.appendChild(host);
617
+ markdownApp = mountSvelte(PlaygroundMarkdown, {
618
+ target: host,
619
+ props: {
620
+ content: previewSource,
621
+ domain: text(p.domain, "playground"),
622
+ },
623
+ });
624
+ }
625
+ schedulePreviewOverflowMeasure();
626
+ };
627
+
628
+ const render = () => {
629
+ const token = ++renderToken;
630
+ queueMicrotask(() => {
631
+ if (token !== renderToken) return;
632
+ renderNow();
633
+ });
634
+ };
635
+
636
+ render();
637
+ return {
638
+ update: render,
639
+ destroy() {
640
+ renderToken += 1;
641
+ cleanup?.();
642
+ if (markdownApp) void unmount(markdownApp);
643
+ node.replaceChildren();
644
+ },
645
+ };
646
+ }
647
+
648
+ function syntaxErrorNode(diagnostic: Exclude<Diagnostic, { ok: true }>) {
649
+ const wrapper = previewInnerNode();
650
+
651
+ const panel = document.createElement("div");
652
+ panel.className = "slex-standalone-playground-error";
653
+ panel.setAttribute("role", "alert");
654
+ panel.append(
655
+ element("div", "slex-standalone-playground-error-title", "SlexKit syntax error"),
656
+ element("div", "slex-standalone-playground-error-message", diagnostic.message),
657
+ element("div", "slex-standalone-playground-error-location", `Block ${diagnostic.block}, editor line ${diagnostic.editorLine}, column ${diagnostic.column}`),
658
+ );
659
+ if (diagnostic.detail) {
660
+ panel.appendChild(element("div", "slex-standalone-playground-error-detail", diagnostic.detail));
661
+ }
662
+ panel.appendChild(element("pre", "slex-standalone-playground-error-excerpt", diagnostic.excerpt ?? ""));
663
+ wrapper.appendChild(panel);
664
+ return wrapper;
665
+ }
666
+
667
+ function element(tag: string, className: string, content: string) {
668
+ const node = document.createElement(tag);
669
+ node.className = className;
670
+ node.textContent = content;
671
+ return node;
672
+ }
673
+
674
+ function previewInnerNode() {
675
+ const host = document.createElement("div");
676
+ host.className = "slex-standalone-playground-preview-inner";
677
+ const maxWidth = text(p.previewMaxWidth, "");
678
+ if (maxWidth) host.style.maxWidth = maxWidth;
679
+ return host;
680
+ }
681
+ </script>
682
+
683
+ <section
684
+ bind:this={playgroundNode}
685
+ class={`slex-playground slex-playground--workbench${p.class ? ` ${text(p.class)}` : ""}`}
686
+ data-mode={mode}
687
+ data-source-type={sourceType}
688
+ data-preview-align={resolvedPreviewAlign()}
689
+ data-preview-overflow={previewOverflow ? "true" : "false"}
690
+ data-preview-anchor={previewAnchorFixed ? "fixed" : "auto"}
691
+ style={`--slex-playground-min-height:${text(p.previewMinHeight, "16rem")};--slex-preview-anchor-offset:${previewAnchorOffset}px;`}
692
+ >
693
+ <h2 class="slex-playground-title slex-sr-only">{text(p.title, "Playground")}</h2>
694
+ <div class="slex-playground-chrome">
695
+ <div class="slex-playground-tabs" role="group" aria-label="Playground view">
696
+ <Tabs componentName="tabs" ctx={viewTabsCtx()} props={viewTabsProps()} />
697
+ <div class="slex-playground-source-picker">
698
+ <Select componentName="select" ctx={sourceSelectCtx()} props={sourceSelectProps()} />
699
+ </div>
700
+ </div>
701
+
702
+ <div class="slex-playground-actions">
703
+ {#if themeToggleEnabled()}
704
+ <Button componentName="button" ctx={actionButtonCtx("theme")} props={actionButtonProps("theme")} />
705
+ {/if}
706
+ <Button componentName="button" ctx={actionButtonCtx("web")} props={actionButtonProps("web")} />
707
+ <Button componentName="button" ctx={actionButtonCtx("copy")} props={actionButtonProps("copy")} />
708
+ </div>
709
+ </div>
710
+
711
+ {#if mode === "code"}
712
+ <div class="slex-playground-code-pane">
713
+ <div class="slex-playground-code">
714
+ <CodeMirror
715
+ class="slex-playground-editor"
716
+ doc={source}
717
+ extensions={editorExtensions}
718
+ onChange={(value) => source = value}
719
+ onEditorView={(view) => editorView = view}
720
+ />
721
+ </div>
722
+ </div>
723
+ {:else if mode === "render"}
724
+ <div class="slex-playground-preview-pane" onpointerdowncapture={capturePreviewAnchor} onkeydowncapture={capturePreviewAnchor}>
725
+ <div class="slex-playground-preview" bind:this={previewNode} use:preview={`${sourceType}:${previewSource}`}></div>
726
+ </div>
727
+ {:else}
728
+ <div
729
+ class={`slex-playground-live-pane ${compact ? "vertical" : "horizontal"}`}
730
+ style={splitStyle()}
731
+ bind:this={splitSurface}
732
+ onpointermove={updateDrag}
733
+ onpointerup={endDrag}
734
+ onpointercancel={endDrag}
735
+ onlostpointercapture={endDrag}
736
+ >
737
+ <div class="slex-playground-live-code">
738
+ <div class="slex-playground-code">
739
+ <CodeMirror
740
+ class="slex-playground-editor"
741
+ doc={source}
742
+ extensions={editorExtensions}
743
+ onChange={(value) => source = value}
744
+ onEditorView={(view) => editorView = view}
745
+ />
746
+ </div>
747
+ </div>
748
+ <div
749
+ class:dragging
750
+ class="slex-playground-splitter"
751
+ role="separator"
752
+ tabindex="0"
753
+ aria-orientation={compact ? "horizontal" : "vertical"}
754
+ onpointerdown={startDrag}
755
+ ></div>
756
+ <div
757
+ class="slex-playground-live-preview"
758
+ bind:this={previewNode}
759
+ use:preview={`${sourceType}:${previewSource}`}
760
+ onpointerdowncapture={capturePreviewAnchor}
761
+ onkeydowncapture={capturePreviewAnchor}
762
+ ></div>
763
+ </div>
764
+ {/if}
765
+ </section>