@selvajs/ui 6.0.0-beta.8 → 6.1.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 (94) hide show
  1. package/README.md +5 -7
  2. package/dist/components/compute/AppLayout.svelte +3 -8
  3. package/dist/components/compute/AppLayout.svelte.d.ts +0 -2
  4. package/dist/components/compute/ComputeApp.svelte +30 -43
  5. package/dist/components/compute/ComputeApp.svelte.d.ts +11 -24
  6. package/dist/components/compute/ParameterPresetManager.svelte +4 -9
  7. package/dist/components/compute/ParameterPresetManager.svelte.d.ts +1 -1
  8. package/dist/components/layout/AppShell.svelte +5 -13
  9. package/dist/components/layout/AppShell.svelte.d.ts +1 -2
  10. package/dist/components/layout/EmptyState.svelte +2 -3
  11. package/dist/components/layout/PageFooter.svelte +0 -4
  12. package/dist/components/layout/index.js +0 -1
  13. package/dist/components/preview/Group.svelte +2 -5
  14. package/dist/components/preview/ImageOutput.svelte +5 -4
  15. package/dist/components/preview/InputControl.svelte +27 -28
  16. package/dist/components/preview/InputControl.svelte.d.ts +3 -3
  17. package/dist/components/preview/OutputDisplay.svelte +3 -6
  18. package/dist/components/preview/TabLayout.svelte +3 -3
  19. package/dist/components/preview/inputs/FileInput.svelte +9 -12
  20. package/dist/components/primitives/field/field-error.svelte +1 -3
  21. package/dist/components/primitives/index.js +0 -1
  22. package/dist/components/viewer/SceneManager.svelte +9 -20
  23. package/dist/components/viewer/SceneManager.svelte.d.ts +3 -3
  24. package/dist/components/viewer/Viewer.svelte +42 -63
  25. package/dist/components/viewer/Viewer.svelte.d.ts +8 -19
  26. package/dist/composables/useFooterItem.svelte.d.ts +3 -4
  27. package/dist/composables/useFooterItem.svelte.js +3 -3
  28. package/dist/compute/solving.svelte.d.ts +6 -6
  29. package/dist/compute/solving.svelte.js +6 -6
  30. package/dist/compute/useSolveSession.svelte.d.ts +5 -7
  31. package/dist/compute/useSolveSession.svelte.js +11 -24
  32. package/dist/constants.js +4 -4
  33. package/dist/contexts/clientSlotContext.svelte.d.ts +3 -5
  34. package/dist/contexts/footerContext.svelte.d.ts +6 -8
  35. package/dist/external/storage.js +2 -3
  36. package/dist/i18n/localeContext.svelte.d.ts +5 -7
  37. package/dist/i18n/localeContext.svelte.js +8 -15
  38. package/dist/i18n/messages.d.ts +0 -2
  39. package/dist/i18n/messages.js +3 -8
  40. package/dist/index.js +7 -13
  41. package/dist/public.js +14 -30
  42. package/dist/schema/defaults.js +2 -3
  43. package/dist/schema/dynamic-value-list.js +13 -15
  44. package/dist/schema/param-exporter.d.ts +7 -14
  45. package/dist/schema/param-exporter.js +6 -12
  46. package/dist/schema/traversal.js +3 -4
  47. package/dist/schema/visibility-rules.d.ts +4 -8
  48. package/dist/schema/visibility-rules.js +4 -8
  49. package/dist/types/actionButton.d.ts +1 -25
  50. package/dist/types/presetLabels.d.ts +2 -3
  51. package/dist/utils/color.js +6 -9
  52. package/dist/utils/debounce.d.ts +1 -11
  53. package/dist/utils/debounce.js +1 -11
  54. package/dist/utils/loadScript.d.ts +2 -7
  55. package/dist/utils/loadScript.js +2 -7
  56. package/package.json +17 -17
  57. package/src/lib/components/compute/AppLayout.svelte +3 -8
  58. package/src/lib/components/compute/ComputeApp.svelte +30 -43
  59. package/src/lib/components/compute/ParameterPresetManager.svelte +4 -9
  60. package/src/lib/components/layout/AppShell.svelte +5 -13
  61. package/src/lib/components/layout/EmptyState.svelte +2 -3
  62. package/src/lib/components/layout/PageFooter.svelte +0 -4
  63. package/src/lib/components/layout/index.ts +0 -1
  64. package/src/lib/components/preview/Group.svelte +2 -5
  65. package/src/lib/components/preview/ImageOutput.svelte +5 -4
  66. package/src/lib/components/preview/InputControl.svelte +27 -28
  67. package/src/lib/components/preview/OutputDisplay.svelte +3 -6
  68. package/src/lib/components/preview/TabLayout.svelte +3 -3
  69. package/src/lib/components/preview/inputs/FileInput.svelte +9 -12
  70. package/src/lib/components/primitives/field/field-error.svelte +1 -3
  71. package/src/lib/components/primitives/index.ts +0 -2
  72. package/src/lib/components/viewer/SceneManager.svelte +9 -20
  73. package/src/lib/components/viewer/Viewer.svelte +42 -63
  74. package/src/lib/composables/useFooterItem.svelte.ts +3 -4
  75. package/src/lib/compute/solving.svelte.ts +6 -6
  76. package/src/lib/compute/useSolveSession.svelte.ts +11 -24
  77. package/src/lib/constants.ts +4 -4
  78. package/src/lib/contexts/clientSlotContext.svelte.ts +7 -12
  79. package/src/lib/contexts/footerContext.svelte.ts +6 -8
  80. package/src/lib/external/storage.ts +2 -3
  81. package/src/lib/i18n/localeContext.svelte.ts +8 -17
  82. package/src/lib/i18n/messages.ts +3 -10
  83. package/src/lib/index.ts +8 -13
  84. package/src/lib/public.ts +22 -40
  85. package/src/lib/schema/defaults.ts +2 -3
  86. package/src/lib/schema/dynamic-value-list.ts +13 -15
  87. package/src/lib/schema/param-exporter.ts +9 -16
  88. package/src/lib/schema/traversal.ts +3 -4
  89. package/src/lib/schema/visibility-rules.ts +4 -8
  90. package/src/lib/types/actionButton.ts +1 -25
  91. package/src/lib/types/presetLabels.ts +2 -3
  92. package/src/lib/utils/color.ts +6 -9
  93. package/src/lib/utils/debounce.ts +1 -11
  94. package/src/lib/utils/loadScript.ts +2 -7
