@selvajs/ui 4.3.0 → 4.5.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 +3 -3
- package/dist/components/compute/AppLayout.svelte.d.ts +2 -2
- package/dist/components/compute/ComputeApp.svelte +47 -140
- package/dist/components/compute/ComputeApp.svelte.d.ts +0 -16
- 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/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/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 +3 -1
- package/dist/index.js +5 -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 +3 -3
- package/src/lib/components/compute/ComputeApp.svelte +47 -140
- 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/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/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 +14 -1
- 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
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import
|
|
4
|
-
import * as Collapsible from '../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 '../../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 '../primitives/card';
|
|
5
5
|
import { ChevronDown } from '@lucide/svelte';
|
|
6
|
-
import {
|
|
6
|
+
import { buildVisibilityMap, itemKey } from '../../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 '../../utils/file-download';
|
|
5
|
+
import { downloadFiles, isFileData, MIME_BY_EXT } from '../../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;
|
|
@@ -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 '../../utils/file-download';
|
|
7
12
|
import { Button } from '../primitives';
|
|
8
13
|
import * as Dialog from '../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 '../primitives/tabs';
|
|
5
5
|
import TabBar from './TabBar.svelte';
|
|
6
6
|
import TabContent from './TabContent.svelte';
|
|
7
|
-
import {
|
|
7
|
+
import { buildVisibilityMap, itemKey } from '../../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
|
});
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
let urlInput = $state('');
|
|
42
42
|
let isDragging = $state(false);
|
|
43
43
|
let isLoading = $state(false);
|
|
44
|
-
let urlError = $state<{ message: string; isCors: boolean } | null>(null);
|
|
44
|
+
let urlError = $state<{ message: string; isCors: boolean; hint?: string } | null>(null);
|
|
45
45
|
let urlSuccess = $state('');
|
|
46
46
|
|
|
47
47
|
// Parse existing value if it's JSON
|
|
@@ -88,26 +88,22 @@
|
|
|
88
88
|
async function handleUrlChange() {
|
|
89
89
|
if (!urlInput.trim()) return;
|
|
90
90
|
|
|
91
|
-
// Reset all state before starting a new fetch
|
|
92
91
|
isLoading = true;
|
|
93
92
|
urlError = null;
|
|
94
93
|
urlSuccess = '';
|
|
95
94
|
uploadedFileName = '';
|
|
96
95
|
|
|
97
96
|
try {
|
|
98
|
-
// Validate URL format first
|
|
99
97
|
let parsedUrl: URL;
|
|
100
98
|
try {
|
|
101
99
|
parsedUrl = new URL(urlInput);
|
|
102
100
|
} catch {
|
|
103
101
|
urlError = { message: 'Invalid URL — make sure it starts with https://', isCors: false };
|
|
104
|
-
isLoading = false;
|
|
105
102
|
return;
|
|
106
103
|
}
|
|
107
104
|
|
|
108
105
|
if (parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') {
|
|
109
106
|
urlError = { message: 'Only http:// and https:// URLs are supported.', isCors: false };
|
|
110
|
-
isLoading = false;
|
|
111
107
|
return;
|
|
112
108
|
}
|
|
113
109
|
|
|
@@ -117,7 +113,6 @@
|
|
|
117
113
|
message: `Server returned ${response.status} ${response.statusText}. Check the URL is correct and publicly accessible.`,
|
|
118
114
|
isCors: false
|
|
119
115
|
};
|
|
120
|
-
isLoading = false;
|
|
121
116
|
return;
|
|
122
117
|
}
|
|
123
118
|
|
|
@@ -129,7 +124,6 @@
|
|
|
129
124
|
message: `File format "${fileEnding}" is not accepted. Allowed: ${acceptedFormats.join(', ')}`,
|
|
130
125
|
isCors: false
|
|
131
126
|
};
|
|
132
|
-
isLoading = false;
|
|
133
127
|
return;
|
|
134
128
|
}
|
|
135
129
|
|
|
@@ -139,24 +133,22 @@
|
|
|
139
133
|
message: `File too large: ${(blob.size / 1024 / 1024).toFixed(2)}MB (max ${APP_DEFAULTS.FILE_UPLOAD.MAX_SIZE_MB}MB). Download it and use Upload instead.`,
|
|
140
134
|
isCors: false
|
|
141
135
|
};
|
|
142
|
-
isLoading = false;
|
|
143
136
|
return;
|
|
144
137
|
}
|
|
145
138
|
|
|
146
|
-
await new Promise<
|
|
139
|
+
const base64Data = await new Promise<string>((resolve, reject) => {
|
|
147
140
|
const reader = new FileReader();
|
|
148
141
|
reader.onload = (e) => {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
const data = JSON.stringify({ file: base64Data, type: 'base64', fileEnding });
|
|
152
|
-
value = data;
|
|
153
|
-
urlSuccess = `Loaded ${fileEnding} (${(blob.size / 1024).toFixed(0)} KB)`;
|
|
154
|
-
onChange(data);
|
|
155
|
-
resolve();
|
|
142
|
+
const result = e.target?.result as string;
|
|
143
|
+
resolve(result.includes(',') ? result.split(',')[1] : result);
|
|
156
144
|
};
|
|
157
145
|
reader.onerror = () => reject(new Error('Failed to read the downloaded file.'));
|
|
158
146
|
reader.readAsDataURL(blob);
|
|
159
147
|
});
|
|
148
|
+
const data = JSON.stringify({ file: base64Data, type: 'base64', fileEnding });
|
|
149
|
+
value = data;
|
|
150
|
+
urlSuccess = `Loaded ${fileEnding} (${(blob.size / 1024).toFixed(0)} KB)`;
|
|
151
|
+
onChange(data);
|
|
160
152
|
} catch (error) {
|
|
161
153
|
if (isCorsError(error)) {
|
|
162
154
|
const host = (() => {
|
|
@@ -184,7 +176,6 @@
|
|
|
184
176
|
urlError = {
|
|
185
177
|
message: `Could not reach "${host}" — the file may not exist, or the server does not allow browser access.`,
|
|
186
178
|
isCors: true,
|
|
187
|
-
// @ts-expect-error hint is properly typed at runtime
|
|
188
179
|
hint
|
|
189
180
|
};
|
|
190
181
|
} else {
|
|
@@ -328,8 +319,8 @@
|
|
|
328
319
|
<CircleAlert size={13} class="mt-0.5 shrink-0 text-destructive" />
|
|
329
320
|
<p class="text-xs leading-snug text-destructive">{urlError.message}</p>
|
|
330
321
|
</div>
|
|
331
|
-
{#if urlError.isCors &&
|
|
332
|
-
<p class="text-xs leading-snug pl-5 text-muted-foreground">{
|
|
322
|
+
{#if urlError.isCors && urlError.hint}
|
|
323
|
+
<p class="text-xs leading-snug pl-5 text-muted-foreground">{urlError.hint}</p>
|
|
333
324
|
{/if}
|
|
334
325
|
</div>
|
|
335
326
|
{/if}
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
+
import { type Component } from 'svelte';
|
|
2
|
+
import { type FooterItem } from '../contexts/footerContext.svelte';
|
|
3
|
+
/** Config for a footer-item registration; generic over the component's props. */
|
|
4
|
+
export type FooterItemConfig<P extends Record<string, unknown> = Record<string, unknown>> = Omit<FooterItem<P>, 'position' | 'priority'> & Partial<Pick<FooterItem<P>, 'position' | 'priority'>>;
|
|
1
5
|
/**
|
|
2
|
-
* Register a footer item
|
|
3
|
-
* The `getProps` function is called on every render, so returning reactive state
|
|
4
|
-
* (e.g. `() => ({ status: myState.status })`) will keep the footer in sync.
|
|
6
|
+
* Register a footer item for the lifetime of the calling component.
|
|
5
7
|
*
|
|
6
|
-
* Registration is deferred to onMount (client-side only) to avoid SSR context errors
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* @param component - Svelte component to render
|
|
10
|
-
* @param getProps - Function returning current props (called reactively)
|
|
11
|
-
* @param position - Where to display ('left' or 'right'), defaults to 'left'
|
|
12
|
-
* @param priority - Higher values render first within their position
|
|
13
|
-
* @param onClick - Optional click handler (e.g. open a popup/dialog)
|
|
8
|
+
* Registration is deferred to onMount (client-side only) to avoid SSR context errors;
|
|
9
|
+
* it's unregistered on destroy. A no-op when no footer context is present (e.g. a
|
|
10
|
+
* component rendered outside the root layout) or no component is supplied.
|
|
14
11
|
*/
|
|
15
|
-
export declare function useFooterItem
|
|
12
|
+
export declare function useFooterItem<P extends Record<string, unknown>>(config: FooterItemConfig<P>): void;
|
|
13
|
+
export type { Component };
|
|
@@ -1,33 +1,26 @@
|
|
|
1
1
|
import { getContext, onMount, onDestroy } from 'svelte';
|
|
2
2
|
import { FOOTER_CONTEXT_KEY } from '../contexts/footerContext.svelte';
|
|
3
3
|
/**
|
|
4
|
-
* Register a footer item
|
|
5
|
-
* The `getProps` function is called on every render, so returning reactive state
|
|
6
|
-
* (e.g. `() => ({ status: myState.status })`) will keep the footer in sync.
|
|
4
|
+
* Register a footer item for the lifetime of the calling component.
|
|
7
5
|
*
|
|
8
|
-
* Registration is deferred to onMount (client-side only) to avoid SSR context errors
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @param component - Svelte component to render
|
|
12
|
-
* @param getProps - Function returning current props (called reactively)
|
|
13
|
-
* @param position - Where to display ('left' or 'right'), defaults to 'left'
|
|
14
|
-
* @param priority - Higher values render first within their position
|
|
15
|
-
* @param onClick - Optional click handler (e.g. open a popup/dialog)
|
|
6
|
+
* Registration is deferred to onMount (client-side only) to avoid SSR context errors;
|
|
7
|
+
* it's unregistered on destroy. A no-op when no footer context is present (e.g. a
|
|
8
|
+
* component rendered outside the root layout) or no component is supplied.
|
|
16
9
|
*/
|
|
17
|
-
export function useFooterItem(
|
|
10
|
+
export function useFooterItem(config) {
|
|
18
11
|
onMount(() => {
|
|
19
12
|
const store = getContext(FOOTER_CONTEXT_KEY);
|
|
20
|
-
if (!store || !component)
|
|
13
|
+
if (!store || !config.component)
|
|
21
14
|
return;
|
|
22
|
-
store.register(
|
|
15
|
+
store.register(config);
|
|
23
16
|
});
|
|
24
17
|
onDestroy(() => {
|
|
25
18
|
try {
|
|
26
19
|
const store = getContext(FOOTER_CONTEXT_KEY);
|
|
27
|
-
store?.unregister(id);
|
|
20
|
+
store?.unregister(config.id);
|
|
28
21
|
}
|
|
29
22
|
catch {
|
|
30
|
-
// Context may not exist during SSR cleanup — safe to ignore
|
|
23
|
+
// Context may not exist during SSR cleanup — safe to ignore.
|
|
31
24
|
}
|
|
32
25
|
});
|
|
33
26
|
}
|
|
@@ -30,8 +30,11 @@ export function createComputeThrottle(computeFn, options = {}) {
|
|
|
30
30
|
await computeFn(values, signal);
|
|
31
31
|
}
|
|
32
32
|
catch (err) {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// AbortError is expected (timeout or cancel). Non-abort errors must be
|
|
34
|
+
// handled inside computeFn — re-throwing here would produce an unhandled
|
|
35
|
+
// rejection because executeCompute is always called fire-and-forget.
|
|
36
|
+
if (!(err instanceof Error) || (err.name !== 'AbortError' && err.name !== 'TimeoutError')) {
|
|
37
|
+
console.error('[computeThrottle] unhandled error in computeFn:', err);
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
finally {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { UISchema } from '@selvajs/schemas';
|
|
2
|
+
import type { SolveFn, SolveResult } from '../types/solveFn';
|
|
3
|
+
/**
|
|
4
|
+
* The transport behind a Solve Session. Knows how to start and cancel a solve and
|
|
5
|
+
* reports its in-flight state. It does NOT return outputs — those come back via the
|
|
6
|
+
* session's report() so push transports (WebSocket) fit without contortion.
|
|
7
|
+
*/
|
|
8
|
+
export interface SolveDriver {
|
|
9
|
+
solve(values: Record<string, unknown>): void;
|
|
10
|
+
cancel(): void;
|
|
11
|
+
readonly isSolving: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface SolveSession {
|
|
14
|
+
readonly values: Record<string, unknown>;
|
|
15
|
+
readonly error: string;
|
|
16
|
+
readonly computeErrors: string[];
|
|
17
|
+
readonly computeWarnings: string[];
|
|
18
|
+
readonly meshes: unknown[];
|
|
19
|
+
readonly hasPendingChanges: boolean;
|
|
20
|
+
readonly hasNeverSolved: boolean;
|
|
21
|
+
readonly isSolving: boolean;
|
|
22
|
+
/** Records a value change and dispatches a solve unless the schema is manual-solve. */
|
|
23
|
+
setValue(id: string, value: unknown): void;
|
|
24
|
+
/** Explicit "calculate" — dispatches a solve with the current values. */
|
|
25
|
+
solve(): void;
|
|
26
|
+
/** Merges incoming values, then solves (auto) or marks dirty (manual). */
|
|
27
|
+
loadValues(incoming: Record<string, unknown>): void;
|
|
28
|
+
/** Re-seed for a new active definition: rebuild values, clear outputs, gate the solve. */
|
|
29
|
+
rebuild(schema: UISchema, scopeKey: string): void;
|
|
30
|
+
/** Feed a completed solve result back into the session (called by the driver/host). */
|
|
31
|
+
report(result: SolveResult): void;
|
|
32
|
+
/** Report a solve failure (transport/solver error) — surfaces in `error`. */
|
|
33
|
+
reportError(message: string): void;
|
|
34
|
+
}
|
|
35
|
+
export interface SolveSessionArgs {
|
|
36
|
+
schema: UISchema;
|
|
37
|
+
scopeKey: string;
|
|
38
|
+
driver: SolveDriver;
|
|
39
|
+
}
|
|
40
|
+
export declare function createSolveSession(args: SolveSessionArgs): SolveSession;
|
|
41
|
+
/** The slice of a SolveSession a driver feeds completed/failed solves back into. */
|
|
42
|
+
export interface SolveReporter {
|
|
43
|
+
report(result: SolveResult): void;
|
|
44
|
+
reportError(message: string): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Request/response Solve Driver: wraps createComputeThrottle around a SolveFn and feeds
|
|
48
|
+
* the resolved result back through the reporter. One solve in flight at a time; the
|
|
49
|
+
* latest triggered values win. Used by ComputeApp (Rhino.Compute over HTTP).
|
|
50
|
+
*
|
|
51
|
+
* Because the session and driver reference each other, the host passes the reporter
|
|
52
|
+
* lazily (`() => session`) so it can construct the session with the driver in hand.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createRequestResponseDriver(onSolve: SolveFn, getReporter: () => SolveReporter, options?: {
|
|
55
|
+
timeout?: number;
|
|
56
|
+
}): SolveDriver;
|