selva-shared 0.8.2 → 0.8.4
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 +2 -0
- package/dist/components/AppLayout.svelte +9 -2
- package/dist/components/AppLayout.svelte.d.ts +4 -0
- package/dist/components/ComputeApp.svelte +239 -0
- package/dist/components/ComputeApp.svelte.d.ts +28 -0
- package/dist/components/StateManager.svelte +40 -10
- package/dist/components/StateManager.svelte.d.ts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/types/actionButton.d.ts +8 -0
- package/dist/types/actionButton.js +1 -0
- package/dist/types/solveFn.d.ts +7 -0
- package/dist/types/solveFn.js +1 -0
- package/dist/utils/color.d.ts +1 -0
- package/dist/utils/color.js +38 -0
- package/package.json +3 -3
- package/src/lib/components/AppLayout.svelte +9 -2
- package/src/lib/components/ComputeApp.svelte +239 -0
- package/src/lib/components/StateManager.svelte +40 -10
- package/src/lib/index.ts +4 -0
- package/src/lib/types/actionButton.ts +8 -0
- package/src/lib/types/solveFn.ts +11 -0
- package/src/lib/utils/color.ts +45 -0
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { UISchema, SupportedTypes } from '../types/generated';
|
|
3
|
+
import type { ActionButton } from '../types/actionButton';
|
|
3
4
|
import { ChevronUp } from '@lucide/svelte';
|
|
4
5
|
import Viewer from './Viewer.svelte';
|
|
5
6
|
import CalculateButton from './ui/CalculateButton.svelte';
|
|
@@ -25,6 +26,9 @@
|
|
|
25
26
|
values: Record<string, unknown>;
|
|
26
27
|
onValueChange: (id: string, val: SupportedTypes) => void | Promise<void>;
|
|
27
28
|
onLoadValues?: () => void | Promise<void>;
|
|
29
|
+
stateManagerActions?: ActionButton[];
|
|
30
|
+
showSaveButton?: boolean;
|
|
31
|
+
showLoadButton?: boolean;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
let {
|
|
@@ -38,7 +42,10 @@
|
|
|
38
42
|
oncalculate = () => {},
|
|
39
43
|
values = $bindable({}),
|
|
40
44
|
onValueChange,
|
|
41
|
-
onLoadValues
|
|
45
|
+
onLoadValues,
|
|
46
|
+
stateManagerActions = [],
|
|
47
|
+
showSaveButton = true,
|
|
48
|
+
showLoadButton = true
|
|
42
49
|
}: Props = $props();
|
|
43
50
|
|
|
44
51
|
// ── Layout flags ─────────────────────────────────────────────────────────────
|
|
@@ -121,7 +128,7 @@
|
|
|
121
128
|
{#if showStateManager || (!isMobile && showCalculateButton && schema.instanceSolve === false)}
|
|
122
129
|
<div class="panel-footer px-3">
|
|
123
130
|
{#if showStateManager}
|
|
124
|
-
<StateManager {schema} currentValues={values} onLoadValues={handleLoadValues} />
|
|
131
|
+
<StateManager {schema} currentValues={values} onLoadValues={handleLoadValues} actions={stateManagerActions} {showSaveButton} {showLoadButton} />
|
|
125
132
|
{/if}
|
|
126
133
|
{#if !isMobile && showCalculateButton && schema.instanceSolve === false}
|
|
127
134
|
<CalculateButton {hasPendingChanges} {hasNeverSolved} {isSolving} {oncalculate} />
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UISchema, SupportedTypes } from '../types/generated';
|
|
2
|
+
import type { ActionButton } from '../types/actionButton';
|
|
2
3
|
interface Props {
|
|
3
4
|
schema: UISchema;
|
|
4
5
|
meshes?: any[];
|
|
@@ -11,6 +12,9 @@ interface Props {
|
|
|
11
12
|
values: Record<string, unknown>;
|
|
12
13
|
onValueChange: (id: string, val: SupportedTypes) => void | Promise<void>;
|
|
13
14
|
onLoadValues?: () => void | Promise<void>;
|
|
15
|
+
stateManagerActions?: ActionButton[];
|
|
16
|
+
showSaveButton?: boolean;
|
|
17
|
+
showLoadButton?: boolean;
|
|
14
18
|
}
|
|
15
19
|
declare const AppLayout: import("svelte").Component<Props, {}, "values" | "isViewerFullscreen">;
|
|
16
20
|
type AppLayout = ReturnType<typeof AppLayout>;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { page } from '$app/state';
|
|
4
|
+
import type { UISchema } from '../types/generated';
|
|
5
|
+
import type { ActionButton } from '../types/actionButton';
|
|
6
|
+
import type { SolveFn } from '../types/solveFn';
|
|
7
|
+
import { getDefaultValue } from '../utils/utils-shared';
|
|
8
|
+
import { createComputeThrottle } from '../utils/computeThrottle.svelte';
|
|
9
|
+
import { createSolvingIndicator } from '../utils/solving.svelte';
|
|
10
|
+
import { useFooterItem } from '../composables/useFooterItem.svelte';
|
|
11
|
+
import { hexToOklch } from '../utils/color';
|
|
12
|
+
import PageContainer from './layout/PageContainer.svelte';
|
|
13
|
+
import PageHeader from './layout/PageHeader.svelte';
|
|
14
|
+
import AppLayout from './AppLayout.svelte';
|
|
15
|
+
import StateDisplay from './ui/StateDisplay.svelte';
|
|
16
|
+
|
|
17
|
+
// ── Props ────────────────────────────────────────────────────────────────────
|
|
18
|
+
import type { Snippet } from 'svelte';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
schema: UISchema;
|
|
22
|
+
onSolve: SolveFn;
|
|
23
|
+
definitionKey?: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
isEmbedded?: boolean;
|
|
26
|
+
primaryColor?: string;
|
|
27
|
+
showModeToggle?: boolean;
|
|
28
|
+
stateManagerActions?: ActionButton[];
|
|
29
|
+
showSaveButton?: boolean;
|
|
30
|
+
showLoadButton?: boolean;
|
|
31
|
+
footerComponent?: any;
|
|
32
|
+
footerComponentProps?: () => Record<string, unknown>;
|
|
33
|
+
footerItemId?: string;
|
|
34
|
+
footerItemPriority?: number;
|
|
35
|
+
// Snippets for custom layout
|
|
36
|
+
header?: Snippet;
|
|
37
|
+
children?: Snippet<[{ errors: string[]; warnings: string[] }]>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let {
|
|
41
|
+
schema,
|
|
42
|
+
onSolve,
|
|
43
|
+
definitionKey = '',
|
|
44
|
+
title,
|
|
45
|
+
isEmbedded,
|
|
46
|
+
primaryColor,
|
|
47
|
+
showModeToggle = false,
|
|
48
|
+
stateManagerActions = [],
|
|
49
|
+
showSaveButton = true,
|
|
50
|
+
showLoadButton = true,
|
|
51
|
+
footerComponent,
|
|
52
|
+
footerComponentProps,
|
|
53
|
+
footerItemId = 'footer-item',
|
|
54
|
+
footerItemPriority = 0,
|
|
55
|
+
header,
|
|
56
|
+
children
|
|
57
|
+
}: Props = $props();
|
|
58
|
+
|
|
59
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
60
|
+
function createInitialValues(s: UISchema) {
|
|
61
|
+
const v: Record<string, unknown> = {};
|
|
62
|
+
for (const input of s.inputs) {
|
|
63
|
+
v[input.id] = input.default ?? getDefaultValue(input.paramType);
|
|
64
|
+
}
|
|
65
|
+
for (const output of s.outputs) {
|
|
66
|
+
v[output.id] = null;
|
|
67
|
+
}
|
|
68
|
+
return v;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Core state ───────────────────────────────────────────────────────────────
|
|
72
|
+
// svelte-ignore state_referenced_locally
|
|
73
|
+
let values = $state<Record<string, unknown>>(createInitialValues(schema));
|
|
74
|
+
let error = $state('');
|
|
75
|
+
let computeErrors = $state<string[]>([]);
|
|
76
|
+
let computeWarnings = $state<string[]>([]);
|
|
77
|
+
let meshes = $state<any[]>([]);
|
|
78
|
+
let pendingValues = $state<Record<string, unknown>>({});
|
|
79
|
+
// svelte-ignore state_referenced_locally
|
|
80
|
+
let hasPendingChanges = $state(schema?.instanceSolve === false);
|
|
81
|
+
// svelte-ignore state_referenced_locally
|
|
82
|
+
let hasNeverSolved = $state(schema?.instanceSolve === false);
|
|
83
|
+
let isViewerFullscreen = $state(false);
|
|
84
|
+
|
|
85
|
+
// ── Solve logic ──────────────────────────────────────────────────────────────
|
|
86
|
+
async function performSolveInternal(solveValues: Record<string, unknown>, signal: AbortSignal) {
|
|
87
|
+
try {
|
|
88
|
+
error = '';
|
|
89
|
+
computeErrors = [];
|
|
90
|
+
computeWarnings = [];
|
|
91
|
+
|
|
92
|
+
const result = await onSolve(solveValues, signal);
|
|
93
|
+
|
|
94
|
+
if (signal.aborted) return;
|
|
95
|
+
|
|
96
|
+
computeErrors = result.errors ?? [];
|
|
97
|
+
computeWarnings = result.warnings ?? [];
|
|
98
|
+
meshes = result.meshes ?? [];
|
|
99
|
+
|
|
100
|
+
Object.assign(values, result.outputs);
|
|
101
|
+
pendingValues = {};
|
|
102
|
+
hasPendingChanges = false;
|
|
103
|
+
hasNeverSolved = false;
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (err instanceof Error && err.name === 'AbortError') return;
|
|
106
|
+
error = err instanceof Error ? err.message : String(err);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const computeThrottle = createComputeThrottle<Record<string, unknown>>(performSolveInternal, {
|
|
111
|
+
timeout: 60000
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
let solving = $derived(computeThrottle.isComputing);
|
|
115
|
+
const solvingIndicator = createSolvingIndicator(() => solving);
|
|
116
|
+
|
|
117
|
+
function performSolve() {
|
|
118
|
+
computeThrottle.trigger($state.snapshot(values));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Definition switching ─────────────────────────────────────────────────────
|
|
122
|
+
let previousDefinitionKey = $state('');
|
|
123
|
+
let isInitialLoad = $state(true);
|
|
124
|
+
|
|
125
|
+
$effect(() => {
|
|
126
|
+
const _ = definitionKey;
|
|
127
|
+
|
|
128
|
+
untrack(() => {
|
|
129
|
+
if (isInitialLoad) {
|
|
130
|
+
isInitialLoad = false;
|
|
131
|
+
previousDefinitionKey = definitionKey;
|
|
132
|
+
if (schema?.instanceSolve !== false) {
|
|
133
|
+
performSolve();
|
|
134
|
+
}
|
|
135
|
+
} else if (previousDefinitionKey !== definitionKey) {
|
|
136
|
+
meshes = [];
|
|
137
|
+
values = createInitialValues(schema);
|
|
138
|
+
error = '';
|
|
139
|
+
computeErrors = [];
|
|
140
|
+
computeWarnings = [];
|
|
141
|
+
if (schema && Object.keys(values).length > 0) {
|
|
142
|
+
performSolve();
|
|
143
|
+
}
|
|
144
|
+
previousDefinitionKey = definitionKey;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ── Handlers ─────────────────────────────────────────────────────────────────
|
|
150
|
+
async function handleValueChange(id: string, val: unknown) {
|
|
151
|
+
values[id] = val;
|
|
152
|
+
|
|
153
|
+
if (schema?.instanceSolve === false) {
|
|
154
|
+
pendingValues[id] = val;
|
|
155
|
+
hasPendingChanges = true;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
performSolve();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function handleCalculate() {
|
|
163
|
+
performSolve();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Footer item ──────────────────────────────────────────────────────────────
|
|
167
|
+
// Use untrack to read these static props without creating reactive dependencies.
|
|
168
|
+
// footerComponentProps is intentionally NOT untracked — it's a getter called every render.
|
|
169
|
+
const _footerItemId = untrack(() => footerItemId);
|
|
170
|
+
const _footerComponent = untrack(() => footerComponent);
|
|
171
|
+
const _footerItemPriority = untrack(() => footerItemPriority);
|
|
172
|
+
useFooterItem(
|
|
173
|
+
_footerItemId,
|
|
174
|
+
_footerComponent,
|
|
175
|
+
() => (_footerComponent ? (footerComponentProps?.() ?? {}) : {}),
|
|
176
|
+
'left',
|
|
177
|
+
_footerItemPriority
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// ── Embed + custom style ─────────────────────────────────────────────────────
|
|
181
|
+
let resolvedIsEmbedded = $derived(isEmbedded ?? page.url.searchParams.get('embed') === 'true');
|
|
182
|
+
let resolvedPrimaryColor = $derived(primaryColor ?? page.url.searchParams.get('primary'));
|
|
183
|
+
let customStyle = $derived(
|
|
184
|
+
resolvedPrimaryColor ? `--primary: ${hexToOklch(resolvedPrimaryColor)}` : ''
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
let pageTitle = $derived(title ?? (schema?.description || schema.name));
|
|
188
|
+
</script>
|
|
189
|
+
|
|
190
|
+
<div style={customStyle} style:display="contents">
|
|
191
|
+
{#if children}
|
|
192
|
+
{@render children({ errors: computeErrors, warnings: computeWarnings })}
|
|
193
|
+
{:else}
|
|
194
|
+
<PageContainer errors={computeErrors} warnings={computeWarnings}>
|
|
195
|
+
{#if header}
|
|
196
|
+
{@render header()}
|
|
197
|
+
{:else if !resolvedIsEmbedded}
|
|
198
|
+
<PageHeader title={pageTitle} {showModeToggle} />
|
|
199
|
+
{/if}
|
|
200
|
+
|
|
201
|
+
<div class="bg-background flex flex-1 flex-col overflow-hidden">
|
|
202
|
+
{#if error}
|
|
203
|
+
<div class="flex min-h-100 items-center justify-center p-8">
|
|
204
|
+
<StateDisplay type="error" size="medium" message={error} />
|
|
205
|
+
</div>
|
|
206
|
+
{:else if !schema}
|
|
207
|
+
<div class="flex min-h-100 items-center justify-center">
|
|
208
|
+
<StateDisplay type="loading" size="large" message="Loading schema..." />
|
|
209
|
+
</div>
|
|
210
|
+
{:else}
|
|
211
|
+
{#key definitionKey}
|
|
212
|
+
<AppLayout
|
|
213
|
+
{schema}
|
|
214
|
+
{meshes}
|
|
215
|
+
isSolving={solving}
|
|
216
|
+
showSolvingIndicator={schema.instanceSolve !== false && solvingIndicator.show}
|
|
217
|
+
{hasPendingChanges}
|
|
218
|
+
{hasNeverSolved}
|
|
219
|
+
bind:isViewerFullscreen
|
|
220
|
+
bind:values
|
|
221
|
+
{stateManagerActions}
|
|
222
|
+
{showSaveButton}
|
|
223
|
+
{showLoadButton}
|
|
224
|
+
onValueChange={handleValueChange}
|
|
225
|
+
oncalculate={handleCalculate}
|
|
226
|
+
onLoadValues={() => {
|
|
227
|
+
if (schema?.instanceSolve !== false) {
|
|
228
|
+
performSolve();
|
|
229
|
+
} else {
|
|
230
|
+
hasPendingChanges = true;
|
|
231
|
+
}
|
|
232
|
+
}}
|
|
233
|
+
/>
|
|
234
|
+
{/key}
|
|
235
|
+
{/if}
|
|
236
|
+
</div>
|
|
237
|
+
</PageContainer>
|
|
238
|
+
{/if}
|
|
239
|
+
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { UISchema } from '../types/generated';
|
|
2
|
+
import type { ActionButton } from '../types/actionButton';
|
|
3
|
+
import type { SolveFn } from '../types/solveFn';
|
|
4
|
+
import type { Snippet } from 'svelte';
|
|
5
|
+
interface Props {
|
|
6
|
+
schema: UISchema;
|
|
7
|
+
onSolve: SolveFn;
|
|
8
|
+
definitionKey?: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
isEmbedded?: boolean;
|
|
11
|
+
primaryColor?: string;
|
|
12
|
+
showModeToggle?: boolean;
|
|
13
|
+
stateManagerActions?: ActionButton[];
|
|
14
|
+
showSaveButton?: boolean;
|
|
15
|
+
showLoadButton?: boolean;
|
|
16
|
+
footerComponent?: any;
|
|
17
|
+
footerComponentProps?: () => Record<string, unknown>;
|
|
18
|
+
footerItemId?: string;
|
|
19
|
+
footerItemPriority?: number;
|
|
20
|
+
header?: Snippet;
|
|
21
|
+
children?: Snippet<[{
|
|
22
|
+
errors: string[];
|
|
23
|
+
warnings: string[];
|
|
24
|
+
}]>;
|
|
25
|
+
}
|
|
26
|
+
declare const ComputeApp: import("svelte").Component<Props, {}, "">;
|
|
27
|
+
type ComputeApp = ReturnType<typeof ComputeApp>;
|
|
28
|
+
export default ComputeApp;
|
|
@@ -10,13 +10,25 @@
|
|
|
10
10
|
} from '../utils/param-exporter';
|
|
11
11
|
import { Button, Input, Label, Textarea, Dialog, Card } from '../components/ui';
|
|
12
12
|
|
|
13
|
+
import type { ActionButton } from '../types/actionButton';
|
|
14
|
+
|
|
13
15
|
interface Props {
|
|
14
16
|
schema: UISchema;
|
|
15
17
|
currentValues: Record<string, unknown>;
|
|
16
18
|
onLoadValues: (values: Record<string, unknown>) => void;
|
|
19
|
+
showSaveButton?: boolean;
|
|
20
|
+
showLoadButton?: boolean;
|
|
21
|
+
actions?: ActionButton[];
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
let {
|
|
24
|
+
let {
|
|
25
|
+
schema,
|
|
26
|
+
currentValues,
|
|
27
|
+
onLoadValues,
|
|
28
|
+
showSaveButton = true,
|
|
29
|
+
showLoadButton = true,
|
|
30
|
+
actions = []
|
|
31
|
+
}: Props = $props();
|
|
20
32
|
|
|
21
33
|
// Save dialog state
|
|
22
34
|
let showExportDialog = $state(false);
|
|
@@ -127,15 +139,33 @@
|
|
|
127
139
|
</script>
|
|
128
140
|
|
|
129
141
|
<div class="gap-2 mb-2 flex items-center justify-center">
|
|
130
|
-
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
{#if showSaveButton}
|
|
143
|
+
<Button variant="default" size="sm" onclick={openExportDialog}>
|
|
144
|
+
<Download class="mr-2 h-4 w-4" />
|
|
145
|
+
Save State
|
|
146
|
+
</Button>
|
|
147
|
+
{/if}
|
|
148
|
+
|
|
149
|
+
{#if showLoadButton}
|
|
150
|
+
<Button variant="outline" size="sm" onclick={openLoadDialog}>
|
|
151
|
+
<Upload class="mr-2 h-4 w-4" />
|
|
152
|
+
Load State
|
|
153
|
+
</Button>
|
|
154
|
+
{/if}
|
|
155
|
+
|
|
156
|
+
{#each actions as action (action.id)}
|
|
157
|
+
<Button
|
|
158
|
+
variant={action.variant ?? 'outline'}
|
|
159
|
+
size={action.size ?? 'sm'}
|
|
160
|
+
onclick={action.onclick}
|
|
161
|
+
>
|
|
162
|
+
{#if action.icon}
|
|
163
|
+
{@const IconComponent = action.icon}
|
|
164
|
+
<IconComponent class="mr-2 h-4 w-4" />
|
|
165
|
+
{/if}
|
|
166
|
+
{action.label}
|
|
167
|
+
</Button>
|
|
168
|
+
{/each}
|
|
139
169
|
|
|
140
170
|
<input
|
|
141
171
|
bind:this={fileInputRef}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { UISchema } from '../types/generated';
|
|
2
|
+
import type { ActionButton } from '../types/actionButton';
|
|
2
3
|
interface Props {
|
|
3
4
|
schema: UISchema;
|
|
4
5
|
currentValues: Record<string, unknown>;
|
|
5
6
|
onLoadValues: (values: Record<string, unknown>) => void;
|
|
7
|
+
showSaveButton?: boolean;
|
|
8
|
+
showLoadButton?: boolean;
|
|
9
|
+
actions?: ActionButton[];
|
|
6
10
|
}
|
|
7
11
|
declare const StateManager: import("svelte").Component<Props, {}, "">;
|
|
8
12
|
type StateManager = ReturnType<typeof StateManager>;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { default as PageContainer } from './components/layout/PageContainer.svel
|
|
|
2
2
|
export { default as PageHeader } from './components/layout/PageHeader.svelte';
|
|
3
3
|
export { default as PageFooter } from './components/layout/PageFooter.svelte';
|
|
4
4
|
export { default as AppLayout } from './components/AppLayout.svelte';
|
|
5
|
+
export { default as ComputeApp } from './components/ComputeApp.svelte';
|
|
5
6
|
export { default as ErrorScreen } from './components/ErrorScreen.svelte';
|
|
6
7
|
export { default as TabLayout } from './components/preview/TabLayout.svelte';
|
|
7
8
|
export { default as InputControl } from './components/preview/InputControl.svelte';
|
|
@@ -14,6 +15,7 @@ export { default as SolvingIndicator } from './components/ui/SolvingIndicator.sv
|
|
|
14
15
|
export { default as ComputeMessages } from './components/ComputeMessages.svelte';
|
|
15
16
|
export * from './features/preview/handlers';
|
|
16
17
|
export * from './features/preview/notifications';
|
|
18
|
+
export * from './utils/color';
|
|
17
19
|
export * from './utils/debounce';
|
|
18
20
|
export * from './utils/utils-shared';
|
|
19
21
|
export * from './utils/file-download';
|
|
@@ -25,5 +27,7 @@ export * from './composables/useFooterItem.svelte';
|
|
|
25
27
|
export { themeStore } from './stores/themeStore.svelte';
|
|
26
28
|
export * from './utils';
|
|
27
29
|
export type * from './types/generated';
|
|
30
|
+
export type { ActionButton } from './types/actionButton';
|
|
31
|
+
export type { SolveFn, SolveResult } from './types/solveFn';
|
|
28
32
|
export { ACCEPTED_FILE_FORMATS } from './types/generated/schema';
|
|
29
33
|
export * from './themes';
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { default as PageContainer } from './components/layout/PageContainer.svel
|
|
|
3
3
|
export { default as PageHeader } from './components/layout/PageHeader.svelte';
|
|
4
4
|
export { default as PageFooter } from './components/layout/PageFooter.svelte';
|
|
5
5
|
export { default as AppLayout } from './components/AppLayout.svelte';
|
|
6
|
+
export { default as ComputeApp } from './components/ComputeApp.svelte';
|
|
6
7
|
// Error components
|
|
7
8
|
export { default as ErrorScreen } from './components/ErrorScreen.svelte';
|
|
8
9
|
// Preview components
|
|
@@ -20,6 +21,7 @@ export { default as ComputeMessages } from './components/ComputeMessages.svelte'
|
|
|
20
21
|
export * from './features/preview/handlers';
|
|
21
22
|
export * from './features/preview/notifications';
|
|
22
23
|
// Utilities
|
|
24
|
+
export * from './utils/color';
|
|
23
25
|
export * from './utils/debounce';
|
|
24
26
|
export * from './utils/utils-shared';
|
|
25
27
|
export * from './utils/file-download';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const hexToOklch: (hex: string | null) => string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Helper to convert hex to OKLCH
|
|
2
|
+
export const hexToOklch = (hex) => {
|
|
3
|
+
if (!hex)
|
|
4
|
+
return '';
|
|
5
|
+
// Normalize hex (remove # if present)
|
|
6
|
+
const normalized = hex.replace(/^#/, '');
|
|
7
|
+
// If it's not a valid hex color, return as-is
|
|
8
|
+
if (!/^[0-9A-Fa-f]{6}$/.test(normalized)) {
|
|
9
|
+
return hex;
|
|
10
|
+
}
|
|
11
|
+
// Parse hex to RGB
|
|
12
|
+
const r = parseInt(normalized.slice(0, 2), 16) / 255;
|
|
13
|
+
const g = parseInt(normalized.slice(2, 4), 16) / 255;
|
|
14
|
+
const b = parseInt(normalized.slice(4, 6), 16) / 255;
|
|
15
|
+
// Convert RGB to linear RGB
|
|
16
|
+
const toLinear = (c) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
|
|
17
|
+
const lr = toLinear(r);
|
|
18
|
+
const lg = toLinear(g);
|
|
19
|
+
const lb = toLinear(b);
|
|
20
|
+
// Convert linear RGB to XYZ
|
|
21
|
+
const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
|
|
22
|
+
const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
|
|
23
|
+
const z = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
|
|
24
|
+
// Convert XYZ to OKLab
|
|
25
|
+
const l_ = Math.cbrt(0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z);
|
|
26
|
+
const m_ = Math.cbrt(0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z);
|
|
27
|
+
const s_ = Math.cbrt(0.0482003018 * x + 0.2643662691 * y + 0.633851707 * z);
|
|
28
|
+
const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
|
|
29
|
+
const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
|
|
30
|
+
const b_lab = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;
|
|
31
|
+
// Convert to LCH
|
|
32
|
+
const C = Math.sqrt(a * a + b_lab * b_lab);
|
|
33
|
+
let H = (Math.atan2(b_lab, a) * 180) / Math.PI;
|
|
34
|
+
if (H < 0)
|
|
35
|
+
H += 360;
|
|
36
|
+
// Format as OKLCH (round to reasonable precision)
|
|
37
|
+
return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`;
|
|
38
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "selva-shared",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "Shared UI components and utilities for Selva applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -44,19 +44,19 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@floating-ui/dom": "^1.6.13",
|
|
46
46
|
"@iconify/svelte": "^5.2.1",
|
|
47
|
+
"@lucide/svelte": "^0.577.0",
|
|
47
48
|
"clsx": "^2.1.1",
|
|
48
49
|
"mode-watcher": "^1.1.0",
|
|
50
|
+
"paneforge": "^1.0.2",
|
|
49
51
|
"svelte-sonner": "^1.0.7",
|
|
50
52
|
"tailwind-merge": "^3.4.0",
|
|
51
53
|
"tw-animate-css": "^1.4.0"
|
|
52
54
|
},
|
|
53
55
|
"devDependencies": {
|
|
54
56
|
"@internationalized/date": "^3.10.1",
|
|
55
|
-
"@lucide/svelte": "^0.577.0",
|
|
56
57
|
"@sveltejs/kit": "2.53.4",
|
|
57
58
|
"@types/three": "^0.183.1",
|
|
58
59
|
"bits-ui": "^2.15.4",
|
|
59
|
-
"paneforge": "^1.0.2",
|
|
60
60
|
"svelte": "5.53.7",
|
|
61
61
|
"tailwind-variants": "^3.2.2",
|
|
62
62
|
"@selva/config": "0.0.0"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { UISchema, SupportedTypes } from '../types/generated';
|
|
3
|
+
import type { ActionButton } from '../types/actionButton';
|
|
3
4
|
import { ChevronUp } from '@lucide/svelte';
|
|
4
5
|
import Viewer from './Viewer.svelte';
|
|
5
6
|
import CalculateButton from './ui/CalculateButton.svelte';
|
|
@@ -25,6 +26,9 @@
|
|
|
25
26
|
values: Record<string, unknown>;
|
|
26
27
|
onValueChange: (id: string, val: SupportedTypes) => void | Promise<void>;
|
|
27
28
|
onLoadValues?: () => void | Promise<void>;
|
|
29
|
+
stateManagerActions?: ActionButton[];
|
|
30
|
+
showSaveButton?: boolean;
|
|
31
|
+
showLoadButton?: boolean;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
let {
|
|
@@ -38,7 +42,10 @@
|
|
|
38
42
|
oncalculate = () => {},
|
|
39
43
|
values = $bindable({}),
|
|
40
44
|
onValueChange,
|
|
41
|
-
onLoadValues
|
|
45
|
+
onLoadValues,
|
|
46
|
+
stateManagerActions = [],
|
|
47
|
+
showSaveButton = true,
|
|
48
|
+
showLoadButton = true
|
|
42
49
|
}: Props = $props();
|
|
43
50
|
|
|
44
51
|
// ── Layout flags ─────────────────────────────────────────────────────────────
|
|
@@ -121,7 +128,7 @@
|
|
|
121
128
|
{#if showStateManager || (!isMobile && showCalculateButton && schema.instanceSolve === false)}
|
|
122
129
|
<div class="panel-footer px-3">
|
|
123
130
|
{#if showStateManager}
|
|
124
|
-
<StateManager {schema} currentValues={values} onLoadValues={handleLoadValues} />
|
|
131
|
+
<StateManager {schema} currentValues={values} onLoadValues={handleLoadValues} actions={stateManagerActions} {showSaveButton} {showLoadButton} />
|
|
125
132
|
{/if}
|
|
126
133
|
{#if !isMobile && showCalculateButton && schema.instanceSolve === false}
|
|
127
134
|
<CalculateButton {hasPendingChanges} {hasNeverSolved} {isSolving} {oncalculate} />
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { page } from '$app/state';
|
|
4
|
+
import type { UISchema } from '../types/generated';
|
|
5
|
+
import type { ActionButton } from '../types/actionButton';
|
|
6
|
+
import type { SolveFn } from '../types/solveFn';
|
|
7
|
+
import { getDefaultValue } from '../utils/utils-shared';
|
|
8
|
+
import { createComputeThrottle } from '../utils/computeThrottle.svelte';
|
|
9
|
+
import { createSolvingIndicator } from '../utils/solving.svelte';
|
|
10
|
+
import { useFooterItem } from '../composables/useFooterItem.svelte';
|
|
11
|
+
import { hexToOklch } from '../utils/color';
|
|
12
|
+
import PageContainer from './layout/PageContainer.svelte';
|
|
13
|
+
import PageHeader from './layout/PageHeader.svelte';
|
|
14
|
+
import AppLayout from './AppLayout.svelte';
|
|
15
|
+
import StateDisplay from './ui/StateDisplay.svelte';
|
|
16
|
+
|
|
17
|
+
// ── Props ────────────────────────────────────────────────────────────────────
|
|
18
|
+
import type { Snippet } from 'svelte';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
schema: UISchema;
|
|
22
|
+
onSolve: SolveFn;
|
|
23
|
+
definitionKey?: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
isEmbedded?: boolean;
|
|
26
|
+
primaryColor?: string;
|
|
27
|
+
showModeToggle?: boolean;
|
|
28
|
+
stateManagerActions?: ActionButton[];
|
|
29
|
+
showSaveButton?: boolean;
|
|
30
|
+
showLoadButton?: boolean;
|
|
31
|
+
footerComponent?: any;
|
|
32
|
+
footerComponentProps?: () => Record<string, unknown>;
|
|
33
|
+
footerItemId?: string;
|
|
34
|
+
footerItemPriority?: number;
|
|
35
|
+
// Snippets for custom layout
|
|
36
|
+
header?: Snippet;
|
|
37
|
+
children?: Snippet<[{ errors: string[]; warnings: string[] }]>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let {
|
|
41
|
+
schema,
|
|
42
|
+
onSolve,
|
|
43
|
+
definitionKey = '',
|
|
44
|
+
title,
|
|
45
|
+
isEmbedded,
|
|
46
|
+
primaryColor,
|
|
47
|
+
showModeToggle = false,
|
|
48
|
+
stateManagerActions = [],
|
|
49
|
+
showSaveButton = true,
|
|
50
|
+
showLoadButton = true,
|
|
51
|
+
footerComponent,
|
|
52
|
+
footerComponentProps,
|
|
53
|
+
footerItemId = 'footer-item',
|
|
54
|
+
footerItemPriority = 0,
|
|
55
|
+
header,
|
|
56
|
+
children
|
|
57
|
+
}: Props = $props();
|
|
58
|
+
|
|
59
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
60
|
+
function createInitialValues(s: UISchema) {
|
|
61
|
+
const v: Record<string, unknown> = {};
|
|
62
|
+
for (const input of s.inputs) {
|
|
63
|
+
v[input.id] = input.default ?? getDefaultValue(input.paramType);
|
|
64
|
+
}
|
|
65
|
+
for (const output of s.outputs) {
|
|
66
|
+
v[output.id] = null;
|
|
67
|
+
}
|
|
68
|
+
return v;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Core state ───────────────────────────────────────────────────────────────
|
|
72
|
+
// svelte-ignore state_referenced_locally
|
|
73
|
+
let values = $state<Record<string, unknown>>(createInitialValues(schema));
|
|
74
|
+
let error = $state('');
|
|
75
|
+
let computeErrors = $state<string[]>([]);
|
|
76
|
+
let computeWarnings = $state<string[]>([]);
|
|
77
|
+
let meshes = $state<any[]>([]);
|
|
78
|
+
let pendingValues = $state<Record<string, unknown>>({});
|
|
79
|
+
// svelte-ignore state_referenced_locally
|
|
80
|
+
let hasPendingChanges = $state(schema?.instanceSolve === false);
|
|
81
|
+
// svelte-ignore state_referenced_locally
|
|
82
|
+
let hasNeverSolved = $state(schema?.instanceSolve === false);
|
|
83
|
+
let isViewerFullscreen = $state(false);
|
|
84
|
+
|
|
85
|
+
// ── Solve logic ──────────────────────────────────────────────────────────────
|
|
86
|
+
async function performSolveInternal(solveValues: Record<string, unknown>, signal: AbortSignal) {
|
|
87
|
+
try {
|
|
88
|
+
error = '';
|
|
89
|
+
computeErrors = [];
|
|
90
|
+
computeWarnings = [];
|
|
91
|
+
|
|
92
|
+
const result = await onSolve(solveValues, signal);
|
|
93
|
+
|
|
94
|
+
if (signal.aborted) return;
|
|
95
|
+
|
|
96
|
+
computeErrors = result.errors ?? [];
|
|
97
|
+
computeWarnings = result.warnings ?? [];
|
|
98
|
+
meshes = result.meshes ?? [];
|
|
99
|
+
|
|
100
|
+
Object.assign(values, result.outputs);
|
|
101
|
+
pendingValues = {};
|
|
102
|
+
hasPendingChanges = false;
|
|
103
|
+
hasNeverSolved = false;
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (err instanceof Error && err.name === 'AbortError') return;
|
|
106
|
+
error = err instanceof Error ? err.message : String(err);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const computeThrottle = createComputeThrottle<Record<string, unknown>>(performSolveInternal, {
|
|
111
|
+
timeout: 60000
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
let solving = $derived(computeThrottle.isComputing);
|
|
115
|
+
const solvingIndicator = createSolvingIndicator(() => solving);
|
|
116
|
+
|
|
117
|
+
function performSolve() {
|
|
118
|
+
computeThrottle.trigger($state.snapshot(values));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Definition switching ─────────────────────────────────────────────────────
|
|
122
|
+
let previousDefinitionKey = $state('');
|
|
123
|
+
let isInitialLoad = $state(true);
|
|
124
|
+
|
|
125
|
+
$effect(() => {
|
|
126
|
+
const _ = definitionKey;
|
|
127
|
+
|
|
128
|
+
untrack(() => {
|
|
129
|
+
if (isInitialLoad) {
|
|
130
|
+
isInitialLoad = false;
|
|
131
|
+
previousDefinitionKey = definitionKey;
|
|
132
|
+
if (schema?.instanceSolve !== false) {
|
|
133
|
+
performSolve();
|
|
134
|
+
}
|
|
135
|
+
} else if (previousDefinitionKey !== definitionKey) {
|
|
136
|
+
meshes = [];
|
|
137
|
+
values = createInitialValues(schema);
|
|
138
|
+
error = '';
|
|
139
|
+
computeErrors = [];
|
|
140
|
+
computeWarnings = [];
|
|
141
|
+
if (schema && Object.keys(values).length > 0) {
|
|
142
|
+
performSolve();
|
|
143
|
+
}
|
|
144
|
+
previousDefinitionKey = definitionKey;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ── Handlers ─────────────────────────────────────────────────────────────────
|
|
150
|
+
async function handleValueChange(id: string, val: unknown) {
|
|
151
|
+
values[id] = val;
|
|
152
|
+
|
|
153
|
+
if (schema?.instanceSolve === false) {
|
|
154
|
+
pendingValues[id] = val;
|
|
155
|
+
hasPendingChanges = true;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
performSolve();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function handleCalculate() {
|
|
163
|
+
performSolve();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Footer item ──────────────────────────────────────────────────────────────
|
|
167
|
+
// Use untrack to read these static props without creating reactive dependencies.
|
|
168
|
+
// footerComponentProps is intentionally NOT untracked — it's a getter called every render.
|
|
169
|
+
const _footerItemId = untrack(() => footerItemId);
|
|
170
|
+
const _footerComponent = untrack(() => footerComponent);
|
|
171
|
+
const _footerItemPriority = untrack(() => footerItemPriority);
|
|
172
|
+
useFooterItem(
|
|
173
|
+
_footerItemId,
|
|
174
|
+
_footerComponent,
|
|
175
|
+
() => (_footerComponent ? (footerComponentProps?.() ?? {}) : {}),
|
|
176
|
+
'left',
|
|
177
|
+
_footerItemPriority
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// ── Embed + custom style ─────────────────────────────────────────────────────
|
|
181
|
+
let resolvedIsEmbedded = $derived(isEmbedded ?? page.url.searchParams.get('embed') === 'true');
|
|
182
|
+
let resolvedPrimaryColor = $derived(primaryColor ?? page.url.searchParams.get('primary'));
|
|
183
|
+
let customStyle = $derived(
|
|
184
|
+
resolvedPrimaryColor ? `--primary: ${hexToOklch(resolvedPrimaryColor)}` : ''
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
let pageTitle = $derived(title ?? (schema?.description || schema.name));
|
|
188
|
+
</script>
|
|
189
|
+
|
|
190
|
+
<div style={customStyle} style:display="contents">
|
|
191
|
+
{#if children}
|
|
192
|
+
{@render children({ errors: computeErrors, warnings: computeWarnings })}
|
|
193
|
+
{:else}
|
|
194
|
+
<PageContainer errors={computeErrors} warnings={computeWarnings}>
|
|
195
|
+
{#if header}
|
|
196
|
+
{@render header()}
|
|
197
|
+
{:else if !resolvedIsEmbedded}
|
|
198
|
+
<PageHeader title={pageTitle} {showModeToggle} />
|
|
199
|
+
{/if}
|
|
200
|
+
|
|
201
|
+
<div class="bg-background flex flex-1 flex-col overflow-hidden">
|
|
202
|
+
{#if error}
|
|
203
|
+
<div class="flex min-h-100 items-center justify-center p-8">
|
|
204
|
+
<StateDisplay type="error" size="medium" message={error} />
|
|
205
|
+
</div>
|
|
206
|
+
{:else if !schema}
|
|
207
|
+
<div class="flex min-h-100 items-center justify-center">
|
|
208
|
+
<StateDisplay type="loading" size="large" message="Loading schema..." />
|
|
209
|
+
</div>
|
|
210
|
+
{:else}
|
|
211
|
+
{#key definitionKey}
|
|
212
|
+
<AppLayout
|
|
213
|
+
{schema}
|
|
214
|
+
{meshes}
|
|
215
|
+
isSolving={solving}
|
|
216
|
+
showSolvingIndicator={schema.instanceSolve !== false && solvingIndicator.show}
|
|
217
|
+
{hasPendingChanges}
|
|
218
|
+
{hasNeverSolved}
|
|
219
|
+
bind:isViewerFullscreen
|
|
220
|
+
bind:values
|
|
221
|
+
{stateManagerActions}
|
|
222
|
+
{showSaveButton}
|
|
223
|
+
{showLoadButton}
|
|
224
|
+
onValueChange={handleValueChange}
|
|
225
|
+
oncalculate={handleCalculate}
|
|
226
|
+
onLoadValues={() => {
|
|
227
|
+
if (schema?.instanceSolve !== false) {
|
|
228
|
+
performSolve();
|
|
229
|
+
} else {
|
|
230
|
+
hasPendingChanges = true;
|
|
231
|
+
}
|
|
232
|
+
}}
|
|
233
|
+
/>
|
|
234
|
+
{/key}
|
|
235
|
+
{/if}
|
|
236
|
+
</div>
|
|
237
|
+
</PageContainer>
|
|
238
|
+
{/if}
|
|
239
|
+
</div>
|
|
@@ -10,13 +10,25 @@
|
|
|
10
10
|
} from '../utils/param-exporter';
|
|
11
11
|
import { Button, Input, Label, Textarea, Dialog, Card } from '../components/ui';
|
|
12
12
|
|
|
13
|
+
import type { ActionButton } from '../types/actionButton';
|
|
14
|
+
|
|
13
15
|
interface Props {
|
|
14
16
|
schema: UISchema;
|
|
15
17
|
currentValues: Record<string, unknown>;
|
|
16
18
|
onLoadValues: (values: Record<string, unknown>) => void;
|
|
19
|
+
showSaveButton?: boolean;
|
|
20
|
+
showLoadButton?: boolean;
|
|
21
|
+
actions?: ActionButton[];
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
let {
|
|
24
|
+
let {
|
|
25
|
+
schema,
|
|
26
|
+
currentValues,
|
|
27
|
+
onLoadValues,
|
|
28
|
+
showSaveButton = true,
|
|
29
|
+
showLoadButton = true,
|
|
30
|
+
actions = []
|
|
31
|
+
}: Props = $props();
|
|
20
32
|
|
|
21
33
|
// Save dialog state
|
|
22
34
|
let showExportDialog = $state(false);
|
|
@@ -127,15 +139,33 @@
|
|
|
127
139
|
</script>
|
|
128
140
|
|
|
129
141
|
<div class="gap-2 mb-2 flex items-center justify-center">
|
|
130
|
-
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
{#if showSaveButton}
|
|
143
|
+
<Button variant="default" size="sm" onclick={openExportDialog}>
|
|
144
|
+
<Download class="mr-2 h-4 w-4" />
|
|
145
|
+
Save State
|
|
146
|
+
</Button>
|
|
147
|
+
{/if}
|
|
148
|
+
|
|
149
|
+
{#if showLoadButton}
|
|
150
|
+
<Button variant="outline" size="sm" onclick={openLoadDialog}>
|
|
151
|
+
<Upload class="mr-2 h-4 w-4" />
|
|
152
|
+
Load State
|
|
153
|
+
</Button>
|
|
154
|
+
{/if}
|
|
155
|
+
|
|
156
|
+
{#each actions as action (action.id)}
|
|
157
|
+
<Button
|
|
158
|
+
variant={action.variant ?? 'outline'}
|
|
159
|
+
size={action.size ?? 'sm'}
|
|
160
|
+
onclick={action.onclick}
|
|
161
|
+
>
|
|
162
|
+
{#if action.icon}
|
|
163
|
+
{@const IconComponent = action.icon}
|
|
164
|
+
<IconComponent class="mr-2 h-4 w-4" />
|
|
165
|
+
{/if}
|
|
166
|
+
{action.label}
|
|
167
|
+
</Button>
|
|
168
|
+
{/each}
|
|
139
169
|
|
|
140
170
|
<input
|
|
141
171
|
bind:this={fileInputRef}
|
package/src/lib/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { default as PageContainer } from './components/layout/PageContainer.svel
|
|
|
3
3
|
export { default as PageHeader } from './components/layout/PageHeader.svelte';
|
|
4
4
|
export { default as PageFooter } from './components/layout/PageFooter.svelte';
|
|
5
5
|
export { default as AppLayout } from './components/AppLayout.svelte';
|
|
6
|
+
export { default as ComputeApp } from './components/ComputeApp.svelte';
|
|
6
7
|
|
|
7
8
|
// Error components
|
|
8
9
|
export { default as ErrorScreen } from './components/ErrorScreen.svelte';
|
|
@@ -25,6 +26,7 @@ export * from './features/preview/handlers';
|
|
|
25
26
|
export * from './features/preview/notifications';
|
|
26
27
|
|
|
27
28
|
// Utilities
|
|
29
|
+
export * from './utils/color';
|
|
28
30
|
export * from './utils/debounce';
|
|
29
31
|
export * from './utils/utils-shared';
|
|
30
32
|
export * from './utils/file-download';
|
|
@@ -44,6 +46,8 @@ export * from './utils';
|
|
|
44
46
|
|
|
45
47
|
// Re-export types from generated schema
|
|
46
48
|
export type * from './types/generated';
|
|
49
|
+
export type { ActionButton } from './types/actionButton';
|
|
50
|
+
export type { SolveFn, SolveResult } from './types/solveFn';
|
|
47
51
|
|
|
48
52
|
// Re-export constants from generated schema
|
|
49
53
|
export { ACCEPTED_FILE_FORMATS } from './types/generated/schema';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Helper to convert hex to OKLCH
|
|
2
|
+
export const hexToOklch = (hex: string | null): string => {
|
|
3
|
+
if (!hex) return '';
|
|
4
|
+
|
|
5
|
+
// Normalize hex (remove # if present)
|
|
6
|
+
const normalized = hex.replace(/^#/, '');
|
|
7
|
+
|
|
8
|
+
// If it's not a valid hex color, return as-is
|
|
9
|
+
if (!/^[0-9A-Fa-f]{6}$/.test(normalized)) {
|
|
10
|
+
return hex;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Parse hex to RGB
|
|
14
|
+
const r = parseInt(normalized.slice(0, 2), 16) / 255;
|
|
15
|
+
const g = parseInt(normalized.slice(2, 4), 16) / 255;
|
|
16
|
+
const b = parseInt(normalized.slice(4, 6), 16) / 255;
|
|
17
|
+
|
|
18
|
+
// Convert RGB to linear RGB
|
|
19
|
+
const toLinear = (c: number) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
|
|
20
|
+
const lr = toLinear(r);
|
|
21
|
+
const lg = toLinear(g);
|
|
22
|
+
const lb = toLinear(b);
|
|
23
|
+
|
|
24
|
+
// Convert linear RGB to XYZ
|
|
25
|
+
const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
|
|
26
|
+
const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
|
|
27
|
+
const z = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
|
|
28
|
+
|
|
29
|
+
// Convert XYZ to OKLab
|
|
30
|
+
const l_ = Math.cbrt(0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z);
|
|
31
|
+
const m_ = Math.cbrt(0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z);
|
|
32
|
+
const s_ = Math.cbrt(0.0482003018 * x + 0.2643662691 * y + 0.633851707 * z);
|
|
33
|
+
|
|
34
|
+
const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
|
|
35
|
+
const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
|
|
36
|
+
const b_lab = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;
|
|
37
|
+
|
|
38
|
+
// Convert to LCH
|
|
39
|
+
const C = Math.sqrt(a * a + b_lab * b_lab);
|
|
40
|
+
let H = (Math.atan2(b_lab, a) * 180) / Math.PI;
|
|
41
|
+
if (H < 0) H += 360;
|
|
42
|
+
|
|
43
|
+
// Format as OKLCH (round to reasonable precision)
|
|
44
|
+
return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`;
|
|
45
|
+
};
|