package/src/lib/index.ts CHANGED
@@ -1,31 +1,28 @@
1
- // Layout components (page chrome: shell, header, footer, nav)
1
+ // The in-monorepo barrel: everything public.ts publishes, plus the design system,
2
+ // page chrome, contexts and utils that never ship to npm. Reached via the
3
+ // "selva-source" export condition.
4
+
2
5
  export * from './components/layout';
3
6
 
4
- // Compute app (schema + viewer + solve controls composed into a runnable app)
5
7
  export { default as AppLayout } from './components/compute/AppLayout.svelte';
6
8
  export { default as ComputeApp } from './components/compute/ComputeApp.svelte';
7
9
 
8
- // Error screen
9
10
  export { default as ErrorScreen } from './components/ErrorScreen.svelte';
10
11
 
11
12
  // Design-system primitives (shadcn-svelte + custom)
12
13
  export * from './components/primitives';
13
14
  export { default as StateDisplay } from './components/primitives/StateDisplay.svelte';
14
15
 
15
- // 3D viewer
16
16
  export { default as Viewer } from './components/viewer/Viewer.svelte';
17
17
 
18
- // Utilities
19
18
  export * from './schema/defaults';
20
19
  export * from './schema/dynamic-value-list';
21
20
  export * from './schema/traversal';
22
21
  export * from './compute/solving.svelte';
23
22
 
24
- // Solve Session seam. The session itself now lives in `@selvajs/solve/client`
25
- // (framework-free); `useSolveSession` is this package's Svelte binding, which republishes
26
- // the session's subscribe() notifications as rune state so its getters read reactively in
27
- // markup. Re-exported so transports outside this package — e.g. plugin-ui's WebSocket
28
- // driver — can satisfy SolveDriver and drive a session. See CONTEXT.md.
23
+ // Solve Session seam. Re-exported so transports outside this package — e.g.
24
+ // plugin-ui's WebSocket driver can satisfy SolveDriver and drive a session.
25
+ // See CONTEXT.md.
29
26
  export { useSolveSession } from './compute/useSolveSession.svelte';
