@selvajs/ui 6.0.0-beta.7 → 6.0.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.
- package/README.md +5 -7
- package/dist/components/compute/AppLayout.svelte +2 -8
- package/dist/components/compute/AppLayout.svelte.d.ts +0 -2
- package/dist/components/compute/ComputeApp.svelte +25 -43
- package/dist/components/compute/ComputeApp.svelte.d.ts +8 -24
- package/dist/components/compute/ParameterPresetManager.svelte +4 -9
- package/dist/components/compute/ParameterPresetManager.svelte.d.ts +1 -1
- package/dist/components/layout/AppShell.svelte +5 -13
- package/dist/components/layout/AppShell.svelte.d.ts +1 -2
- package/dist/components/layout/EmptyState.svelte +2 -3
- package/dist/components/layout/PageFooter.svelte +0 -4
- package/dist/components/layout/index.js +0 -1
- package/dist/components/preview/Group.svelte +2 -5
- package/dist/components/preview/ImageOutput.svelte +5 -4
- package/dist/components/preview/InputControl.svelte +27 -28
- package/dist/components/preview/InputControl.svelte.d.ts +3 -3
- package/dist/components/preview/OutputDisplay.svelte +3 -6
- package/dist/components/preview/TabLayout.svelte +3 -3
- package/dist/components/preview/inputs/FileInput.svelte +9 -12
- package/dist/components/primitives/field/field-error.svelte +1 -3
- package/dist/components/primitives/index.js +0 -1
- package/dist/components/viewer/SceneManager.svelte +9 -20
- package/dist/components/viewer/SceneManager.svelte.d.ts +3 -3
- package/dist/components/viewer/Viewer.svelte +36 -61
- package/dist/components/viewer/Viewer.svelte.d.ts +6 -19
- package/dist/composables/useFooterItem.svelte.d.ts +3 -4
- package/dist/composables/useFooterItem.svelte.js +3 -3
- package/dist/compute/solving.svelte.d.ts +6 -6
- package/dist/compute/solving.svelte.js +6 -6
- package/dist/compute/useSolveSession.svelte.d.ts +5 -7
- package/dist/compute/useSolveSession.svelte.js +11 -24
- package/dist/constants.js +4 -4
- package/dist/contexts/clientSlotContext.svelte.d.ts +3 -5
- package/dist/contexts/footerContext.svelte.d.ts +6 -8
- package/dist/external/storage.js +2 -3
- package/dist/i18n/localeContext.svelte.d.ts +5 -7
- package/dist/i18n/localeContext.svelte.js +8 -15
- package/dist/i18n/messages.d.ts +0 -2
- package/dist/i18n/messages.js +3 -8
- package/dist/index.js +7 -13
- package/dist/public.js +14 -30
- package/dist/schema/defaults.js +2 -3
- package/dist/schema/dynamic-value-list.js +13 -15
- package/dist/schema/param-exporter.d.ts +7 -14
- package/dist/schema/param-exporter.js +6 -12
- package/dist/schema/traversal.js +3 -4
- package/dist/schema/visibility-rules.d.ts +4 -8
- package/dist/schema/visibility-rules.js +4 -8
- package/dist/types/actionButton.d.ts +1 -25
- package/dist/types/presetLabels.d.ts +2 -3
- package/dist/utils/color.js +6 -9
- package/dist/utils/debounce.d.ts +1 -11
- package/dist/utils/debounce.js +1 -11
- package/dist/utils/file-download.js +4 -2
- package/dist/utils/loadScript.d.ts +2 -7
- package/dist/utils/loadScript.js +2 -7
- package/package.json +16 -16
- package/src/lib/components/compute/AppLayout.svelte +2 -8
- package/src/lib/components/compute/ComputeApp.svelte +25 -43
- package/src/lib/components/compute/ParameterPresetManager.svelte +4 -9
- package/src/lib/components/layout/AppShell.svelte +5 -13
- package/src/lib/components/layout/EmptyState.svelte +2 -3
- package/src/lib/components/layout/PageFooter.svelte +0 -4
- package/src/lib/components/layout/index.ts +0 -1
- package/src/lib/components/preview/Group.svelte +2 -5
- package/src/lib/components/preview/ImageOutput.svelte +5 -4
- package/src/lib/components/preview/InputControl.svelte +27 -28
- package/src/lib/components/preview/OutputDisplay.svelte +3 -6
- package/src/lib/components/preview/TabLayout.svelte +3 -3
- package/src/lib/components/preview/inputs/FileInput.svelte +9 -12
- package/src/lib/components/primitives/field/field-error.svelte +1 -3
- package/src/lib/components/primitives/index.ts +0 -2
- package/src/lib/components/viewer/SceneManager.svelte +9 -20
- package/src/lib/components/viewer/Viewer.svelte +36 -61
- package/src/lib/composables/useFooterItem.svelte.ts +3 -4
- package/src/lib/compute/solving.svelte.ts +6 -6
- package/src/lib/compute/useSolveSession.svelte.ts +11 -24
- package/src/lib/constants.ts +4 -4
- package/src/lib/contexts/clientSlotContext.svelte.ts +7 -12
- package/src/lib/contexts/footerContext.svelte.ts +6 -8
- package/src/lib/external/storage.ts +2 -3
- package/src/lib/i18n/localeContext.svelte.ts +8 -17
- package/src/lib/i18n/messages.ts +3 -10
- package/src/lib/index.ts +8 -13
- package/src/lib/public.ts +22 -40
- package/src/lib/schema/defaults.ts +2 -3
- package/src/lib/schema/dynamic-value-list.ts +13 -15
- package/src/lib/schema/param-exporter.ts +9 -16
- package/src/lib/schema/traversal.ts +3 -4
- package/src/lib/schema/visibility-rules.ts +4 -8
- package/src/lib/types/actionButton.ts +1 -25
- package/src/lib/types/presetLabels.ts +2 -3
- package/src/lib/utils/color.ts +6 -9
- package/src/lib/utils/debounce.ts +1 -11
- package/src/lib/utils/file-download.ts +4 -2
- package/src/lib/utils/loadScript.ts +2 -7
package/dist/utils/color.js
CHANGED
|
@@ -1,38 +1,35 @@
|
|
|
1
|
-
//
|
|
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) => {
|
|
3
4
|
if (!hex)
|
|
4
5
|
return '';
|
|
5
|
-
// Normalize hex (remove # if present)
|
|
6
6
|
const normalized = hex.replace(/^#/, '');
|
|
7
|
-
// If it's not a valid hex color, return as-is
|
|
8
7
|
if (!/^[0-9A-Fa-f]{6}$/.test(normalized)) {
|
|
9
8
|
return hex;
|
|
10
9
|
}
|
|
11
|
-
// Parse hex to RGB
|
|
12
10
|
const r = parseInt(normalized.slice(0, 2), 16) / 255;
|
|
13
11
|
const g = parseInt(normalized.slice(2, 4), 16) / 255;
|
|
14
12
|
const b = parseInt(normalized.slice(4, 6), 16) / 255;
|
|
15
|
-
//
|
|
13
|
+
// sRGB transfer function, inverted: gamma-encoded channel → linear light.
|
|
16
14
|
const toLinear = (c) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
|
|
17
15
|
const lr = toLinear(r);
|
|
18
16
|
const lg = toLinear(g);
|
|
19
17
|
const lb = toLinear(b);
|
|
20
|
-
//
|
|
18
|
+
// Linear sRGB → CIE XYZ (D65 white point).
|
|
21
19
|
const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
|
|
22
20
|
const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
|
|
23
21
|
const z = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
|
|
24
|
-
//
|
|
22
|
+
// XYZ → OKLab: the M1 cone-response matrix, cube root, then the M2 matrix.
|
|
25
23
|
const l_ = Math.cbrt(0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z);
|
|
26
24
|
const m_ = Math.cbrt(0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z);
|
|
27
25
|
const s_ = Math.cbrt(0.0482003018 * x + 0.2643662691 * y + 0.633851707 * z);
|
|
28
26
|
const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
|
|
29
27
|
const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
|
|
30
28
|
const b_lab = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;
|
|
31
|
-
//
|
|
29
|
+
// OKLab → OKLCH: chroma and hue as polar coordinates of (a, b).
|
|
32
30
|
const C = Math.sqrt(a * a + b_lab * b_lab);
|
|
33
31
|
let H = (Math.atan2(b_lab, a) * 180) / Math.PI;
|
|
34
32
|
if (H < 0)
|
|
35
33
|
H += 360;
|
|
36
|
-
// Format as OKLCH (round to reasonable precision)
|
|
37
34
|
return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`;
|
|
38
35
|
};
|
package/dist/utils/debounce.d.ts
CHANGED
|
@@ -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 declare function debounce<T extends (...args: any[]) => any>(func: T, wait: number): ((...args: Parameters<T>) => void) & {
|
|
13
3
|
cancel: () => void;
|
|
14
4
|
};
|
package/dist/utils/debounce.js
CHANGED
|
@@ -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(func, wait) {
|
|
13
3
|
let timeout = null;
|
|
14
4
|
function debounced(...args) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { downloadFileDataByRoot } from '@selvajs/compute/core';
|
|
2
2
|
import { SvelteMap } from 'svelte/reactivity';
|
|
3
3
|
import { APP_DEFAULTS } from '../constants';
|
|
4
4
|
export const MIME_BY_EXT = {
|
|
@@ -45,11 +45,13 @@ export async function downloadFiles(fileData, fileName = 'grasshopper-output') {
|
|
|
45
45
|
console.warn('[FileDownload] No files to download');
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
|
+
// A lone file is saved as itself rather than zipped — the one case that needs the DOM
|
|
49
|
+
// directly. Everything else, including the per-root archive split, belongs to compute.
|
|
48
50
|
if (filesArray.length === 1) {
|
|
49
51
|
saveSingleFile(filesArray[0]);
|
|
50
52
|
return;
|
|
51
53
|
}
|
|
52
|
-
await
|
|
54
|
+
await downloadFileDataByRoot(filesArray, fileName);
|
|
53
55
|
}
|
|
54
56
|
catch (error) {
|
|
55
57
|
console.error('[FileDownload] Error downloading files:', error);
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* preventing duplicate script tags and race conditions.
|
|
5
|
-
*
|
|
6
|
-
* @param src The script URL
|
|
7
|
-
* @param options Script attributes (crossOrigin, async, etc.)
|
|
8
|
-
* @returns Promise that resolves when script is loaded
|
|
2
|
+
* Appends a `<script>` for `src` and resolves on load. Concurrent calls for the same URL share
|
|
3
|
+
* one promise, so a script never gets a second tag. A failed load is evicted so a retry can run.
|
|
9
4
|
*/
|
|
10
5
|
export declare function loadScript(src: string, options?: {
|
|
11
6
|
crossOrigin?: string;
|
package/dist/utils/loadScript.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
const scriptLoaders = new Map();
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* preventing duplicate script tags and race conditions.
|
|
6
|
-
*
|
|
7
|
-
* @param src The script URL
|
|
8
|
-
* @param options Script attributes (crossOrigin, async, etc.)
|
|
9
|
-
* @returns Promise that resolves when script is loaded
|
|
3
|
+
* Appends a `<script>` for `src` and resolves on load. Concurrent calls for the same URL share
|
|
4
|
+
* one promise, so a script never gets a second tag. A failed load is evicted so a retry can run.
|
|
10
5
|
*/
|
|
11
6
|
export function loadScript(src, options) {
|
|
12
7
|
if (scriptLoaders.has(src)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/ui",
|
|
3
|
-
"version": "6.0.0
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Shared UI components and utilities for Selva applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "VektorNode",
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
"@sveltejs/kit": "^2",
|
|
58
58
|
"bits-ui": "^2.18.0",
|
|
59
59
|
"svelte": "^5",
|
|
60
|
-
"tailwind-variants": "^3.3.
|
|
60
|
+
"tailwind-variants": "^3.3.1",
|
|
61
61
|
"three": "^0.185.1",
|
|
62
|
-
"@selvajs/
|
|
63
|
-
"@selvajs/compute": "^4.0.0
|
|
64
|
-
"@selvajs/
|
|
65
|
-
"@selvajs/visualization": "^1.0.0
|
|
62
|
+
"@selvajs/schemas": "^5.0.0",
|
|
63
|
+
"@selvajs/compute": "^4.0.0",
|
|
64
|
+
"@selvajs/solve": "^1.0.0",
|
|
65
|
+
"@selvajs/visualization": "^1.0.0"
|
|
66
66
|
},
|
|
67
67
|
"peerDependenciesMeta": {
|
|
68
68
|
"three": {
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"dependencies": {
|
|
73
73
|
"@floating-ui/dom": "^1.8.0",
|
|
74
74
|
"@iconify/svelte": "^5.2.1",
|
|
75
|
-
"@lucide/svelte": "^1.
|
|
75
|
+
"@lucide/svelte": "^1.30.0",
|
|
76
76
|
"clsx": "^2.1.1",
|
|
77
77
|
"mode-watcher": "^1.1.0",
|
|
78
78
|
"paneforge": "^1.0.2",
|
|
@@ -81,20 +81,20 @@
|
|
|
81
81
|
"tw-animate-css": "^1.4.0"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@internationalized/date": "^3.12.
|
|
85
|
-
"@sveltejs/kit": "2.70.
|
|
84
|
+
"@internationalized/date": "^3.12.3",
|
|
85
|
+
"@sveltejs/kit": "2.70.2",
|
|
86
86
|
"@sveltejs/vite-plugin-svelte": "^7.2.0",
|
|
87
|
-
"@types/three": "^0.185.
|
|
87
|
+
"@types/three": "^0.185.4",
|
|
88
88
|
"bits-ui": "^2.18.0",
|
|
89
89
|
"rimraf": "^6.0.1",
|
|
90
90
|
"svelte": "5.56.8",
|
|
91
|
-
"tailwind-variants": "^3.3.
|
|
91
|
+
"tailwind-variants": "^3.3.1",
|
|
92
92
|
"vitest": "^4.1.10",
|
|
93
|
-
"@selvajs/compute": "4.0.0
|
|
94
|
-
"@selvajs/config": "0.0.
|
|
95
|
-
"@selvajs/schemas": "5.0.0
|
|
96
|
-
"@selvajs/
|
|
97
|
-
"@selvajs/
|
|
93
|
+
"@selvajs/compute": "4.0.0",
|
|
94
|
+
"@selvajs/config": "0.0.4",
|
|
95
|
+
"@selvajs/schemas": "5.0.0",
|
|
96
|
+
"@selvajs/visualization": "1.0.0",
|
|
97
|
+
"@selvajs/solve": "1.0.0"
|
|
98
98
|
},
|
|
99
99
|
"scripts": {
|
|
100
100
|
"predev": "node ../../scripts/sync-shared-assets.js",
|
|
@@ -38,9 +38,7 @@
|
|
|
38
38
|
onListStates?: () => ParameterPreset[] | Promise<ParameterPreset[]>;
|
|
39
39
|
presetLabels?: Partial<PresetLabels>;
|
|
40
40
|
viewerConfig?: ViewerConfig;
|
|
41
|
-
/** Branding logo URL shown as a watermark in the viewer's bottom-right corner. */
|
|
42
41
|
logoUrl?: string;
|
|
43
|
-
/** Hands the live three.js viewer to the host. See `Viewer.svelte`. */
|
|
44
42
|
onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
|
|
45
43
|
}
|
|
46
44
|
|
|
@@ -79,7 +77,7 @@
|
|
|
79
77
|
const hasLeftPanel = $derived(leftTabs.length > 0);
|
|
80
78
|
const hasRightPanel = $derived(rightTabs.length > 0);
|
|
81
79
|
const hasSidebar = $derived(hasViewer || hasRightPanel);
|
|
82
|
-
//
|
|
80
|
+
// Left + right, no viewer — both panes grow to fill the full width.
|
|
83
81
|
const isTwoPanelMode = $derived(!hasViewer && hasLeftPanel && hasRightPanel);
|
|
84
82
|
|
|
85
83
|
let isMobile = $state(false);
|
|
@@ -298,7 +296,7 @@
|
|
|
298
296
|
</div>
|
|
299
297
|
{:else}
|
|
300
298
|
<div class="min-h-0 flex h-full flex-1">
|
|
301
|
-
<!--
|
|
299
|
+
<!-- Outside PaneGroup so the strip keeps a fixed width. -->
|
|
302
300
|
{#if leftCollapsed && hasSidebar && hasLeftPanel}
|
|
303
301
|
<CollapsedPanelStrip
|
|
304
302
|
side="left"
|
|
@@ -317,7 +315,6 @@
|
|
|
317
315
|
autoSaveId="selva-layout-{layoutKey}"
|
|
318
316
|
class="min-h-0 flex-1"
|
|
319
317
|
>
|
|
320
|
-
<!-- Left pane -->
|
|
321
318
|
{#if hasLeftPanel}
|
|
322
319
|
<Resizable.Pane
|
|
323
320
|
bind:this={leftPaneRef}
|
|
@@ -349,7 +346,6 @@
|
|
|
349
346
|
/>
|
|
350
347
|
{/if}
|
|
351
348
|
|
|
352
|
-
<!-- Viewer pane -->
|
|
353
349
|
{#if hasViewer}
|
|
354
350
|
<Resizable.Pane order={2} minSize={20} class="min-h-0 mx-1 flex flex-col">
|
|
355
351
|
<Viewer
|
|
@@ -365,7 +361,6 @@
|
|
|
365
361
|
</Resizable.Pane>
|
|
366
362
|
{/if}
|
|
367
363
|
|
|
368
|
-
<!-- Right pane -->
|
|
369
364
|
{#if hasRightPanel}
|
|
370
365
|
<Resizable.Handle
|
|
371
366
|
withHandle
|
|
@@ -398,7 +393,6 @@
|
|
|
398
393
|
{/if}
|
|
399
394
|
</Resizable.PaneGroup>
|
|
400
395
|
|
|
401
|
-
<!-- Right collapsed strip -->
|
|
402
396
|
{#if rightCollapsed && hasRightPanel}
|
|
403
397
|
<CollapsedPanelStrip
|
|
404
398
|
side="right"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
onSolve: SolveFn;
|
|
28
28
|
definitionKey?: string;
|
|
29
29
|
title?: string;
|
|
30
|
-
/**
|
|
30
|
+
/** Watermark shown in the viewer's bottom-right corner. */
|
|
31
31
|
logo?: string;
|
|
32
32
|
isEmbedded?: boolean;
|
|
33
33
|
primaryColor?: string;
|
|
@@ -41,55 +41,42 @@
|
|
|
41
41
|
onListStates?: () => ParameterPreset[] | Promise<ParameterPreset[]>;
|
|
42
42
|
/** Partial overrides for the preset-manager UI strings (e.g. for localization). */
|
|
43
43
|
presetLabels?: Partial<PresetLabels>;
|
|
44
|
-
/** Name
|
|
44
|
+
/** Name in the footer copyright line. Defaults to the brand name ("Selva"). */
|
|
45
45
|
copyrightName?: string;
|
|
46
46
|
/** Fully overrides the footer copyright line. `{name}` and `{year}` are substituted. */
|
|
47
47
|
footerText?: string;
|
|
48
48
|
/**
|
|
49
|
-
* How long one solve may take before the client aborts it (ms).
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* How long one solve may take before the client aborts it (ms). Pass the same value the
|
|
50
|
+
* server enforces (`COMPUTE_SOLVE_DEADLINE_MS`), or the client aborts solves that would
|
|
51
|
+
* have finished.
|
|
52
52
|
*/
|
|
53
53
|
solveDeadlineMs: number;
|
|
54
54
|
footerComponent?: any;
|
|
55
55
|
footerComponentProps?: () => Record<string, unknown>;
|
|
56
56
|
footerItemId?: string;
|
|
57
57
|
footerItemPriority?: number;
|
|
58
|
+
// `onReady` fires once, so the result and session are getters rather than snapshots.
|
|
58
59
|
onReady?: (api: {
|
|
59
60
|
loadValues: (values: Record<string, unknown>) => void;
|
|
60
|
-
/**
|
|
61
|
-
* The last result reported to the session — the one the viewer is showing, carrying
|
|
62
|
-
* `source`/`values` even when a memo hit served it. Null before the first solve.
|
|
63
|
-
* A getter, not a snapshot: `onReady` fires once.
|
|
64
|
-
*/
|
|
61
|
+
/** What the viewer is showing — carries `source`/`values` even on a memo hit. Null before the first solve. */
|
|
65
62
|
getLastResult: () => RetainedSolveResult | null;
|
|
66
63
|
/**
|
|
67
|
-
*
|
|
68
|
-
* `setValue`/`solve` to push inputs, `subscribe` to react to results.
|
|
69
|
-
*
|
|
70
|
-
* A getter for the same reason as `getLastResult`. Values written here go through the
|
|
64
|
+
* For hosts driving solves from their own state. Values written here go through the
|
|
71
65
|
* same throttle and memo as the UI's, so it's safe to call at interaction rate.
|
|
72
66
|
*/
|
|
73
67
|
getSession: () => SolveSession;
|
|
74
68
|
}) => void;
|
|
75
|
-
/**
|
|
76
|
-
* Hands the live three.js viewer to the host once it mounts, for apps drawing their own
|
|
77
|
-
* content alongside solve results. See `Viewer.svelte` for the contract.
|
|
78
|
-
*/
|
|
69
|
+
/** Hands the live three.js viewer to the host once it mounts. See `Viewer.svelte` for the contract. */
|
|
79
70
|
onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
|
|
80
71
|
headerRight?: Snippet;
|
|
81
72
|
// Replaces the built-in header; takes precedence over `headerRight`.
|
|
82
73
|
header?: Snippet;
|
|
83
74
|
// Scopes sessionStorage for external-input values; falls back to definitionKey then schema.id.
|
|
84
75
|
externalScopeKey?: string;
|
|
85
|
-
// Renders client-sourced inputs with presentation === 'slot'
|
|
76
|
+
// Renders client-sourced inputs with presentation === 'slot'.
|
|
86
77
|
clientSlot?: ClientSlot;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
* Provided once here and read by every descendant via locale context.
|
|
90
|
-
* Defaults to English when unset. Does not translate schema-authored labels
|
|
91
|
-
* or Grasshopper-sourced names/metadata.
|
|
92
|
-
*/
|
|
78
|
+
// Language for the app's own chrome, English when unset. Does not translate
|
|
79
|
+
// schema-authored labels or Grasshopper-sourced names/metadata.
|
|
93
80
|
lang?: Locale;
|
|
94
81
|
}
|
|
95
82
|
|
|
@@ -124,33 +111,30 @@
|
|
|
124
111
|
lang
|
|
125
112
|
}: Props = $props();
|
|
126
113
|
|
|
127
|
-
//
|
|
114
|
+
// Reaches InputControl deep in the tree.
|
|
128
115
|
// svelte-ignore state_referenced_locally
|
|
129
116
|
setClientSlot(clientSlot);
|
|
130
117
|
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
// getter is re-read reactively, so switching `lang` updates the chrome live.
|
|
118
|
+
// Resolution: explicit `lang` → host-provided locale → English. Passed as a getter so
|
|
119
|
+
// switching `lang` updates the chrome live.
|
|
134
120
|
const hostLocale = getLocaleContext();
|
|
135
121
|
setLocaleContext(() => lang ?? hostLocale.locale);
|
|
136
122
|
const t = $derived(getLocaleContext().messages);
|
|
137
123
|
|
|
138
124
|
const resolvedScopeKey = $derived(externalScopeKey || definitionKey || schema?.id || '');
|
|
139
125
|
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
// reads the reporter lazily so it can capture the session it's wired into.
|
|
126
|
+
// The session owns the value/lifecycle state machine; the driver is its transport. The
|
|
127
|
+
// session is passed as a getter because the two reference each other.
|
|
143
128
|
// svelte-ignore state_referenced_locally
|
|
144
129
|
const driver = createRequestResponseDriver(onSolve, () => session, {
|
|
145
130
|
solveDeadlineMs,
|
|
146
|
-
// The driver's
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
131
|
+
// The driver's memo caches whole solve results, meshes included, and the viewer disposes
|
|
132
|
+
// what it renders on the next scene update. `@selvajs/solve` keeps meshes opaque, so the
|
|
133
|
+
// three.js clone/dispose rules come from the renderer that owns them. Without this, a memo
|
|
134
|
+
// hit serves an already-disposed mesh.
|
|
150
135
|
meshPolicy,
|
|
151
|
-
// `
|
|
152
|
-
//
|
|
153
|
-
// callback, so it reads `session` after initialization rather than during it.
|
|
136
|
+
// `isSolving` lives on the driver, which the session can't observe — republish so the
|
|
137
|
+
// spinner and disabled states track it.
|
|
154
138
|
onChange: () => session.notify()
|
|
155
139
|
});
|
|
156
140
|
// svelte-ignore state_referenced_locally
|
|
@@ -201,10 +185,8 @@
|
|
|
201
185
|
session.solve();
|
|
202
186
|
}
|
|
203
187
|
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
// calls every render to keep the footer in sync. The composable no-ops when component
|
|
207
|
-
// is absent, so the hook itself stays unconditional.
|
|
188
|
+
// Registration is fixed at mount, so the static props are untracked. `footerComponentProps`
|
|
189
|
+
// stays live — the renderer calls it every render to keep the footer in sync.
|
|
208
190
|
useFooterItem({
|
|
209
191
|
id: untrack(() => footerItemId),
|
|
210
192
|
component: untrack(() => footerComponent),
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
onSaveState?: (state: ParameterPreset) => void | Promise<void>;
|
|
25
25
|
/** When set, the Load dialog lists these states instead of showing a file input. */
|
|
26
26
|
onListStates?: () => ParameterPreset[] | Promise<ParameterPreset[]>;
|
|
27
|
-
/**
|
|
27
|
+
/** Overrides merged over `DEFAULT_PRESET_LABELS`. */
|
|
28
28
|
labels?: Partial<PresetLabels>;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -42,21 +42,18 @@
|
|
|
42
42
|
|
|
43
43
|
const t = $derived({ ...DEFAULT_PRESET_LABELS, ...labels });
|
|
44
44
|
|
|
45
|
-
// Save dialog state
|
|
46
45
|
let showExportDialog = $state(false);
|
|
47
46
|
let exportName = $state('');
|
|
48
47
|
let exportDescription = $state('');
|
|
49
48
|
let exportAuthor = $state('');
|
|
50
49
|
let exportTags = $state('');
|
|
51
50
|
|
|
52
|
-
// Import/validation state
|
|
53
51
|
let showValidationDialog = $state(false);
|
|
54
52
|
let showLoadDialog = $state(false);
|
|
55
53
|
let loadResult = $state<PresetLoadResult | null>(null);
|
|
56
54
|
let loadingPresetName = $state('');
|
|
57
55
|
let fileInputRef = $state<HTMLInputElement | null>(null);
|
|
58
56
|
|
|
59
|
-
// Listed states (when onListStates is provided)
|
|
60
57
|
let listedStates = $state<ParameterPreset[]>([]);
|
|
61
58
|
let isLoadingList = $state(false);
|
|
62
59
|
let listError = $state('');
|
|
@@ -90,8 +87,8 @@
|
|
|
90
87
|
showExportDialog = false;
|
|
91
88
|
}
|
|
92
89
|
|
|
93
|
-
//
|
|
94
|
-
//
|
|
90
|
+
// Shared by every load path: applies clean presets directly, routes any errors or
|
|
91
|
+
// warnings through the validation dialog.
|
|
95
92
|
function tryLoad(preset: ParameterPreset) {
|
|
96
93
|
const result = loadPreset(preset, schema);
|
|
97
94
|
if (result.isValid) {
|
|
@@ -114,7 +111,7 @@
|
|
|
114
111
|
alert(t.loadImportError + (error as Error).message);
|
|
115
112
|
}
|
|
116
113
|
|
|
117
|
-
// Reset
|
|
114
|
+
// Reset so re-picking the same file fires `change` again.
|
|
118
115
|
input.value = '';
|
|
119
116
|
}
|
|
120
117
|
|
|
@@ -298,7 +295,6 @@
|
|
|
298
295
|
|
|
299
296
|
{#if loadResult}
|
|
300
297
|
<div class="gap-4 py-4 grid">
|
|
301
|
-
<!-- Summary Alert -->
|
|
302
298
|
{#if loadResult.isValid}
|
|
303
299
|
<Card.Root class="p-4 border-success/30 bg-success/5">
|
|
304
300
|
<div class="gap-3 flex items-start">
|
|
@@ -341,7 +337,6 @@
|
|
|
341
337
|
</Card.Root>
|
|
342
338
|
{/if}
|
|
343
339
|
|
|
344
|
-
<!-- Issues List -->
|
|
345
340
|
{#if !loadResult.isValid}
|
|
346
341
|
<div class="space-y-2">
|
|
347
342
|
<h4 class="text-sm font-medium">{t.validationIssuesHeading}</h4>
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import PageFooter from './PageFooter.svelte';
|
|
5
5
|
|
|
6
6
|
interface AppShellProps {
|
|
7
|
-
// Header
|
|
8
7
|
showHeader?: boolean;
|
|
9
8
|
homeUrl?: string;
|
|
10
9
|
title?: string | null;
|
|
@@ -17,21 +16,19 @@
|
|
|
17
16
|
subnav?: Snippet;
|
|
18
17
|
headerClass?: string;
|
|
19
18
|
/**
|
|
20
|
-
*
|
|
21
|
-
* header bar instead of the built-in PageHeader. The bar keeps the
|
|
19
|
+
* Replaces the built-in PageHeader inside the sticky header bar. The bar keeps the
|
|
22
20
|
* standard `--header-h` height so the fixed-mode layout math is unaffected.
|
|
23
21
|
*/
|
|
24
22
|
header?: Snippet;
|
|
25
23
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// 'scroll' —
|
|
24
|
+
// 'fixed' — viewport-locked, no page scroll; body owns its own scroll. For app-like
|
|
25
|
+
// pages (builder, preview, library/[guid]).
|
|
26
|
+
// 'scroll' — normal page flow, footer sticks to the bottom. For dashboard-style pages.
|
|
29
27
|
mode?: 'fixed' | 'scroll';
|
|
30
28
|
|
|
31
|
-
//
|
|
29
|
+
// When present, body becomes a flex row: sidenav | main.
|
|
32
30
|
sidenav?: Snippet;
|
|
33
31
|
|
|
34
|
-
// Footer (only rendered when `showFooter`)
|
|
35
32
|
showFooter?: boolean;
|
|
36
33
|
errors?: string[];
|
|
37
34
|
warnings?: string[];
|
|
@@ -39,7 +36,6 @@
|
|
|
39
36
|
footerText?: string;
|
|
40
37
|
footerChildren?: Snippet;
|
|
41
38
|
|
|
42
|
-
// Body content
|
|
43
39
|
class?: string;
|
|
44
40
|
bodyClass?: string;
|
|
45
41
|
children: Snippet;
|
|
@@ -70,12 +66,8 @@
|
|
|
70
66
|
children
|
|
71
67
|
}: AppShellProps = $props();
|
|
72
68
|
|
|
73
|
-
// Default the footer's copyright owner to the header's brand name when the
|
|
74
|
-
// caller didn't specify one.
|
|
75
69
|
const _copyright = $derived(copyrightName ?? brandName);
|
|
76
70
|
|
|
77
|
-
// fixed: viewport-locked, no page scroll — body owns its own scroll internally.
|
|
78
|
-
// scroll: normal page flow, footer sticks to bottom via sticky positioning.
|
|
79
71
|
const rootClass = $derived(
|
|
80
72
|
_mode === 'fixed'
|
|
81
73
|
? `flex flex-col h-screen overflow-hidden bg-background ${className}`
|
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
import type { Component, Snippet } from 'svelte';
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
|
-
//
|
|
5
|
+
// Renders at h-8 w-8 above the title.
|
|
6
6
|
icon?: Component<{ class?: string }>;
|
|
7
7
|
title: string;
|
|
8
8
|
description?: string;
|
|
9
9
|
// Vertical breathing room. Most pages want `lg`; tighter contexts (drawer, nested card) use `sm`.
|
|
10
10
|
size?: 'sm' | 'md' | 'lg';
|
|
11
11
|
class?: string;
|
|
12
|
-
//
|
|
12
|
+
// Overrides `description`. Use when the text needs rich content like <code>.
|
|
13
13
|
body?: Snippet;
|
|
14
|
-
// Buttons / links rendered below.
|
|
15
14
|
actions?: Snippet;
|
|
16
15
|
}
|
|
17
16
|
|
|
@@ -60,9 +60,7 @@
|
|
|
60
60
|
<footer
|
|
61
61
|
class="px-4 gap-4 text-xs h-7 flex shrink-0 items-center justify-between border-t border-border bg-background text-muted-foreground select-none"
|
|
62
62
|
>
|
|
63
|
-
<!-- Left section: Status items + custom context items -->
|
|
64
63
|
<div class="gap-4 flex items-center">
|
|
65
|
-
<!-- Custom left items from context -->
|
|
66
64
|
{#each leftItems as item (item.id)}
|
|
67
65
|
<FooterItemRenderer {item} />
|
|
68
66
|
{/each}
|
|
@@ -103,9 +101,7 @@
|
|
|
103
101
|
{/if}
|
|
104
102
|
</div>
|
|
105
103
|
|
|
106
|
-
<!-- Right section: Custom items + Copyright -->
|
|
107
104
|
<div class="gap-4 ml-auto flex items-center">
|
|
108
|
-
<!-- Custom right items from context -->
|
|
109
105
|
{#each rightItems as item (item.id)}
|
|
110
106
|
<FooterItemRenderer {item} />
|
|
111
107
|
{/each}
|
|
@@ -47,11 +47,8 @@
|
|
|
47
47
|
// One visibility evaluation per item per render; columnStarts and gridItem both read it.
|
|
48
48
|
const visibilityMap = $derived(buildVisibilityMap(items, values));
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
* for spans and linebreak resets. Items hidden by visibility don't consume
|
|
53
|
-
* a slot (matching what the renderer does — they're skipped entirely).
|
|
54
|
-
*/
|
|
50
|
+
// Hidden items must not consume a slot here, because gridItem skips them entirely —
|
|
51
|
+
// counting them would offset every following item's column.
|
|
55
52
|
const columnStarts = $derived.by(() => {
|
|
56
53
|
const positions: number[] = [];
|
|
57
54
|
let col = 0;
|
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
const ext = $derived(file ? (file.fileType ?? '').toLowerCase() : '');
|
|
22
22
|
const isSupported = $derived(SUPPORTED_EXTS.has(ext));
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// image-context SVGs
|
|
24
|
+
// Binary formats (PNG/JPG/WEBP/GIF) arrive base64; SVG arrives as plain UTF-8 XML
|
|
25
|
+
// and must be percent-encoded into the data URL.
|
|
26
|
+
// Rendering through <img src> rather than inlining the SVG is a security choice:
|
|
27
|
+
// browsers disable scripting in image-context SVGs, so untrusted SVG cannot
|
|
28
|
+
// execute against the host. Don't switch to {@html} or an inline <svg>.
|
|
28
29
|
const dataUrl = $derived.by(() => {
|
|
29
30
|
if (!file || !isSupported) return null;
|
|
30
31
|
const mime = MIME_BY_EXT[ext] ?? 'application/octet-stream';
|