@selvajs/ui 4.2.0 → 4.4.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/dist/components/compute/AppLayout.svelte +1 -1
- package/dist/components/compute/AppLayout.svelte.d.ts +1 -1
- package/dist/components/compute/ComputeApp.svelte +54 -133
- package/dist/components/compute/ComputeApp.svelte.d.ts +2 -9
- package/dist/components/compute/ComputeMessagesDialog.svelte +125 -0
- package/dist/components/compute/ComputeMessagesDialog.svelte.d.ts +10 -0
- package/dist/components/compute/ParameterPresetManager.svelte +25 -33
- package/dist/components/layout/PageFooter.svelte +22 -144
- package/dist/components/preview/Group.svelte +6 -3
- package/dist/components/preview/ImageOutput.svelte +1 -13
- package/dist/components/preview/InputControl.svelte +96 -72
- package/dist/components/preview/OutputDisplay.svelte +6 -6
- package/dist/components/preview/TabLayout.svelte +6 -5
- package/dist/components/preview/inputs/FileInput.svelte +10 -19
- package/dist/composables/useFooterItem.svelte.d.ts +10 -12
- package/dist/composables/useFooterItem.svelte.js +9 -16
- package/dist/compute/computeThrottle.svelte.js +5 -2
- package/dist/compute/createSolveSession.svelte.d.ts +56 -0
- package/dist/compute/createSolveSession.svelte.js +122 -0
- package/dist/compute/solve-session-core.d.ts +45 -0
- package/dist/compute/solve-session-core.js +69 -0
- package/dist/constants.d.ts +0 -2
- package/dist/constants.js +0 -2
- package/dist/contexts/clientSlotContext.svelte.d.ts +13 -0
- package/dist/contexts/clientSlotContext.svelte.js +8 -0
- package/dist/contexts/footerContext.svelte.d.ts +17 -5
- package/dist/contexts/footerContext.svelte.js +10 -2
- package/dist/external/storage.js +7 -23
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/schema/param-exporter.d.ts +25 -1
- package/dist/schema/param-exporter.js +30 -27
- package/dist/schema/traversal.d.ts +1 -0
- package/dist/schema/traversal.js +5 -0
- package/dist/schema/visibility-rules.d.ts +12 -0
- package/dist/schema/visibility-rules.js +20 -0
- package/dist/types/solveFn.d.ts +1 -2
- package/dist/utils/file-download.d.ts +5 -17
- package/dist/utils/file-download.js +12 -19
- package/dist/utils/randomId.js +3 -19
- package/package.json +10 -26
- package/src/lib/components/compute/AppLayout.svelte +1 -1
- package/src/lib/components/compute/ComputeApp.svelte +54 -133
- package/src/lib/components/compute/ComputeMessagesDialog.svelte +125 -0
- package/src/lib/components/compute/ParameterPresetManager.svelte +25 -33
- package/src/lib/components/layout/PageFooter.svelte +22 -144
- package/src/lib/components/preview/Group.svelte +6 -3
- package/src/lib/components/preview/ImageOutput.svelte +1 -13
- package/src/lib/components/preview/InputControl.svelte +96 -72
- package/src/lib/components/preview/OutputDisplay.svelte +6 -6
- package/src/lib/components/preview/TabLayout.svelte +6 -5
- package/src/lib/components/preview/inputs/FileInput.svelte +10 -19
- package/src/lib/composables/useFooterItem.svelte.ts +25 -25
- package/src/lib/compute/computeThrottle.svelte.ts +5 -2
- package/src/lib/compute/computeThrottle.test.ts +106 -0
- package/src/lib/compute/createSolveSession.svelte.ts +188 -0
- package/src/lib/compute/solve-session-core.test.ts +153 -0
- package/src/lib/compute/solve-session-core.ts +102 -0
- package/src/lib/constants.ts +0 -2
- package/src/lib/contexts/clientSlotContext.svelte.ts +33 -0
- package/src/lib/contexts/footerContext.svelte.ts +29 -14
- package/src/lib/external/storage.test.ts +99 -0
- package/src/lib/external/storage.ts +6 -22
- package/src/lib/index.ts +2 -0
- package/src/lib/schema/param-exporter.test.ts +136 -0
- package/src/lib/schema/param-exporter.ts +44 -30
- package/src/lib/schema/traversal.test.ts +92 -0
- package/src/lib/schema/traversal.ts +5 -0
- package/src/lib/schema/visibility-rules.test.ts +173 -0
- package/src/lib/schema/visibility-rules.ts +25 -0
- package/src/lib/types/solveFn.ts +1 -1
- package/src/lib/utils/file-download.ts +13 -20
- package/src/lib/utils/randomId.ts +3 -19
- package/dist/components/compute/ComputeMessages.svelte +0 -169
- package/dist/components/compute/ComputeMessages.svelte.d.ts +0 -7
- package/dist/components/compute/index.d.ts +0 -5
- package/dist/components/compute/index.js +0 -5
- package/dist/components/preview/index.d.ts +0 -3
- package/dist/components/preview/index.js +0 -4
- package/dist/components/viewer/index.d.ts +0 -3
- package/dist/components/viewer/index.js +0 -3
- package/src/lib/components/compute/ComputeMessages.svelte +0 -169
- package/src/lib/components/compute/index.ts +0 -5
- package/src/lib/components/preview/index.ts +0 -4
- package/src/lib/components/viewer/index.ts +0 -3
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import type { UISchema, ParameterPreset } from '@selvajs/schemas';
|
|
4
4
|
import {
|
|
5
5
|
createSavedState,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
loadPreset,
|
|
7
|
+
type PresetLoadResult,
|
|
8
8
|
exportStateAsJson,
|
|
9
9
|
importStateFromJson
|
|
10
10
|
} from '../../schema/param-exporter';
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
// Import/validation state
|
|
53
53
|
let showValidationDialog = $state(false);
|
|
54
54
|
let showLoadDialog = $state(false);
|
|
55
|
-
let
|
|
56
|
-
let
|
|
55
|
+
let loadResult = $state<PresetLoadResult | null>(null);
|
|
56
|
+
let loadingPresetName = $state('');
|
|
57
57
|
let fileInputRef = $state<HTMLInputElement | null>(null);
|
|
58
58
|
|
|
59
59
|
// Listed states (when onListStates is provided)
|
|
@@ -90,15 +90,15 @@
|
|
|
90
90
|
showExportDialog = false;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
//
|
|
93
|
+
// Load a preset in one pass, then either apply it directly (no issues) or open the
|
|
94
94
|
// validation dialog (any errors or warnings). Shared by every load path.
|
|
95
95
|
function tryLoad(preset: ParameterPreset) {
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
98
|
-
onLoadValues(
|
|
96
|
+
const result = loadPreset(preset, schema);
|
|
97
|
+
if (result.isValid) {
|
|
98
|
+
onLoadValues(result.values);
|
|
99
99
|
} else {
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
loadResult = result;
|
|
101
|
+
loadingPresetName = preset.name;
|
|
102
102
|
showValidationDialog = true;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
@@ -119,21 +119,16 @@
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
function confirmLoad() {
|
|
122
|
-
if (!
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
showValidationDialog = false;
|
|
128
|
-
importedState = null;
|
|
129
|
-
validationResult = null;
|
|
130
|
-
}
|
|
122
|
+
if (!loadResult || !loadResult.canLoad) return;
|
|
123
|
+
|
|
124
|
+
onLoadValues(loadResult.values);
|
|
125
|
+
showValidationDialog = false;
|
|
126
|
+
loadResult = null;
|
|
131
127
|
}
|
|
132
128
|
|
|
133
129
|
function cancelImport() {
|
|
134
130
|
showValidationDialog = false;
|
|
135
|
-
|
|
136
|
-
validationResult = null;
|
|
131
|
+
loadResult = null;
|
|
137
132
|
}
|
|
138
133
|
|
|
139
134
|
async function openLoadDialog() {
|
|
@@ -288,16 +283,16 @@
|
|
|
288
283
|
<Dialog.Header>
|
|
289
284
|
<Dialog.Title>{t.validationTitle}</Dialog.Title>
|
|
290
285
|
<Dialog.Description>
|
|
291
|
-
{#if
|
|
292
|
-
{t.validationValidatingPrefix}{
|
|
286
|
+
{#if loadingPresetName}
|
|
287
|
+
{t.validationValidatingPrefix}{loadingPresetName}
|
|
293
288
|
{/if}
|
|
294
289
|
</Dialog.Description>
|
|
295
290
|
</Dialog.Header>
|
|
296
291
|
|
|
297
|
-
{#if
|
|
292
|
+
{#if loadResult}
|
|
298
293
|
<div class="gap-4 py-4 grid">
|
|
299
294
|
<!-- Summary Alert -->
|
|
300
|
-
{#if
|
|
295
|
+
{#if loadResult.isValid}
|
|
301
296
|
<Card.Root class="p-4 border-success/30 bg-success/5">
|
|
302
297
|
<div class="gap-3 flex items-start">
|
|
303
298
|
<CheckCircle class="h-5 w-5 text-success" />
|
|
@@ -311,7 +306,7 @@
|
|
|
311
306
|
</div>
|
|
312
307
|
</div>
|
|
313
308
|
</Card.Root>
|
|
314
|
-
{:else if
|
|
309
|
+
{:else if loadResult.canLoad}
|
|
315
310
|
<Card.Root class="p-4 border-warning/30 bg-warning/5">
|
|
316
311
|
<div class="gap-3 flex items-start">
|
|
317
312
|
<AlertTriangle class="h-5 w-5 text-warning" />
|
|
@@ -320,10 +315,7 @@
|
|
|
320
315
|
{t.validationWarningsTitle}
|
|
321
316
|
</h4>
|
|
322
317
|
<p class="text-sm mt-1 text-warning-foreground/80">
|
|
323
|
-
{t.validationWarningsBody.replace(
|
|
324
|
-
'{count}',
|
|
325
|
-
String(validationResult.issues.length)
|
|
326
|
-
)}
|
|
318
|
+
{t.validationWarningsBody.replace('{count}', String(loadResult.issues.length))}
|
|
327
319
|
</p>
|
|
328
320
|
</div>
|
|
329
321
|
</div>
|
|
@@ -343,10 +335,10 @@
|
|
|
343
335
|
{/if}
|
|
344
336
|
|
|
345
337
|
<!-- Issues List -->
|
|
346
|
-
{#if !
|
|
338
|
+
{#if !loadResult.isValid}
|
|
347
339
|
<div class="space-y-2">
|
|
348
340
|
<h4 class="text-sm font-medium">{t.validationIssuesHeading}</h4>
|
|
349
|
-
{#each
|
|
341
|
+
{#each loadResult.issues as issue (issue.message)}
|
|
350
342
|
<div
|
|
351
343
|
class="p-3 rounded-lg border {issue.severity === 'error'
|
|
352
344
|
? 'border-destructive/30 bg-destructive/5'
|
|
@@ -378,7 +370,7 @@
|
|
|
378
370
|
|
|
379
371
|
<Dialog.Footer>
|
|
380
372
|
<Button variant="outline" onclick={cancelImport}>{t.cancelButton}</Button>
|
|
381
|
-
{#if
|
|
373
|
+
{#if loadResult?.canLoad}
|
|
382
374
|
<Button onclick={confirmLoad}>{t.loadAnywayButton}</Button>
|
|
383
375
|
{/if}
|
|
384
376
|
</Dialog.Footer>
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import
|
|
4
|
-
import * as Collapsible from '$lib/components/primitives/collapsible';
|
|
5
|
-
import { CircleAlert, TriangleAlert, ChevronDown, ChevronRight } from '@lucide/svelte';
|
|
6
|
-
import { SvelteMap } from 'svelte/reactivity';
|
|
3
|
+
import { CircleAlert, TriangleAlert } from '@lucide/svelte';
|
|
7
4
|
import { useFooter } from '$lib/contexts/footerContext.svelte';
|
|
5
|
+
import ComputeMessagesDialog from '../compute/ComputeMessagesDialog.svelte';
|
|
8
6
|
import FooterItemRenderer from './FooterItemRenderer.svelte';
|
|
9
7
|
|
|
10
8
|
interface Props {
|
|
@@ -48,31 +46,10 @@
|
|
|
48
46
|
);
|
|
49
47
|
|
|
50
48
|
let _currentYear = new Date().getFullYear();
|
|
51
|
-
let dialogOpen = $state(false);
|
|
52
|
-
let showErrors = $state(true);
|
|
53
|
-
let showWarnings = $state(false);
|
|
54
49
|
|
|
55
50
|
const hasMessages = $derived(errors.length > 0 || warnings.length > 0);
|
|
56
51
|
const totalCount = $derived(errors.length + warnings.length);
|
|
57
52
|
|
|
58
|
-
function groupMessages(messages: string[]): Array<{ message: string; count: number }> {
|
|
59
|
-
const grouped = new SvelteMap<string, number>();
|
|
60
|
-
|
|
61
|
-
for (const msg of messages) {
|
|
62
|
-
// Extract the base message without the component GUID
|
|
63
|
-
const baseMsg = msg.replace(/\([a-f0-9-]{36}\)$/i, '(...)').trim();
|
|
64
|
-
const currentCount = grouped.get(baseMsg) || 0;
|
|
65
|
-
grouped.set(baseMsg, currentCount + 1);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return Array.from(grouped.entries())
|
|
69
|
-
.map(([message, count]) => ({ message, count }))
|
|
70
|
-
.sort((a, b) => b.count - a.count);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const groupedErrors = $derived(groupMessages(errors));
|
|
74
|
-
const groupedWarnings = $derived(groupMessages(warnings));
|
|
75
|
-
|
|
76
53
|
const copyrightLine = $derived(
|
|
77
54
|
footerText
|
|
78
55
|
? footerText.replace('{name}', copyrightName).replace('{year}', String(_currentYear))
|
|
@@ -91,131 +68,32 @@
|
|
|
91
68
|
{/each}
|
|
92
69
|
|
|
93
70
|
{#if hasMessages}
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<CircleAlert class="h-3.5 w-3.5" />
|
|
104
|
-
<span class="font-medium">{errors.length} Error{errors.length !== 1 ? 's' : ''}</span>
|
|
105
|
-
{/if}
|
|
106
|
-
|
|
107
|
-
{#if warnings.length > 0}
|
|
71
|
+
<ComputeMessagesDialog {errors} {warnings}>
|
|
72
|
+
{#snippet trigger()}
|
|
73
|
+
<div
|
|
74
|
+
class="gap-1.5 px-2 py-1 rounded flex cursor-pointer items-center transition-colors hover:bg-muted {errors.length >
|
|
75
|
+
0
|
|
76
|
+
? 'text-destructive hover:bg-destructive/10'
|
|
77
|
+
: 'text-warning hover:bg-warning/10'}"
|
|
78
|
+
title={`${totalCount} ${totalCount === 1 ? 'issue' : 'issues'}`}
|
|
79
|
+
>
|
|
108
80
|
{#if errors.length > 0}
|
|
109
|
-
<
|
|
81
|
+
<CircleAlert class="h-3.5 w-3.5" />
|
|
82
|
+
<span class="font-medium">{errors.length} Error{errors.length !== 1 ? 's' : ''}</span>
|
|
110
83
|
{/if}
|
|
111
|
-
<TriangleAlert class="h-3.5 w-3.5" />
|
|
112
|
-
<span class="font-medium"
|
|
113
|
-
>{warnings.length} Warning{warnings.length !== 1 ? 's' : ''}</span
|
|
114
|
-
>
|
|
115
|
-
{/if}
|
|
116
|
-
</Dialog.Trigger>
|
|
117
84
|
|
|
118
|
-
<Dialog.Content class="max-w-2xl max-h-[80vh]">
|
|
119
|
-
<Dialog.Header>
|
|
120
|
-
<Dialog.Title class="gap-2 flex items-center">
|
|
121
|
-
<CircleAlert class="h-5 w-5" />
|
|
122
|
-
Compute Messages
|
|
123
|
-
</Dialog.Title>
|
|
124
|
-
<Dialog.Description>
|
|
125
|
-
{totalCount}
|
|
126
|
-
{totalCount === 1 ? 'issue' : 'issues'} detected during solve
|
|
127
|
-
</Dialog.Description>
|
|
128
|
-
</Dialog.Header>
|
|
129
|
-
|
|
130
|
-
<div class="space-y-3 pr-2 overflow-y-auto" style="max-height: calc(80vh - 180px);">
|
|
131
|
-
<!-- Errors Section -->
|
|
132
|
-
{#if errors.length > 0}
|
|
133
|
-
<Collapsible.Root bind:open={showErrors}>
|
|
134
|
-
<div class="overflow-hidden rounded-lg border border-destructive bg-card">
|
|
135
|
-
<div class="px-4 py-3 flex items-center">
|
|
136
|
-
<Collapsible.Trigger
|
|
137
|
-
class="-mx-4 -my-3 gap-3 px-4 py-3 flex flex-1 items-center text-left transition-colors hover:bg-destructive/5"
|
|
138
|
-
>
|
|
139
|
-
{#if showErrors}
|
|
140
|
-
<ChevronDown class="h-4 w-4 shrink-0 text-destructive" />
|
|
141
|
-
{:else}
|
|
142
|
-
<ChevronRight class="h-4 w-4 shrink-0 text-destructive" />
|
|
143
|
-
{/if}
|
|
144
|
-
<CircleAlert class="h-4 w-4 shrink-0 text-destructive" />
|
|
145
|
-
<span class="text-sm font-medium text-destructive">
|
|
146
|
-
{errors.length === 1 ? '1 Error' : `${errors.length} Errors`}
|
|
147
|
-
</span>
|
|
148
|
-
</Collapsible.Trigger>
|
|
149
|
-
</div>
|
|
150
|
-
|
|
151
|
-
<Collapsible.Content class="space-y-0">
|
|
152
|
-
<div
|
|
153
|
-
class="max-h-60 px-4 py-3 overflow-y-auto border-t border-destructive bg-card"
|
|
154
|
-
>
|
|
155
|
-
<ul class="space-y-2">
|
|
156
|
-
{#each groupedErrors as { message, count } (message)}
|
|
157
|
-
<li class="gap-2 text-sm flex text-destructive/90">
|
|
158
|
-
<span class="shrink-0">•</span>
|
|
159
|
-
<span class="flex-1">
|
|
160
|
-
{message}
|
|
161
|
-
{#if count > 1}
|
|
162
|
-
<span class="ml-1 font-medium text-destructive/70">×{count} </span>
|
|
163
|
-
{/if}
|
|
164
|
-
</span>
|
|
165
|
-
</li>
|
|
166
|
-
{/each}
|
|
167
|
-
</ul>
|
|
168
|
-
</div>
|
|
169
|
-
</Collapsible.Content>
|
|
170
|
-
</div>
|
|
171
|
-
</Collapsible.Root>
|
|
172
|
-
{/if}
|
|
173
|
-
|
|
174
|
-
<!-- Warnings Section -->
|
|
175
85
|
{#if warnings.length > 0}
|
|
176
|
-
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
<ChevronDown class="h-4 w-4 shrink-0 text-warning" />
|
|
184
|
-
{:else}
|
|
185
|
-
<ChevronRight class="h-4 w-4 shrink-0 text-warning" />
|
|
186
|
-
{/if}
|
|
187
|
-
<TriangleAlert class="h-4 w-4 shrink-0 text-warning" />
|
|
188
|
-
<span class="text-sm font-medium text-warning">
|
|
189
|
-
{warnings.length === 1 ? '1 Warning' : `${warnings.length} Warnings`}
|
|
190
|
-
</span>
|
|
191
|
-
</Collapsible.Trigger>
|
|
192
|
-
</div>
|
|
193
|
-
|
|
194
|
-
<Collapsible.Content class="space-y-0">
|
|
195
|
-
<div
|
|
196
|
-
class="max-h-60 px-4 py-3 overflow-y-auto border-t border-warning/50 bg-card"
|
|
197
|
-
>
|
|
198
|
-
<ul class="space-y-2">
|
|
199
|
-
{#each groupedWarnings as { message, count } (message)}
|
|
200
|
-
<li class="gap-2 text-sm flex text-muted-foreground">
|
|
201
|
-
<span class="shrink-0">•</span>
|
|
202
|
-
<span class="flex-1">
|
|
203
|
-
{message}
|
|
204
|
-
{#if count > 1}
|
|
205
|
-
<span class="ml-1 font-medium text-warning/70">×{count}</span>
|
|
206
|
-
{/if}
|
|
207
|
-
</span>
|
|
208
|
-
</li>
|
|
209
|
-
{/each}
|
|
210
|
-
</ul>
|
|
211
|
-
</div>
|
|
212
|
-
</Collapsible.Content>
|
|
213
|
-
</div>
|
|
214
|
-
</Collapsible.Root>
|
|
86
|
+
{#if errors.length > 0}
|
|
87
|
+
<span class="text-border">•</span>
|
|
88
|
+
{/if}
|
|
89
|
+
<TriangleAlert class="h-3.5 w-3.5" />
|
|
90
|
+
<span class="font-medium"
|
|
91
|
+
>{warnings.length} Warning{warnings.length !== 1 ? 's' : ''}</span
|
|
92
|
+
>
|
|
215
93
|
{/if}
|
|
216
94
|
</div>
|
|
217
|
-
|
|
218
|
-
</
|
|
95
|
+
{/snippet}
|
|
96
|
+
</ComputeMessagesDialog>
|
|
219
97
|
{/if}
|
|
220
98
|
|
|
221
99
|
{#if children}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { Snippet } from 'svelte';
|
|
4
4
|
import * as Card from '$lib/components/primitives/card';
|
|
5
5
|
import { ChevronDown } from '@lucide/svelte';
|
|
6
|
-
import {
|
|
6
|
+
import { buildVisibilityMap, itemKey } from '$lib/schema/visibility-rules';
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
label: string;
|
|
@@ -44,6 +44,9 @@
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// One visibility evaluation per item per render; columnStarts and gridItem both read it.
|
|
48
|
+
const visibilityMap = $derived(buildVisibilityMap(items, values));
|
|
49
|
+
|
|
47
50
|
/**
|
|
48
51
|
* Compute the column position (0-indexed) where each item starts, accounting
|
|
49
52
|
* for spans and linebreak resets. Items hidden by visibility don't consume
|
|
@@ -58,7 +61,7 @@
|
|
|
58
61
|
col = 0;
|
|
59
62
|
continue;
|
|
60
63
|
}
|
|
61
|
-
const visibility =
|
|
64
|
+
const visibility = visibilityMap[itemKey(item)];
|
|
62
65
|
if (!visibility.visible) {
|
|
63
66
|
positions.push(0);
|
|
64
67
|
continue;
|
|
@@ -120,7 +123,7 @@
|
|
|
120
123
|
{#if layoutItem.type === 'linebreak'}
|
|
121
124
|
<div style="grid-column: 1 / -1" class="h-px bg-border" aria-hidden="true"></div>
|
|
122
125
|
{:else}
|
|
123
|
-
{@const visibility =
|
|
126
|
+
{@const visibility = visibilityMap[itemKey(layoutItem)]}
|
|
124
127
|
{@const span = Math.min(Math.max(1, layoutItem.span ?? 1), cols)}
|
|
125
128
|
{#if visibility.visible}
|
|
126
129
|
{#if layoutItem.type === 'input'}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { OutputImageLayoutItem } from '@selvajs/schemas';
|
|
3
3
|
import type { FileData } from '@selvajs/compute';
|
|
4
4
|
import { Download, Maximize, Minimize } from '@lucide/svelte';
|
|
5
|
-
import { downloadFiles } from '$lib/utils/file-download';
|
|
5
|
+
import { downloadFiles, isFileData, MIME_BY_EXT } from '$lib/utils/file-download';
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
8
|
item: OutputImageLayoutItem;
|
|
@@ -12,18 +12,6 @@
|
|
|
12
12
|
let { item, value }: Props = $props();
|
|
13
13
|
|
|
14
14
|
const SUPPORTED_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg']);
|
|
15
|
-
const MIME_BY_EXT: Record<string, string> = {
|
|
16
|
-
'.png': 'image/png',
|
|
17
|
-
'.jpg': 'image/jpeg',
|
|
18
|
-
'.jpeg': 'image/jpeg',
|
|
19
|
-
'.webp': 'image/webp',
|
|
20
|
-
'.gif': 'image/gif',
|
|
21
|
-
'.svg': 'image/svg+xml'
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
function isFileData(x: unknown): x is FileData {
|
|
25
|
-
return !!x && typeof x === 'object' && 'fileName' in x && 'data' in x && 'fileType' in x;
|
|
26
|
-
}
|
|
27
15
|
|
|
28
16
|
const file = $derived.by<FileData | null>(() => {
|
|
29
17
|
if (Array.isArray(value)) return value.find(isFileData) ?? null;
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
NumberInput,
|
|
21
21
|
TextInput
|
|
22
22
|
} from '$lib/components/preview/inputs';
|
|
23
|
+
import { getClientSlot } from '$lib/contexts/clientSlotContext.svelte';
|
|
23
24
|
|
|
24
25
|
interface Props {
|
|
25
26
|
item: InputLayoutItem;
|
|
@@ -40,6 +41,18 @@
|
|
|
40
41
|
const inputId = $derived(`input-${item.paramId}`);
|
|
41
42
|
const label = $derived(displayName || item.displayName || item.paramId);
|
|
42
43
|
|
|
44
|
+
// Client-sourced input set to render a host element in its place. The 'hidden'
|
|
45
|
+
// presentation never reaches here (visible:false filters it out upstream), so a
|
|
46
|
+
// client slot source means presentation === 'slot'.
|
|
47
|
+
const clientSlotConfig = $derived.by(() => {
|
|
48
|
+
const source = (
|
|
49
|
+
item as { source?: { kind?: string; client?: { presentation?: string; slotLabel?: string } } }
|
|
50
|
+
).source;
|
|
51
|
+
if (source?.kind !== 'client' || source.client?.presentation !== 'slot') return null;
|
|
52
|
+
return { slotLabel: source.client.slotLabel };
|
|
53
|
+
});
|
|
54
|
+
const clientSlot = getClientSlot();
|
|
55
|
+
|
|
43
56
|
// Number range hint — shown next to label for sliders, under the input for plain number fields.
|
|
44
57
|
const numberRangeHint = $derived.by(() => {
|
|
45
58
|
if (!isNumberWidget(item)) return null;
|
|
@@ -61,87 +74,98 @@
|
|
|
61
74
|
}
|
|
62
75
|
</script>
|
|
63
76
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
{#if clientSlotConfig}
|
|
78
|
+
{#if clientSlot}
|
|
79
|
+
{@render clientSlot({
|
|
80
|
+
inputId: item.paramId,
|
|
81
|
+
displayName: label,
|
|
82
|
+
slotLabel: clientSlotConfig.slotLabel,
|
|
83
|
+
value
|
|
84
|
+
})}
|
|
85
|
+
{/if}
|
|
86
|
+
{:else}
|
|
87
|
+
<Field.Field>
|
|
88
|
+
<Field.Label for={inputId} class="gap-2 flex items-center">
|
|
89
|
+
{label}
|
|
90
|
+
{#if item.description}
|
|
91
|
+
<Dialog.Root>
|
|
92
|
+
<Dialog.Trigger class="p-1 cursor-help opacity-60 transition-opacity hover:opacity-100">
|
|
93
|
+
<HelpCircle size={16} />
|
|
94
|
+
</Dialog.Trigger>
|
|
95
|
+
<Dialog.Content class="sm:max-w-md">
|
|
96
|
+
<Dialog.Header>
|
|
97
|
+
<Dialog.Title>{label}</Dialog.Title>
|
|
98
|
+
<Dialog.Description>{item.description}</Dialog.Description>
|
|
99
|
+
</Dialog.Header>
|
|
100
|
+
</Dialog.Content>
|
|
101
|
+
</Dialog.Root>
|
|
102
|
+
{/if}
|
|
103
|
+
{#if showRangeInLabel}
|
|
104
|
+
<span class="text-xs font-normal text-muted-foreground">{numberRangeHint}</span>
|
|
105
|
+
{/if}
|
|
106
|
+
</Field.Label>
|
|
84
107
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
{inputId}
|
|
89
|
-
value={typeof value === 'number' ? value : undefined}
|
|
90
|
-
{config}
|
|
91
|
-
onChange={commit}
|
|
92
|
-
{disabled}
|
|
93
|
-
/>
|
|
94
|
-
{:else if isCheckboxWidget(item)}
|
|
95
|
-
<CheckboxInput
|
|
96
|
-
{inputId}
|
|
97
|
-
value={typeof value === 'boolean' ? value : undefined}
|
|
98
|
-
onChange={commit}
|
|
99
|
-
{disabled}
|
|
100
|
-
/>
|
|
101
|
-
{:else if isTextWidget(item)}
|
|
102
|
-
{@const config = item.config}
|
|
103
|
-
<TextInput
|
|
104
|
-
{inputId}
|
|
105
|
-
value={typeof value === 'string' ? value : ''}
|
|
106
|
-
{config}
|
|
107
|
-
onChange={commit}
|
|
108
|
-
{disabled}
|
|
109
|
-
/>
|
|
110
|
-
{:else if isDropdownWidget(item)}
|
|
111
|
-
{@const config = item.config}
|
|
112
|
-
{#if config.displayAs === 'checklist'}
|
|
113
|
-
<ChecklistInput
|
|
108
|
+
{#if isNumberWidget(item)}
|
|
109
|
+
{@const config = item.config}
|
|
110
|
+
<NumberInput
|
|
114
111
|
{inputId}
|
|
115
|
-
value={
|
|
116
|
-
? (value as string[])
|
|
117
|
-
: typeof value === 'string' && value
|
|
118
|
-
? [value]
|
|
119
|
-
: []}
|
|
112
|
+
value={typeof value === 'number' ? value : undefined}
|
|
120
113
|
{config}
|
|
121
114
|
onChange={commit}
|
|
122
115
|
{disabled}
|
|
123
116
|
/>
|
|
124
|
-
{:else}
|
|
125
|
-
<
|
|
117
|
+
{:else if isCheckboxWidget(item)}
|
|
118
|
+
<CheckboxInput
|
|
119
|
+
{inputId}
|
|
120
|
+
value={typeof value === 'boolean' ? value : undefined}
|
|
121
|
+
onChange={commit}
|
|
122
|
+
{disabled}
|
|
123
|
+
/>
|
|
124
|
+
{:else if isTextWidget(item)}
|
|
125
|
+
{@const config = item.config}
|
|
126
|
+
<TextInput
|
|
127
|
+
{inputId}
|
|
126
128
|
value={typeof value === 'string' ? value : ''}
|
|
127
129
|
{config}
|
|
128
130
|
onChange={commit}
|
|
129
131
|
{disabled}
|
|
130
132
|
/>
|
|
133
|
+
{:else if isDropdownWidget(item)}
|
|
134
|
+
{@const config = item.config}
|
|
135
|
+
{#if config.displayAs === 'checklist'}
|
|
136
|
+
<ChecklistInput
|
|
137
|
+
{inputId}
|
|
138
|
+
value={Array.isArray(value)
|
|
139
|
+
? (value as string[])
|
|
140
|
+
: typeof value === 'string' && value
|
|
141
|
+
? [value]
|
|
142
|
+
: []}
|
|
143
|
+
{config}
|
|
144
|
+
onChange={commit}
|
|
145
|
+
{disabled}
|
|
146
|
+
/>
|
|
147
|
+
{:else}
|
|
148
|
+
<DropdownInput
|
|
149
|
+
value={typeof value === 'string' ? value : ''}
|
|
150
|
+
{config}
|
|
151
|
+
onChange={commit}
|
|
152
|
+
{disabled}
|
|
153
|
+
/>
|
|
154
|
+
{/if}
|
|
155
|
+
{:else if isFileWidget(item)}
|
|
156
|
+
{@const config = item.config as FileInputWidgetConfig}
|
|
157
|
+
<FileInput
|
|
158
|
+
value={typeof value === 'string' ? value : ''}
|
|
159
|
+
acceptedFormats={config?.acceptedFormats ?? []}
|
|
160
|
+
onChange={(newValue) => commit(newValue)}
|
|
161
|
+
defaultInputMode={config?.defaultInputMode}
|
|
162
|
+
allowedInputModes={config?.allowedInputModes}
|
|
163
|
+
/>
|
|
164
|
+
{:else if isColorWidget(item)}
|
|
165
|
+
<ColorInput
|
|
166
|
+
value={typeof value === 'string' ? value : '#000000'}
|
|
167
|
+
onChange={(newValue) => commit(newValue)}
|
|
168
|
+
/>
|
|
131
169
|
{/if}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
<FileInput
|
|
135
|
-
value={typeof value === 'string' ? value : ''}
|
|
136
|
-
acceptedFormats={config?.acceptedFormats ?? []}
|
|
137
|
-
onChange={(newValue) => commit(newValue)}
|
|
138
|
-
defaultInputMode={config?.defaultInputMode}
|
|
139
|
-
allowedInputModes={config?.allowedInputModes}
|
|
140
|
-
/>
|
|
141
|
-
{:else if isColorWidget(item)}
|
|
142
|
-
<ColorInput
|
|
143
|
-
value={typeof value === 'string' ? value : '#000000'}
|
|
144
|
-
onChange={(newValue) => commit(newValue)}
|
|
145
|
-
/>
|
|
146
|
-
{/if}
|
|
147
|
-
</Field.Field>
|
|
170
|
+
</Field.Field>
|
|
171
|
+
{/if}
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
import type { FileData } from '@selvajs/compute';
|
|
4
4
|
import ChartOutput from './ChartOutput.svelte';
|
|
5
5
|
import ImageOutput from './ImageOutput.svelte';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
downloadFiles,
|
|
8
|
+
formatFileSize,
|
|
9
|
+
getBase64FileSize,
|
|
10
|
+
isFileData
|
|
11
|
+
} from '$lib/utils/file-download';
|
|
7
12
|
import { Button } from '../primitives';
|
|
8
13
|
import * as Dialog from '$lib/components/primitives/dialog';
|
|
9
14
|
import { Info, Folder, FolderOpen, FileIcon, ChevronRight } from '@lucide/svelte';
|
|
@@ -46,10 +51,6 @@
|
|
|
46
51
|
let downloading = $state(false);
|
|
47
52
|
let downloadError = $state<string | null>(null);
|
|
48
53
|
|
|
49
|
-
function isFileData(data: any): data is FileData {
|
|
50
|
-
return data && typeof data === 'object' && 'fileName' in data && 'data' in data;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
54
|
const filesArray = $derived(
|
|
54
55
|
!value ? [] : Array.isArray(value) ? value.filter(isFileData) : isFileData(value) ? [value] : []
|
|
55
56
|
);
|
|
@@ -114,7 +115,6 @@
|
|
|
114
115
|
});
|
|
115
116
|
const hasDuplicates = $derived(duplicatePaths.length > 0);
|
|
116
117
|
|
|
117
|
-
// All folders expanded by default
|
|
118
118
|
let expandedFolders = new SvelteSet<string>();
|
|
119
119
|
|
|
120
120
|
$effect(() => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import * as Tabs from '$lib/components/primitives/tabs';
|
|
5
5
|
import TabBar from './TabBar.svelte';
|
|
6
6
|
import TabContent from './TabContent.svelte';
|
|
7
|
-
import {
|
|
7
|
+
import { buildVisibilityMap, itemKey } from '$lib/schema/visibility-rules';
|
|
8
8
|
|
|
9
9
|
interface Props {
|
|
10
10
|
schema: UISchema;
|
|
@@ -64,17 +64,18 @@
|
|
|
64
64
|
if (schema.layout.type !== 'tabbed') return;
|
|
65
65
|
const updates: Record<string, unknown> = {};
|
|
66
66
|
visibleTabs.forEach((tab) =>
|
|
67
|
-
tab.groups.forEach((group) =>
|
|
67
|
+
tab.groups.forEach((group) => {
|
|
68
|
+
const visibilityMap = buildVisibilityMap(group.items, values);
|
|
68
69
|
group.items.forEach((layoutItem) => {
|
|
69
70
|
if (layoutItem.type === 'linebreak') return;
|
|
70
|
-
const { visible, disabled, defaultValue } =
|
|
71
|
+
const { visible, disabled, defaultValue } = visibilityMap[itemKey(layoutItem)];
|
|
71
72
|
const input = schema.inputs.find((i) => i.id === layoutItem.paramId);
|
|
72
73
|
if (!input || defaultValue === undefined) return;
|
|
73
74
|
if ((!visible || disabled) && values[input.id] !== defaultValue) {
|
|
74
75
|
updates[input.id] = defaultValue;
|
|
75
76
|
}
|
|
76
|
-
})
|
|
77
|
-
)
|
|
77
|
+
});
|
|
78
|
+
})
|
|
78
79
|
);
|
|
79
80
|
if (Object.keys(updates).length > 0) Object.assign(values, updates);
|
|
80
81
|
});
|