30
27
  export {
31
28
  createSolveSession,
@@ -36,7 +33,7 @@ export {
36
33
  type SolveReporter
37
34
  } from '@selvajs/solve/client';
38
35
 
39
- // External-input transit storage (used by routes that wire pre-step producers)
36
+ // Pre-step producer transit storage
40
37
  export {
41
38
  writeExternalValue,
42
39
  readExternalValue,
@@ -46,12 +43,10 @@ export {
46
43
  type ExternalInput
47
44
  } from '@selvajs/solve/client';
48
45
 
49
- // Contexts & Composables
50
46
  export * from './contexts/footerContext.svelte';
51
47
  export * from './contexts/clientSlotContext.svelte';
52
48
  export * from './composables/useFooterItem.svelte';
53
49
 
54
- // Utils (cn function)
55
50
  export * from './utils';
56
51
  export { randomId } from './utils/randomId';
57
52
 
package/src/lib/public.ts CHANGED
@@ -1,40 +1,24 @@
1
- // ============================================================================
2
- // Published public API of @selvajs/ui
3
- // ============================================================================
1
+ // The only surface npm consumers of @selvajs/ui see. package.json maps the "."
2
+ // export's `svelte`/`types` conditions at this file's build output.
4
3
  //
5
- // This is the ONLY surface npm consumers of @selvajs/ui see (package.json maps
6
- // the "." export's `svelte`/`types` conditions at this file's build output).
7
- //
8
- // Scope: the compute-app SDK everything an external host app needs to embed a
9
- // Grasshopper-driven app (ComputeApp), drive solves, and wire pre-step
10
- // producers. Verified against real external host apps: they import
11
- // ComputeApp + its types, the solve seam, and the external-input storage
12
- // helpers. Nothing else.
13
- //
14
- // Deliberately NOT public: design-system primitives (Button, Card, Dialog, …),
15
- // page-chrome layout (AppShell, SideNav, …), toast/Toaster, ThemeSwitcher,
16
- // DataTable, contexts/composables, cn/randomId. These remain importable from
17
- // the full barrel (./index.ts) INSIDE the monorepo via the "@selvajs/source"
18
- // export condition, but never ship to npm. If an external consumer ever needs a
19
- // primitive directly, promote it here explicitly rather than re-exporting the
20
- // whole primitives barrel.
4
+ // Design-system primitives, page chrome, toasts, DataTable, contexts and small
5
+ // utils stay out on purpose: they remain importable from ./index.ts inside the
6
+ // monorepo via the "selva-source" export condition, but never ship to npm.
7
+ // Promote a symbol here explicitly rather than re-exporting a whole barrel.
21
8
 
22
- // Compute app (schema + viewer + solve controls composed into a runnable app)
9
+ // Compute app: schema + viewer + solve controls composed into a runnable app.
23
10
  export { default as AppLayout } from './components/compute/AppLayout.svelte';
24
11
  export { default as ComputeApp } from './components/compute/ComputeApp.svelte';
25
12
 
26
- // Standalone 3D viewer. Render Grasshopper meshes on their own, outside a
27
- // ComputeApp host — external apps drive it directly with a `meshes` array and
28
- // an optional `viewerConfig`. Pass `lang` to localize its chrome, or provide a
29
- // locale context (setLocaleContext) at the host root to drive it app-wide.
13
+ // Standalone 3D viewer: renders Grasshopper meshes outside a ComputeApp host,
14
+ // driven directly with a `meshes` array and an optional `viewerConfig`.
30
15
  export { default as Viewer, type ViewerConfig } from './components/viewer/Viewer.svelte';
31
16
 
32
17
  // Viewer app seam. `onViewerReady` (on <Viewer> and <ComputeApp>) hands over the
33
18
  // live three.js viewer, so a host can draw its own content into the same scene as
34
- // the solve results a point cloud, draft lines, annotations and register
35
- // pointer tools that claim clicks ahead of object selection. Re-exported from
36
- // @selvajs/visualization/render so hosts can annotate without depending on it
37
- // directly; `three` stays a peer dep of that package either way.
19
+ // the solve results and register pointer tools that claim clicks ahead of object
20
+ // selection. Re-exported from @selvajs/visualization/render so hosts can annotate
21
+ // without depending on it directly; `three` stays a peer dep of that package either way.
38
22
  export type {
39
23
  ThreeViewer,
40
24
  PointerTool,
@@ -47,10 +31,9 @@ export type {
47
31
  } from '@selvajs/visualization/render';
48
32
  export { appSource, isHostOwned, isOwnedBy, pointerToNdc } from '@selvajs/visualization/render';
49
33
 
50
- // Viewer localization. The library renders English + German chrome; switch at
51
- // runtime by passing `lang` to <Viewer> or by setting a reactive locale context
52
- // once at the host root (e.g. feed in an app-wide Paraglide locale). Does not
53
- // translate Grasshopper-sourced names/metadata.
34
+ // Viewer localization: English + German chrome, switched by passing `lang` to
35
+ // <Viewer> or by setting a reactive locale context once at the host root.
36
+ // Does not translate Grasshopper-sourced names/metadata.
54
37
  export type { Locale, ViewerMessages } from './i18n/messages';
55
38
  export { VIEWER_MESSAGES, DEFAULT_LOCALE, messagesFor } from './i18n/messages';
56
39
  export {
@@ -59,14 +42,13 @@ export {
59
42
  type LocaleContext
60
43
  } from './i18n/localeContext.svelte';
61
44
 
62
- // Full-screen states a host app renders
45
+ // Full-screen states a host app renders.
63
46
  export { default as ErrorScreen } from './components/ErrorScreen.svelte';
64
47
 
65
- // Solve Session seam (transport-agnostic value/lifecycle state machine + its
66
- // driver interface). The session lives in `@selvajs/solve/client` and is
48
+ // Solve Session seam, for hosts driving a session themselves rather than
49
+ // embedding <ComputeApp>. The session lives in `@selvajs/solve/client` and is
67
50
  // framework-free; `useSolveSession` is the Svelte binding that makes its getters
68
- // read reactively inside components. A host embedding <ComputeApp> needs neither —
69
- // both are re-exported for hosts driving a session themselves. See CONTEXT.md.
51
+ // read reactively inside components. See CONTEXT.md.
70
52
  export { useSolveSession } from './compute/useSolveSession.svelte';
71
53
  export {
72
54
  createSolveSession,
@@ -77,8 +59,8 @@ export {
77
59
  type SolveReporter
78
60
  } from '@selvajs/solve/client';
79
61
 
80
- // Client-slot context type (host apps render their own cell for client-sourced
81
- // inputs, and may commit a value back via ClientSlotArgs.onValueChange).
62
+ // Client-slot context: host apps render their own cell for client-sourced inputs,
63
+ // and may commit a value back via ClientSlotArgs.onValueChange.
82
64
  export type { ClientSlotArgs, ClientSlot } from './contexts/clientSlotContext.svelte';
83
65
 
84
66
  // Pre-step producer transit storage (host apps wire producers via these).
@@ -91,7 +73,7 @@ export {
91
73
  type ExternalInput
92
74
  } from '@selvajs/solve/client';
93
75
 
94
- // Schema utilities a ComputeApp host reasonably needs to read/shape values.
76
+ // Schema utilities a ComputeApp host needs to read/shape values.
95
77
  export * from './schema/defaults';
96
78
  export * from './schema/traversal';
97
79
  export * from './schema/dynamic-value-list';
@@ -1,4 +1,3 @@
1
- // Moved to @selvajs/schemas so server-side callers can share it without pulling
2
- // in the UI package. Re-exported here to keep existing `@selvajs/ui` importers
3
- // working.
1
+ // Moved to @selvajs/schemas so server-side callers can share it without pulling in the
2
+ // UI package. Re-exported to keep existing @selvajs/ui importers working.
4
3
  export { getDefaultValue } from '@selvajs/schemas';
@@ -41,12 +41,11 @@ function coercePayload(value: unknown): DynamicValueListPayload | null {
41
41
  }
42
42
 
43
43
  // Memoizes string-payload coercion. In compute mode the payload arrives as a JSON
44
- // string — potentially several MB for a large options list — and the options map is
45
- // derived from `values` (TabLayout), so it recomputes on EVERY value change. Without
46
- // memoization each keystroke/output-merge re-parses megabytes and allocates a fresh
47
- // options object, whose new identity re-renders the entire dropdown subtree; with a
48
- // 6.4 MB payload this was measured driving a tab out of memory. Map keys use
49
- // SameValueZero, so an identical response string from a later solve also hits.
44
+ // string — several MB for a large options list — and the options map is derived from
45
+ // `values` (TabLayout), so it recomputes on EVERY value change. Unmemoized, each
46
+ // keystroke re-parses megabytes and allocates a fresh options object whose new
47
+ // identity re-renders the whole dropdown subtree; a 6.4 MB payload drove a tab OOM.
48
+ // Map keys use SameValueZero, so an identical string from a later solve also hits.
50
49
  const coerceCache = new Map<string, DynamicValueListPayload | null>();
51
50
  const COERCE_CACHE_MAX = 8;
52
51
 
@@ -62,10 +61,9 @@ function coercePayloadMemo(value: unknown): DynamicValueListPayload | null {
62
61
  }
63
62
  const parseStart = performance.now();
64
63
  const parsed = coercePayload(value);
65
- // Diagnostic: an expensive parse should happen ONCE per distinct solve result.
66
- // If this line storms in the console, payload memoization is being defeated
67
- // (e.g. payload strings differing per recompute) — the pre-memoization churn
68
- // pattern that could OOM a tab.
64
+ // An expensive parse should happen ONCE per distinct solve result. If this line
65
+ // storms the console, memoization is being defeated (payload strings differing
66
+ // per recompute) — the churn pattern that could OOM a tab.
69
67
  if (value.length > 256 * 1024) {
70
68
  const optionCount = parsed?.options ? Object.keys(parsed.options).length : 0;
71
69
  console.info(
@@ -90,11 +88,11 @@ interface DynamicValueListSource {
90
88
  /**
91
89
  * Every dynamicValueList output reference in the schema.
92
90
  *
93
- * Canonical location is `schema.outputs[]` — the plugin's SchemaSynchronizer enforces that every
94
- * dynamicValueList layout item is mirrored there (see CanonicalizeDynamicValueListOutputs). We ALSO
95
- * scan the layout purely as back-compat defense for schemas persisted by an older plugin that lacked
96
- * that invariant; for current schemas the layout pass finds nothing new.
97
- * Deduped by id, outputs[] winning so the canonical record's targetInputId takes precedence.
91
+ * Canonical location is `schema.outputs[]` — the plugin's SchemaSynchronizer mirrors every
92
+ * dynamicValueList layout item there (`CanonicalizeDynamicValueListOutputs`). The layout scan is
93
+ * back-compat defense for schemas persisted by an older plugin that lacked that invariant; on
94
+ * current schemas it finds nothing new. Deduped by id, `outputs[]` winning so the canonical
95
+ * record's targetInputId takes precedence.
98
96
  */
99
97
  function collectDynamicValueListSources(schema: UISchema): DynamicValueListSource[] {
100
98
  const byId = new Map<string, DynamicValueListSource>();
@@ -107,11 +107,9 @@ export function validateSavedState(
107
107
  }
108
108
 
109
109
  /**
110
- * The subset of a preset's parameters that can safely be applied to the current schema:
111
- * every param that still exists as an input. A param that no longer exists is dropped
112
- * (it's the same condition `validateSavedState` flags as an error), so this derives its
113
- * own "loadable" rule from the schema rather than re-reading validation issues — no
114
- * coupling to how sentinel ids are encoded.
110
+ * Params the current schema no longer has are dropped. That's the same condition
111
+ * `validateSavedState` flags as an error, but the rule is re-derived from the schema here
112
+ * rather than read back off the issue list nothing couples to how sentinel ids are encoded.
115
113
  */
116
114
  export function extractLoadableValues(
117
115
  savedState: ParameterPreset,
@@ -127,23 +125,18 @@ export function extractLoadableValues(
127
125
  return values;
128
126
  }
129
127
 
130
- /** The full outcome of loading a preset: what to apply, plus the diagnostics to surface. */
131
128
  export interface PresetLoadResult {
132
- /** Values safe to apply now (params that still exist). */
129
+ /** Safe to apply now — the params that still exist in the schema. */
133
130
  values: Record<string, unknown>;
134
- /** All validation issues (errors + warnings) for the load dialog. */
131
+ /** Errors and warnings both, for the load dialog. */
135
132
  issues: ValidationIssueMessage[];
136
133
  /** No issues at all — load silently. */
137
134
  isValid: boolean;
138
- /** No blocking errors — load is allowed (warnings are fine). */
135
+ /** No errors — warnings alone still allow the load. */
139
136
  canLoad: boolean;
140
137
  }
141
138
 
142
- /**
143
- * Single entry point for applying a preset: validates against the current schema and, in
144
- * the same pass, computes the loadable values. Callers get one object instead of
145
- * threading a validation result back into a separate extraction step.
146
- */
139
+ /** Validates and computes the loadable values in one pass, so callers thread one object. */
147
140
  export function loadPreset(savedState: ParameterPreset, currentSchema: UISchema): PresetLoadResult {
148
141
  const validation = validateSavedState(savedState, currentSchema);
149
142
  return {
@@ -161,8 +154,8 @@ export function exportStateAsJson(savedState: ParameterPreset): void {
161
154
 
162
155
  const link = document.createElement('a');
163
156
  link.href = URL.createObjectURL(blob);
164
- // .slvp = Selva parameter preset. Import accepts legacy .sps files too the content is
165
- // identical JSON, only the writer's extension changed.
157
+ // .slvp = Selva parameter preset. Import still accepts the pre-rename .sps — same JSON,
158
+ // only the writer's extension changed.
166
159
  link.download = `${safeName}_${date}.slvp`;
167
160
  link.click();
168
161
  URL.revokeObjectURL(link.href);
@@ -1,5 +1,4 @@
1
- // Schema layout traversal now lives in @selvajs/schemas (next to the types it walks) so
2
- // any package depending on the schema can traverse it without pulling in @selvajs/ui.
3
- // Re-exported here to keep it part of @selvajs/ui's published surface for existing
4
- // consumers.
1
+ // Moved to @selvajs/schemas, next to the types it walks, so any package depending on the
2
+ // schema can traverse it without pulling in @selvajs/ui. Re-exported to keep existing
3
+ // @selvajs/ui importers working.
5
4
  export { getGroups, getLayoutItems, getInputItems, type InputLayoutItem } from '@selvajs/schemas';
@@ -84,19 +84,15 @@ export function evaluateVisibility(
84
84
  return actionFn(met, defaultValue);
85
85
  }
86
86
 
87
- /**
88
- * Stable key for a layout item: `paramId` for inputs/outputs, `id` for linebreaks.
89
- * Mirrors the key the renderers use in their `{#each}` blocks.
90
- */
87
+ /** Must stay identical to the key the renderers use in their `{#each}` blocks. */
91
88
  export function itemKey(item: LayoutItem): string {
92
89
  return item.type === 'linebreak' ? item.id : item.paramId;
93
90
  }
94
91
 
95
92
  /**
96
- * Evaluates every item's visibility once against the current values, keyed by itemKey.
97
- * Callers that touch the same items more than once per render (column layout + cell
98
- * render, plus the default-value sweep) read from this single map instead of
99
- * re-evaluating per access — one source of truth for "what's visible right now".
93
+ * Evaluates each item once per render. Callers that touch the same item several times
94
+ * (column layout, then cell render, then the default-value sweep) read this map instead of
95
+ * re-evaluating, so they can't disagree about what's visible mid-render.
100
96
  */
101
97
  export function buildVisibilityMap(
102
98
  items: LayoutItem[],
@@ -1,31 +1,7 @@
1
- /**
2
- * Defines an action button for use in the UI.
3
- *
4
- * @example
5
- * ```typescript
6
- * const saveButton: ActionButton = {
7
- * id: 'save-btn',
8
- * label: 'Save Changes',
9
- * icon: SaveIcon,
10
- * variant: 'default',
11
- * size: 'lg',
12
- * onclick: async () => {
13
- * await saveData();
14
- * showNotification('Saved!');
15
- * }
16
- * };
17
- * ```
18
- *
19
- * @property id - Unique identifier for this button
20
- * @property label - Display text shown on the button
21
- * @property icon - Optional icon component to display alongside the label
22
- * @property variant - Button styling variant (controls color/appearance)
23
- * @property size - Button size preset
24
- * @property onclick - Callback function executed when clicked; can be async
25
- */
26
1
  export interface ActionButton {
27
2
  id: string;
28
3
  label: string;
4
+ /** A Svelte component, rendered before the label. */
29
5
  icon?: any;
30
6
  variant?: 'default' | 'outline' | 'destructive' | 'secondary' | 'ghost';
31
7
  size?: 'default' | 'sm' | 'lg';
@@ -1,7 +1,6 @@
1
1
  /**
2
- * Overridable UI strings for the parameter-preset manager (Save/Load flow) and
3
- * footer copyright text. All optional — unset keys fall back to the English
4
- * defaults in `DEFAULT_PRESET_LABELS`. Pass a partial object to localize.
2
+ * Overridable UI strings for the parameter-preset manager. Pass a partial object to localize;
3
+ * unset keys fall back to `DEFAULT_PRESET_LABELS`.
5
4
  */
6
5
  export interface PresetLabels {
7
6
  // Toolbar buttons
@@ -1,32 +1,30 @@
1
- // Helper to convert hex to OKLCH
1
+ // Hex OKLCH. A non-null input that isn't a 6-digit hex comes back unchanged, so
2
+ // callers can pass through CSS colors this function doesn't understand; null gives ''.
2
3
  export const hexToOklch = (hex: string | null): string => {
3
4
  if (!hex) return '';
4
5
 
5
- // Normalize hex (remove # if present)
6
6
  const normalized = hex.replace(/^#/, '');
7
7
 
8
- // If it's not a valid hex color, return as-is
9
8
  if (!/^[0-9A-Fa-f]{6}$/.test(normalized)) {
10
9
  return hex;
11
10
  }
12
11
 
13
- // Parse hex to RGB
14
12
  const r = parseInt(normalized.slice(0, 2), 16) / 255;
15
13
  const g = parseInt(normalized.slice(2, 4), 16) / 255;
16
14
  const b = parseInt(normalized.slice(4, 6), 16) / 255;
17
15
 
18
- // Convert RGB to linear RGB
16
+ // sRGB transfer function, inverted: gamma-encoded channel → linear light.
19
17
  const toLinear = (c: number) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
20
18
  const lr = toLinear(r);
21
19
  const lg = toLinear(g);
22
20
  const lb = toLinear(b);
23
21
 
24
- // Convert linear RGB to XYZ
22
+ // Linear sRGB CIE XYZ (D65 white point).
25
23
  const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
26
24
  const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
27
25
  const z = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
28
26
 
29
- // Convert XYZ to OKLab
27
+ // XYZ OKLab: the M1 cone-response matrix, cube root, then the M2 matrix.
30
28
  const l_ = Math.cbrt(0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z);
31
29
  const m_ = Math.cbrt(0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z);
32
30
  const s_ = Math.cbrt(0.0482003018 * x + 0.2643662691 * y + 0.633851707 * z);
@@ -35,11 +33,10 @@ export const hexToOklch = (hex: string | null): string => {
35
33
  const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
36
34
  const b_lab = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;
37
35
 
38
- // Convert to LCH
36
+ // OKLab OKLCH: chroma and hue as polar coordinates of (a, b).
39
37
  const C = Math.sqrt(a * a + b_lab * b_lab);
40
38
  let H = (Math.atan2(b_lab, a) * 180) / Math.PI;
41
39
  if (H < 0) H += 360;
42
40
 
43
- // Format as OKLCH (round to reasonable precision)
44
41
  return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`;
45
42
  };
@@ -1,14 +1,4 @@
1
- /**
2
- * Debounce utility - delays function execution until after a specified wait time
3
- * has elapsed since the last time it was invoked.
4
- *
5
- * Use for text inputs, search boxes, etc. where you want to wait for the user
6
- * to finish typing before executing.
7
- *
8
- * @param func The function to debounce
9
- * @param wait The delay in milliseconds
10
- * @returns A debounced version of the function with a cancel method
11
- */
1
+ /** Runs `func` once `wait` ms have passed with no further calls. `cancel()` drops a pending call. */
12
2
  export function debounce<T extends (...args: any[]) => any>(
13
3
  func: T,
14
4
  wait: number
@@ -1,13 +1,8 @@
1
1
  const scriptLoaders = new Map<string, Promise<void>>();
2
2
 
3
3
  /**
4
- * Load an external script from a CDN with automatic deduplication.
5
- * All calls to load the same script will share a single promise,
6
- * preventing duplicate script tags and race conditions.
7
- *
8
- * @param src The script URL
9
- * @param options Script attributes (crossOrigin, async, etc.)
10
- * @returns Promise that resolves when script is loaded
4
+ * Appends a `<script>` for `src` and resolves on load. Concurrent calls for the same URL share
5
+ * one promise, so a script never gets a second tag. A failed load is evicted so a retry can run.
11
6
  */
12
7
  export function loadScript(
13
8
  src: string,