@selvajs/ui 6.6.0-beta.4 → 6.6.0-beta.6
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/ErrorScreen.svelte +6 -2
- package/dist/components/compute/AppLayout.svelte +163 -74
- package/dist/components/compute/AppLayout.svelte.d.ts +11 -0
- package/dist/components/compute/ComputeApp.svelte +40 -20
- package/dist/components/compute/ComputeApp.svelte.d.ts +19 -2
- package/dist/components/compute/ComputeMessagesDialog.svelte +30 -40
- package/dist/components/compute/ParameterPresetManager.svelte +7 -44
- package/dist/components/compute/ParameterPresetManager.svelte.d.ts +4 -5
- package/dist/components/compute/SolveMessages.svelte +30 -37
- package/dist/components/compute/SolveMessages.svelte.d.ts +0 -12
- package/dist/components/layout/PageFooter.svelte +21 -11
- package/dist/components/preview/ChartOutput.svelte +17 -8
- package/dist/components/preview/Group.svelte +33 -18
- package/dist/components/preview/HelpButton.svelte +4 -2
- package/dist/components/preview/ImageOutput.svelte +427 -68
- package/dist/components/preview/InputControl.svelte +11 -2
- package/dist/components/preview/OutputDisplay.svelte +17 -12
- package/dist/components/preview/TabContent.svelte +3 -0
- package/dist/components/preview/inputs/ColorInput.svelte +3 -1
- package/dist/components/preview/inputs/ColorInput.svelte.d.ts +1 -0
- package/dist/components/preview/inputs/DropdownInput.svelte +4 -1
- package/dist/components/preview/inputs/FileInput.svelte +50 -34
- package/dist/components/preview/inputs/NumberInput.svelte +92 -10
- package/dist/components/preview/inputs/NumberInput.svelte.d.ts +2 -0
- package/dist/components/preview/inputs/TextInput.svelte +8 -3
- package/dist/components/primitives/CalculateButton.svelte +49 -23
- package/dist/components/primitives/CalculateButton.svelte.d.ts +3 -0
- package/dist/components/primitives/SolvingIndicator.svelte +21 -17
- package/dist/components/primitives/badge/badge.svelte +2 -2
- package/dist/components/primitives/badge/badge.svelte.d.ts +4 -4
- package/dist/components/primitives/button/button.svelte +2 -2
- package/dist/components/primitives/button/button.svelte.d.ts +4 -4
- package/dist/components/primitives/checkbox/checkbox.svelte +1 -1
- package/dist/components/primitives/dialog/dialog-content.svelte +6 -2
- package/dist/components/primitives/input/input.svelte +2 -2
- package/dist/components/primitives/input-group/input-group.svelte +1 -1
- package/dist/components/primitives/mode-toggle/mode-toggle.svelte +4 -1
- package/dist/components/primitives/scroll-area/scroll-area.svelte +1 -1
- package/dist/components/primitives/search/search.svelte +1 -1
- package/dist/components/primitives/select/select-trigger.svelte +1 -1
- package/dist/components/primitives/slider/slider.svelte +7 -2
- package/dist/components/primitives/slider/slider.svelte.d.ts +5 -1
- package/dist/components/primitives/switch/switch.svelte +1 -1
- package/dist/components/primitives/tabs/tabs-trigger.svelte +1 -1
- package/dist/components/primitives/tabs/tabs-trigger.svelte.d.ts +1 -1
- package/dist/components/primitives/textarea/textarea.svelte +1 -1
- package/dist/components/primitives/toggle/toggle.svelte +1 -1
- package/dist/components/primitives/toggle/toggle.svelte.d.ts +1 -1
- package/dist/components/viewer/Viewer.svelte +20 -17
- package/dist/compute/run-panel-action.d.ts +8 -0
- package/dist/compute/run-panel-action.js +12 -0
- package/dist/compute/useSolveSession.svelte.js +3 -0
- package/dist/i18n/localeContext.svelte.d.ts +10 -3
- package/dist/i18n/localeContext.svelte.js +27 -5
- package/dist/i18n/messages.d.ts +115 -2
- package/dist/i18n/messages.js +199 -11
- package/dist/index.d.ts +1 -1
- package/dist/public.d.ts +1 -1
- package/dist/schema/visibility-rules.js +21 -0
- package/dist/styles/base.css +9 -6
- package/dist/types/actionButton.d.ts +17 -3
- package/dist/types/presetLabels.d.ts +3 -2
- package/dist/types/presetLabels.js +9 -8
- package/dist/utils/loadScript.d.ts +2 -0
- package/dist/utils/loadScript.js +2 -0
- package/package.json +8 -8
- package/src/lib/components/ErrorScreen.svelte +6 -2
- package/src/lib/components/compute/AppLayout.svelte +163 -74
- package/src/lib/components/compute/ComputeApp.svelte +40 -20
- package/src/lib/components/compute/ComputeMessagesDialog.svelte +30 -40
- package/src/lib/components/compute/ParameterPresetManager.svelte +7 -44
- package/src/lib/components/compute/SolveMessages.svelte +30 -37
- package/src/lib/components/layout/PageFooter.svelte +21 -11
- package/src/lib/components/preview/ChartOutput.svelte +17 -8
- package/src/lib/components/preview/Group.svelte +33 -18
- package/src/lib/components/preview/HelpButton.svelte +4 -2
- package/src/lib/components/preview/ImageOutput.svelte +427 -68
- package/src/lib/components/preview/InputControl.svelte +11 -2
- package/src/lib/components/preview/OutputDisplay.svelte +17 -12
- package/src/lib/components/preview/TabContent.svelte +3 -0
- package/src/lib/components/preview/inputs/ColorInput.svelte +3 -1
- package/src/lib/components/preview/inputs/DropdownInput.svelte +4 -1
- package/src/lib/components/preview/inputs/FileInput.svelte +50 -34
- package/src/lib/components/preview/inputs/NumberInput.svelte +92 -10
- package/src/lib/components/preview/inputs/TextInput.svelte +8 -3
- package/src/lib/components/primitives/CalculateButton.svelte +49 -23
- package/src/lib/components/primitives/SolvingIndicator.svelte +21 -17
- package/src/lib/components/primitives/badge/badge.svelte +2 -2
- package/src/lib/components/primitives/button/button.svelte +2 -2
- package/src/lib/components/primitives/checkbox/checkbox.svelte +1 -1
- package/src/lib/components/primitives/dialog/dialog-content.svelte +6 -2
- package/src/lib/components/primitives/input/input.svelte +2 -2
- package/src/lib/components/primitives/input-group/input-group.svelte +1 -1
- package/src/lib/components/primitives/mode-toggle/mode-toggle.svelte +4 -1
- package/src/lib/components/primitives/scroll-area/scroll-area.svelte +1 -1
- package/src/lib/components/primitives/search/search.svelte +1 -1
- package/src/lib/components/primitives/select/select-trigger.svelte +1 -1
- package/src/lib/components/primitives/slider/slider.svelte +7 -2
- package/src/lib/components/primitives/switch/switch.svelte +1 -1
- package/src/lib/components/primitives/tabs/tabs-trigger.svelte +1 -1
- package/src/lib/components/primitives/textarea/textarea.svelte +1 -1
- package/src/lib/components/primitives/toggle/toggle.svelte +1 -1
- package/src/lib/components/viewer/Viewer.svelte +20 -17
- package/src/lib/compute/run-panel-action.ts +16 -0
- package/src/lib/compute/useSolveSession.svelte.ts +3 -0
- package/src/lib/i18n/localeContext.svelte.ts +40 -5
- package/src/lib/i18n/messages.ts +351 -13
- package/src/lib/index.ts +1 -0
- package/src/lib/public.ts +1 -0
- package/src/lib/schema/visibility-rules.ts +23 -0
- package/src/lib/styles/base.css +9 -6
- package/src/lib/types/actionButton.ts +16 -3
- package/src/lib/types/presetLabels.ts +13 -11
- package/src/lib/utils/loadScript.ts +3 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as Card from './primitives/card/index.js';
|
|
4
4
|
import { Button } from './primitives/button/index.js';
|
|
5
5
|
import Callout from './primitives/Callout.svelte';
|
|
6
|
+
import { getLocaleContext } from '$lib/i18n/localeContext.svelte';
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
status?: number;
|
|
@@ -12,6 +13,9 @@
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
let { status = 500, message, details, homeLink = '/' }: Props = $props();
|
|
16
|
+
|
|
17
|
+
const locale = getLocaleContext();
|
|
18
|
+
const t = $derived(locale.messages);
|
|
15
19
|
</script>
|
|
16
20
|
|
|
17
21
|
<div class="p-4 flex min-h-screen items-center justify-center bg-background">
|
|
@@ -26,7 +30,7 @@
|
|
|
26
30
|
</Card.Header>
|
|
27
31
|
<Card.Content>
|
|
28
32
|
<div class="gap-4 flex flex-col">
|
|
29
|
-
<Callout tone="danger" title=
|
|
33
|
+
<Callout tone="danger" title={t.errorTitle}>
|
|
30
34
|
{#if message}{message}{/if}
|
|
31
35
|
</Callout>
|
|
32
36
|
{#if details}
|
|
@@ -37,7 +41,7 @@
|
|
|
37
41
|
</div>
|
|
38
42
|
</Card.Content>
|
|
39
43
|
<Card.Footer>
|
|
40
|
-
<Button variant="outline" href={homeLink} rel="external" class="w-full">
|
|
44
|
+
<Button variant="outline" href={homeLink} rel="external" class="w-full">{t.goHome}</Button>
|
|
41
45
|
</Card.Footer>
|
|
42
46
|
</Card.Root>
|
|
43
47
|
</div>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { UISchema, SupportedTypes, ParameterPreset } from '@selvajs/schemas';
|
|
3
|
+
import type { SolveSettlement } from '@selvajs/solve/client';
|
|
3
4
|
import type { ActionButton } from '../../types/actionButton';
|
|
4
5
|
import type { PresetLabels } from '../../types/presetLabels';
|
|
5
|
-
import { ChevronUp } from '@lucide/svelte';
|
|
6
|
+
import { ChevronUp, Download, Ellipsis, Upload } from '@lucide/svelte';
|
|
6
7
|
import type { ThreeViewer } from '@selvajs/visualization/render';
|
|
7
8
|
import Viewer, { type ViewerConfig } from '../viewer/Viewer.svelte';
|
|
8
9
|
import CalculateButton from '../primitives/CalculateButton.svelte';
|
|
@@ -10,10 +11,13 @@
|
|
|
10
11
|
import TabLayout from '../preview/TabLayout.svelte';
|
|
11
12
|
import CollapsedPanelStrip from './CollapsedPanelStrip.svelte';
|
|
12
13
|
import * as Resizable from '$lib/components/primitives/resizable';
|
|
14
|
+
import * as DropdownMenu from '$lib/components/primitives/dropdown-menu/index.js';
|
|
13
15
|
import { Button } from '$lib';
|
|
14
16
|
import ParameterPresetManager from './ParameterPresetManager.svelte';
|
|
17
|
+
import { runPanelAction } from '../../compute/run-panel-action';
|
|
15
18
|
import { getLocaleContext } from '../../i18n/localeContext.svelte';
|
|
16
19
|
import { buildVisibilityMap, isTabVisible, itemKey } from '../../schema/visibility-rules';
|
|
20
|
+
import { DEFAULT_PRESET_LABELS } from '../../types/presetLabels';
|
|
17
21
|
|
|
18
22
|
const locale = getLocaleContext();
|
|
19
23
|
const t = $derived(locale.messages);
|
|
@@ -22,6 +26,11 @@
|
|
|
22
26
|
|
|
23
27
|
interface Props {
|
|
24
28
|
schema: UISchema;
|
|
29
|
+
/**
|
|
30
|
+
* Whether to draw the 3D viewer. Each host reads its own switch: the plugin's preview
|
|
31
|
+
* `viewerOptions.enableLocal`, a Rhino.Compute host `viewerOptions.enableRemote`.
|
|
32
|
+
*/
|
|
33
|
+
showViewer: boolean;
|
|
25
34
|
meshes?: any[];
|
|
26
35
|
isSolving: boolean;
|
|
27
36
|
showSolvingIndicator?: boolean;
|
|
@@ -29,6 +38,11 @@
|
|
|
29
38
|
hasNeverSolved?: boolean;
|
|
30
39
|
isViewerFullscreen?: boolean;
|
|
31
40
|
oncalculate?: () => void;
|
|
41
|
+
/**
|
|
42
|
+
* The result for the current inputs, solving if it must (`session.ensureSolved`). Needed
|
|
43
|
+
* by `solvesFirst` actions; without it they run straight away.
|
|
44
|
+
*/
|
|
45
|
+
ensureSolved?: () => Promise<SolveSettlement>;
|
|
32
46
|
values: Record<string, unknown>;
|
|
33
47
|
onValueChange: (id: string, val: SupportedTypes, forceSolve?: boolean) => void | Promise<void>;
|
|
34
48
|
onLoadValues?: (values: Record<string, unknown>) => void | Promise<void>;
|
|
@@ -45,6 +59,7 @@
|
|
|
45
59
|
|
|
46
60
|
let {
|
|
47
61
|
schema,
|
|
62
|
+
showViewer: hasViewer,
|
|
48
63
|
meshes = [],
|
|
49
64
|
isSolving,
|
|
50
65
|
showSolvingIndicator = false,
|
|
@@ -52,6 +67,7 @@
|
|
|
52
67
|
hasNeverSolved = false,
|
|
53
68
|
isViewerFullscreen = $bindable(false),
|
|
54
69
|
oncalculate = () => {},
|
|
70
|
+
ensureSolved,
|
|
55
71
|
values,
|
|
56
72
|
onValueChange,
|
|
57
73
|
onLoadValues,
|
|
@@ -66,9 +82,11 @@
|
|
|
66
82
|
onViewerReady
|
|
67
83
|
}: Props = $props();
|
|
68
84
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
);
|
|
85
|
+
const presetText = $derived({ ...DEFAULT_PRESET_LABELS, ...presetLabels });
|
|
86
|
+
let presets = $state<ParameterPresetManager | null>(null);
|
|
87
|
+
const hasMenu = $derived(showSaveButton || showLoadButton || panelActions.length > 0);
|
|
88
|
+
const resultIsStale = $derived(hasPendingChanges || hasNeverSolved);
|
|
89
|
+
|
|
72
90
|
const shownTabs = $derived(
|
|
73
91
|
schema.layout.type === 'tabbed'
|
|
74
92
|
? schema.layout.tabs.filter((tab) => isTabVisible(tab, values))
|
|
@@ -151,54 +169,110 @@
|
|
|
151
169
|
}
|
|
152
170
|
</script>
|
|
153
171
|
|
|
172
|
+
<!-- Everything but Calculate: saving and loading inputs, then the host's own actions. -->
|
|
173
|
+
{#snippet actionsMenu()}
|
|
174
|
+
<DropdownMenu.Root>
|
|
175
|
+
<DropdownMenu.Trigger>
|
|
176
|
+
{#snippet child({ props })}
|
|
177
|
+
<Button
|
|
178
|
+
{...props}
|
|
179
|
+
variant="outline"
|
|
180
|
+
size="icon-lg"
|
|
181
|
+
class="shrink-0"
|
|
182
|
+
title={t.moreActions}
|
|
183
|
+
aria-label={t.moreActions}
|
|
184
|
+
>
|
|
185
|
+
<Ellipsis />
|
|
186
|
+
</Button>
|
|
187
|
+
{/snippet}
|
|
188
|
+
</DropdownMenu.Trigger>
|
|
189
|
+
<!-- Above the fullscreen viewer, which sits at z-index 10000. -->
|
|
190
|
+
<DropdownMenu.Content side="top" align="end" sideOffset={6} class="min-w-56 z-10001 w-auto">
|
|
191
|
+
{#if showSaveButton}
|
|
192
|
+
<DropdownMenu.Item onSelect={() => presets?.openSave()}>
|
|
193
|
+
<Download />
|
|
194
|
+
{presetText.saveMenuItem}
|
|
195
|
+
</DropdownMenu.Item>
|
|
196
|
+
{/if}
|
|
197
|
+
{#if showLoadButton}
|
|
198
|
+
<DropdownMenu.Item onSelect={() => presets?.openLoad()}>
|
|
199
|
+
<Upload />
|
|
200
|
+
{presetText.loadMenuItem}
|
|
201
|
+
</DropdownMenu.Item>
|
|
202
|
+
{/if}
|
|
203
|
+
{#if (showSaveButton || showLoadButton) && panelActions.length > 0}
|
|
204
|
+
<DropdownMenu.Separator />
|
|
205
|
+
{/if}
|
|
206
|
+
{#each panelActions as action (action.id)}
|
|
207
|
+
{@const hint =
|
|
208
|
+
action.hint ?? (action.solvesFirst && resultIsStale ? t.calculatesFirst : undefined)}
|
|
209
|
+
<DropdownMenu.Item
|
|
210
|
+
disabled={action.disabled}
|
|
211
|
+
variant={action.variant ?? 'default'}
|
|
212
|
+
onSelect={() => runPanelAction(action, ensureSolved)}
|
|
213
|
+
class="items-start"
|
|
214
|
+
>
|
|
215
|
+
{#if action.icon}
|
|
216
|
+
{@const IconComponent = action.icon}
|
|
217
|
+
<IconComponent class="mt-0.5" />
|
|
218
|
+
{/if}
|
|
219
|
+
<span class="flex flex-col">
|
|
220
|
+
{action.label}
|
|
221
|
+
{#if hint}
|
|
222
|
+
<span class="text-xs leading-snug text-muted-foreground">{hint}</span>
|
|
223
|
+
{/if}
|
|
224
|
+
</span>
|
|
225
|
+
</DropdownMenu.Item>
|
|
226
|
+
{/each}
|
|
227
|
+
</DropdownMenu.Content>
|
|
228
|
+
</DropdownMenu.Root>
|
|
229
|
+
{/snippet}
|
|
230
|
+
|
|
231
|
+
<!-- One primary action and one menu. A definition that solves on every change has no Calculate,
|
|
232
|
+
so the menu sits alone at the end of the row. -->
|
|
233
|
+
{#snippet footer()}
|
|
234
|
+
{#if schema.instanceSolve === false}
|
|
235
|
+
<CalculateButton
|
|
236
|
+
{hasPendingChanges}
|
|
237
|
+
{hasNeverSolved}
|
|
238
|
+
{isSolving}
|
|
239
|
+
{oncalculate}
|
|
240
|
+
trailing={hasMenu ? actionsMenu : undefined}
|
|
241
|
+
/>
|
|
242
|
+
{:else if hasMenu}
|
|
243
|
+
<div class="mt-3 pb-3 pt-2 px-2 flex justify-end border-t border-border/50">
|
|
244
|
+
{@render actionsMenu()}
|
|
245
|
+
</div>
|
|
246
|
+
{/if}
|
|
247
|
+
{/snippet}
|
|
248
|
+
|
|
249
|
+
<!-- `hostsFooter` marks the one panel that carries the footer. On a phone the footer moves to the
|
|
250
|
+
drawer's pinned bottom instead. -->
|
|
154
251
|
{#snippet panelContent(
|
|
155
252
|
panelFilter: 'left' | 'right' | undefined,
|
|
156
253
|
requestedTabId: string | null,
|
|
157
|
-
|
|
158
|
-
showCalculateButton = true
|
|
254
|
+
hostsFooter = true
|
|
159
255
|
)}
|
|
160
256
|
<div class="panel-content-wrapper">
|
|
161
257
|
{#if schema.layout.type === 'tabbed'}
|
|
162
258
|
<TabLayout {schema} {values} {onValueChange} {panelFilter} {requestedTabId} />
|
|
163
259
|
{/if}
|
|
164
|
-
{#if
|
|
260
|
+
{#if hostsFooter && !isMobile}
|
|
165
261
|
<div class="panel-footer px-1">
|
|
166
|
-
|
|
167
|
-
{#if showParameterStateManager}
|
|
168
|
-
<ParameterPresetManager
|
|
169
|
-
{schema}
|
|
170
|
-
currentValues={values}
|
|
171
|
-
onLoadValues={handleLoadValues}
|
|
172
|
-
{showSaveButton}
|
|
173
|
-
{showLoadButton}
|
|
174
|
-
{onSaveState}
|
|
175
|
-
{onListStates}
|
|
176
|
-
labels={presetLabels}
|
|
177
|
-
/>
|
|
178
|
-
{/if}
|
|
179
|
-
{#each panelActions as action (action.id)}
|
|
180
|
-
<Button
|
|
181
|
-
variant={action.variant ?? 'outline'}
|
|
182
|
-
size={action.size ?? 'sm'}
|
|
183
|
-
onclick={action.onclick}
|
|
184
|
-
>
|
|
185
|
-
{#if action.icon}
|
|
186
|
-
{@const IconComponent = action.icon}
|
|
187
|
-
<IconComponent class="mr-2 h-4 w-4" />
|
|
188
|
-
{/if}
|
|
189
|
-
{action.label}
|
|
190
|
-
</Button>
|
|
191
|
-
{/each}
|
|
192
|
-
</div>
|
|
193
|
-
{#if !isMobile && showCalculateButton && schema.instanceSolve === false}
|
|
194
|
-
<CalculateButton {hasPendingChanges} {hasNeverSolved} {isSolving} {oncalculate} />
|
|
195
|
-
{/if}
|
|
262
|
+
{@render footer()}
|
|
196
263
|
</div>
|
|
197
264
|
{/if}
|
|
198
265
|
</div>
|
|
199
266
|
{/snippet}
|
|
200
267
|
|
|
201
|
-
|
|
268
|
+
<!-- Pinned under the scrolling fields, so the primary action never sits at the end of a long list. -->
|
|
269
|
+
{#snippet drawerFooter(isInert: boolean)}
|
|
270
|
+
<div class="drawer-footer" inert={isInert}>
|
|
271
|
+
{@render footer()}
|
|
272
|
+
</div>
|
|
273
|
+
{/snippet}
|
|
274
|
+
|
|
275
|
+
<main
|
|
202
276
|
data-layout-root
|
|
203
277
|
class="min-h-0 sm:flex-row sm:py-(--page-py) flex flex-1 flex-col overflow-hidden {leftCollapsed
|
|
204
278
|
? 'sm:pl-0'
|
|
@@ -230,12 +304,12 @@
|
|
|
230
304
|
class:drawer-open={drawerOpen}
|
|
231
305
|
class:drawer-closed={!drawerOpen}
|
|
232
306
|
>
|
|
307
|
+
<!-- Named by its visible label; aria-expanded carries the open/closed state. -->
|
|
233
308
|
<button
|
|
234
309
|
class="drawer-handle-bar"
|
|
235
310
|
onclick={() => (drawerOpen = !drawerOpen)}
|
|
236
311
|
ontouchstart={onDrawerTouchStart}
|
|
237
312
|
ontouchend={onDrawerTouchEnd}
|
|
238
|
-
aria-label={drawerOpen ? t.collapsePanel : t.expandPanel}
|
|
239
313
|
aria-expanded={drawerOpen}
|
|
240
314
|
>
|
|
241
315
|
<div class="drawer-pill"></div>
|
|
@@ -248,62 +322,47 @@
|
|
|
248
322
|
/>
|
|
249
323
|
</button>
|
|
250
324
|
|
|
251
|
-
|
|
325
|
+
<!-- Inert while closed: the fields are clipped out of view, and tabbing into them
|
|
326
|
+
would scroll the drawer's insides around behind the handle. -->
|
|
327
|
+
<div class="drawer-content" inert={!drawerOpen}>
|
|
252
328
|
<div class="px-3 pb-6">
|
|
253
329
|
{#if hasLeftPanel}
|
|
254
|
-
{@render panelContent(
|
|
255
|
-
hasRightPanel ? 'left' : undefined,
|
|
256
|
-
requestedLeftTabId,
|
|
257
|
-
!hasRightPanel,
|
|
258
|
-
false
|
|
259
|
-
)}
|
|
330
|
+
{@render panelContent(hasRightPanel ? 'left' : undefined, requestedLeftTabId, false)}
|
|
260
331
|
{/if}
|
|
261
332
|
{#if hasRightPanel}
|
|
262
|
-
|
|
333
|
+
<!-- Decorative: the section below names itself with a heading. -->
|
|
334
|
+
<div class="my-4 gap-2 flex items-center" aria-hidden="true">
|
|
263
335
|
<div class="h-px flex-1 bg-border"></div>
|
|
264
336
|
<span class="text-xs font-medium px-1 text-muted-foreground">
|
|
265
337
|
{rightTabs[0]?.label ?? t.moreTab}
|
|
266
338
|
</span>
|
|
267
339
|
<div class="h-px flex-1 bg-border"></div>
|
|
268
340
|
</div>
|
|
269
|
-
{@render panelContent('right', requestedRightTabId,
|
|
341
|
+
{@render panelContent('right', requestedRightTabId, false)}
|
|
270
342
|
{/if}
|
|
271
343
|
</div>
|
|
272
344
|
</div>
|
|
273
345
|
|
|
274
|
-
{
|
|
275
|
-
<div class="drawer-footer">
|
|
276
|
-
<CalculateButton {hasPendingChanges} {hasNeverSolved} {isSolving} {oncalculate} />
|
|
277
|
-
</div>
|
|
278
|
-
{/if}
|
|
346
|
+
{@render drawerFooter(!drawerOpen)}
|
|
279
347
|
</div>
|
|
280
348
|
{:else}
|
|
281
349
|
<div class="flex flex-1 flex-col overflow-hidden">
|
|
282
350
|
<div class="px-3 pt-3 flex-1 overflow-y-auto">
|
|
283
351
|
{#if hasLeftPanel}
|
|
284
|
-
{@render panelContent(
|
|
285
|
-
hasRightPanel ? 'left' : undefined,
|
|
286
|
-
requestedLeftTabId,
|
|
287
|
-
!hasRightPanel,
|
|
288
|
-
false
|
|
289
|
-
)}
|
|
352
|
+
{@render panelContent(hasRightPanel ? 'left' : undefined, requestedLeftTabId, false)}
|
|
290
353
|
{/if}
|
|
291
354
|
{#if hasRightPanel}
|
|
292
|
-
<div class="my-4 gap-2 flex items-center">
|
|
355
|
+
<div class="my-4 gap-2 flex items-center" aria-hidden="true">
|
|
293
356
|
<div class="h-px flex-1 bg-border"></div>
|
|
294
357
|
<span class="text-xs font-medium px-1 text-muted-foreground">
|
|
295
358
|
{rightTabs[0]?.label ?? t.moreTab}
|
|
296
359
|
</span>
|
|
297
360
|
<div class="h-px flex-1 bg-border"></div>
|
|
298
361
|
</div>
|
|
299
|
-
{@render panelContent('right', requestedRightTabId,
|
|
362
|
+
{@render panelContent('right', requestedRightTabId, false)}
|
|
300
363
|
{/if}
|
|
301
364
|
</div>
|
|
302
|
-
{
|
|
303
|
-
<div class="drawer-footer">
|
|
304
|
-
<CalculateButton {hasPendingChanges} {hasNeverSolved} {isSolving} {oncalculate} />
|
|
305
|
-
</div>
|
|
306
|
-
{/if}
|
|
365
|
+
{@render drawerFooter(false)}
|
|
307
366
|
</div>
|
|
308
367
|
{/if}
|
|
309
368
|
{:else if !hasViewer && !isTwoPanelMode && hasLeftPanel !== hasRightPanel}
|
|
@@ -312,9 +371,7 @@
|
|
|
312
371
|
<div class="min-h-0 flex h-full w-4/5 flex-col">
|
|
313
372
|
{@render panelContent(
|
|
314
373
|
hasRightPanel ? 'right' : undefined,
|
|
315
|
-
hasRightPanel ? requestedRightTabId : requestedLeftTabId
|
|
316
|
-
!hasRightPanel,
|
|
317
|
-
!hasRightPanel
|
|
374
|
+
hasRightPanel ? requestedRightTabId : requestedLeftTabId
|
|
318
375
|
)}
|
|
319
376
|
</div>
|
|
320
377
|
</div>
|
|
@@ -412,7 +469,7 @@
|
|
|
412
469
|
? 'hidden'
|
|
413
470
|
: ''}"
|
|
414
471
|
>
|
|
415
|
-
{@render panelContent('right', requestedRightTabId, !hasLeftPanel
|
|
472
|
+
{@render panelContent('right', requestedRightTabId, !hasLeftPanel)}
|
|
416
473
|
</div>
|
|
417
474
|
</Resizable.Pane>
|
|
418
475
|
{/if}
|
|
@@ -432,9 +489,20 @@
|
|
|
432
489
|
{/if}
|
|
433
490
|
</div>
|
|
434
491
|
{/if}
|
|
435
|
-
</div>
|
|
436
492
|
|
|
437
|
-
<SolvingIndicator show={showSolvingIndicator} />
|
|
493
|
+
<SolvingIndicator show={showSolvingIndicator} />
|
|
494
|
+
</main>
|
|
495
|
+
|
|
496
|
+
<!-- The save, load and validation dialogs; the footer's menu opens them. -->
|
|
497
|
+
<ParameterPresetManager
|
|
498
|
+
bind:this={presets}
|
|
499
|
+
{schema}
|
|
500
|
+
currentValues={values}
|
|
501
|
+
onLoadValues={handleLoadValues}
|
|
502
|
+
{onSaveState}
|
|
503
|
+
{onListStates}
|
|
504
|
+
labels={presetLabels}
|
|
505
|
+
/>
|
|
438
506
|
|
|
439
507
|
<style>
|
|
440
508
|
.fullscreen-layout {
|
|
@@ -480,8 +548,10 @@
|
|
|
480
548
|
.drawer-closed {
|
|
481
549
|
height: 60px;
|
|
482
550
|
}
|
|
551
|
+
/* The pinned footer takes a fixed slice, so a short phone opens the drawer further to leave
|
|
552
|
+
room for the fields; a tall one keeps it at 60% of the screen. */
|
|
483
553
|
.drawer-open {
|
|
484
|
-
height: 60svh;
|
|
554
|
+
height: max(60svh, min(30rem, 100svh - 10rem));
|
|
485
555
|
}
|
|
486
556
|
|
|
487
557
|
.drawer-handle-bar {
|
|
@@ -524,6 +594,25 @@
|
|
|
524
594
|
.drawer-footer {
|
|
525
595
|
flex-shrink: 0;
|
|
526
596
|
padding: 0rem 1rem;
|
|
527
|
-
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/* Touch gets 44px targets on the panel's controls; a fine pointer keeps the denser sizes. */
|
|
600
|
+
@media (pointer: coarse) {
|
|
601
|
+
.panel-content-wrapper
|
|
602
|
+
:global(
|
|
603
|
+
:is(
|
|
604
|
+
[data-slot='button'],
|
|
605
|
+
[data-slot='input'],
|
|
606
|
+
[data-slot='select-trigger'],
|
|
607
|
+
[data-slot='input-group']
|
|
608
|
+
)
|
|
609
|
+
),
|
|
610
|
+
.drawer-footer :global([data-slot='button']) {
|
|
611
|
+
min-height: 2.75rem;
|
|
612
|
+
}
|
|
613
|
+
.panel-content-wrapper :global([data-slot='input-group'] > [data-slot='input-group-control']) {
|
|
614
|
+
align-self: stretch;
|
|
615
|
+
height: auto;
|
|
616
|
+
}
|
|
528
617
|
}
|
|
529
618
|
</style>
|
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
import type { PresetLabels } from '../../types/presetLabels';
|
|
8
8
|
import { createSolvingIndicator } from '../../compute/solving.svelte';
|
|
9
9
|
import { createRequestResponseDriver } from '@selvajs/solve/client';
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
RetainedSolveResult,
|
|
12
|
+
SolveEventSource,
|
|
13
|
+
SolveSession,
|
|
14
|
+
SolveSettlement
|
|
15
|
+
} from '@selvajs/solve/client';
|
|
11
16
|
import SolveMessages from './SolveMessages.svelte';
|
|
12
17
|
import { meshPolicy } from '@selvajs/visualization/parse';
|
|
13
18
|
import type { ThreeViewer } from '@selvajs/visualization/render';
|
|
@@ -19,8 +24,8 @@
|
|
|
19
24
|
import { type ViewerConfig } from '../viewer/Viewer.svelte';
|
|
20
25
|
import StateDisplay from '../primitives/StateDisplay.svelte';
|
|
21
26
|
import { setClientSlot, type ClientSlot } from '../../contexts/clientSlotContext.svelte';
|
|
22
|
-
import type { Locale } from '../../i18n/messages';
|
|
23
|
-
import {
|
|
27
|
+
import type { Locale, ViewerMessages } from '../../i18n/messages';
|
|
28
|
+
import { layerLocaleContext } from '../../i18n/localeContext.svelte';
|
|
24
29
|
|
|
25
30
|
import type { Snippet } from 'svelte';
|
|
26
31
|
|
|
@@ -34,8 +39,11 @@
|
|
|
34
39
|
isEmbedded?: boolean;
|
|
35
40
|
primaryColor?: string;
|
|
36
41
|
showModeToggle?: boolean;
|
|
42
|
+
/** Host actions, listed in the panel footer's ⋯ menu after saving and loading inputs. */
|
|
37
43
|
panelActions?: ActionButton[];
|
|
44
|
+
/** Lists "Save inputs…" in the footer menu. */
|
|
38
45
|
showSaveButton?: boolean;
|
|
46
|
+
/** Lists "Load inputs…" in the footer menu. */
|
|
39
47
|
showLoadButton?: boolean;
|
|
40
48
|
/** When set, persist saved states via this callback instead of downloading a .slvp file. */
|
|
41
49
|
onSaveState?: (state: ParameterPreset) => void | Promise<void>;
|
|
@@ -74,6 +82,12 @@
|
|
|
74
82
|
*/
|
|
75
83
|
getSession: () => SolveSession;
|
|
76
84
|
}) => void;
|
|
85
|
+
/**
|
|
86
|
+
* Once per settled solve: applied, discarded, blocked, aborted or failed. A result held
|
|
87
|
+
* for review counts when the user accepts or rejects it, and one a queued solve replaces
|
|
88
|
+
* never does. Auto-solves from input changes count too.
|
|
89
|
+
*/
|
|
90
|
+
onSolved?: (settlement: SolveSettlement) => void;
|
|
77
91
|
/** Hands the live three.js viewer to the host once it mounts. See `Viewer.svelte` for the contract. */
|
|
78
92
|
onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
|
|
79
93
|
/** Viewer chrome and defaults. `backgroundColor` and `showSceneManager` are set by the layout. */
|
|
@@ -92,6 +106,14 @@
|
|
|
92
106
|
// Language for the app's own chrome, English when unset. Does not translate
|
|
93
107
|
// schema-authored labels or Grasshopper-sourced names/metadata.
|
|
94
108
|
lang?: Locale;
|
|
109
|
+
/**
|
|
110
|
+
* Replaces individual strings of the app's own chrome, layered over the `lang` catalog
|
|
111
|
+
* (`VIEWER_MESSAGES`): reword a label, or supply every string for a language with no
|
|
112
|
+
* built-in catalog. Reaches every component inside, the viewer included. A `{name}`
|
|
113
|
+
* placeholder in the default is filled the same way in an override. The preset manager's
|
|
114
|
+
* strings come from `presetLabels` instead.
|
|
115
|
+
*/
|
|
116
|
+
messages?: Partial<ViewerMessages>;
|
|
95
117
|
}
|
|
96
118
|
|
|
97
119
|
let {
|
|
@@ -123,22 +145,26 @@
|
|
|
123
145
|
brandName,
|
|
124
146
|
header,
|
|
125
147
|
onReady,
|
|
148
|
+
onSolved,
|
|
126
149
|
onViewerReady,
|
|
127
150
|
viewerConfig = {},
|
|
128
151
|
externalScopeKey,
|
|
129
152
|
clientSlot,
|
|
130
|
-
lang
|
|
153
|
+
lang,
|
|
154
|
+
messages
|
|
131
155
|
}: Props = $props();
|
|
132
156
|
|
|
133
157
|
// Reaches InputControl deep in the tree.
|
|
134
158
|
// svelte-ignore state_referenced_locally
|
|
135
159
|
setClientSlot(clientSlot);
|
|
136
160
|
|
|
137
|
-
// Resolution: explicit `lang` → host-provided locale → English
|
|
138
|
-
// switching
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
161
|
+
// Resolution: explicit `lang` → host-provided locale → English, with `messages` on top.
|
|
162
|
+
// Getters, so switching either updates the chrome live.
|
|
163
|
+
const locale = layerLocaleContext(
|
|
164
|
+
() => lang,
|
|
165
|
+
() => messages
|
|
166
|
+
);
|
|
167
|
+
const t = $derived(locale.messages);
|
|
142
168
|
|
|
143
169
|
const resolvedScopeKey = $derived(externalScopeKey || definitionKey || schema?.id || '');
|
|
144
170
|
|
|
@@ -168,6 +194,9 @@
|
|
|
168
194
|
|
|
169
195
|
const solvingIndicator = createSolvingIndicator(() => session.isSolving);
|
|
170
196
|
|
|
197
|
+
// Read at call time, so a host swapping the callback needs no resubscribe.
|
|
198
|
+
$effect(() => session.onSettled((settlement) => onSolved?.(settlement)));
|
|
199
|
+
|
|
171
200
|
$effect(() => {
|
|
172
201
|
onReady?.({
|
|
173
202
|
loadValues: (incoming) => session.loadValues(incoming),
|
|
@@ -254,6 +283,7 @@
|
|
|
254
283
|
{#key definitionKey}
|
|
255
284
|
<AppLayout
|
|
256
285
|
{schema}
|
|
286
|
+
showViewer={!!schema.viewerOptions?.enableRemote}
|
|
257
287
|
meshes={session.meshes}
|
|
258
288
|
isSolving={session.isSolving}
|
|
259
289
|
showSolvingIndicator={schema.instanceSolve !== false && solvingIndicator.show}
|
|
@@ -272,6 +302,7 @@
|
|
|
272
302
|
{presetLabels}
|
|
273
303
|
onValueChange={handleValueChange}
|
|
274
304
|
oncalculate={handleCalculate}
|
|
305
|
+
ensureSolved={() => session.ensureSolved()}
|
|
275
306
|
onLoadValues={() => session.loadValues({})}
|
|
276
307
|
/>
|
|
277
308
|
{/key}
|
|
@@ -286,15 +317,4 @@
|
|
|
286
317
|
onabort={() => session.abort()}
|
|
287
318
|
onconfirm={() => session.acknowledge()}
|
|
288
319
|
ondiscard={() => session.discard()}
|
|
289
|
-
labels={{
|
|
290
|
-
solving: t.solveSolving,
|
|
291
|
-
blockedTitle: t.solveBlockedTitle,
|
|
292
|
-
messagesTitle: t.solveMessagesTitle,
|
|
293
|
-
blockedDescription: t.solveBlocked,
|
|
294
|
-
messagesDescription: t.solveMessagesDescription,
|
|
295
|
-
confirm: t.solveMessagesConfirm,
|
|
296
|
-
discard: t.solveMessagesDiscard,
|
|
297
|
-
abort: t.solveMessagesAbort,
|
|
298
|
-
dismiss: t.solveMessagesDismiss
|
|
299
|
-
}}
|
|
300
320
|
/>
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import type { SolveDiagnostic } from '@selvajs/solve/shared';
|
|
11
11
|
import * as Dialog from '$lib/components/primitives/dialog';
|
|
12
12
|
import IssueList, { type Issue } from '$lib/components/primitives/IssueList.svelte';
|
|
13
|
+
import { getLocaleContext } from '$lib/i18n/localeContext.svelte';
|
|
14
|
+
import { formatCount } from '$lib/i18n/messages';
|
|
13
15
|
|
|
14
16
|
interface Props {
|
|
15
17
|
/** The last solve's messages. Preferred over `errors`/`warnings` when both are given. */
|
|
@@ -23,6 +25,9 @@
|
|
|
23
25
|
|
|
24
26
|
let { diagnostics, errors = [], warnings = [], trigger }: Props = $props();
|
|
25
27
|
|
|
28
|
+
const locale = getLocaleContext();
|
|
29
|
+
const t = $derived(locale.messages);
|
|
30
|
+
|
|
26
31
|
let open = $state(false);
|
|
27
32
|
|
|
28
33
|
const rows = $derived<SolveDiagnostic[]>(
|
|
@@ -41,26 +46,11 @@
|
|
|
41
46
|
count: number;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
const
|
|
45
|
-
{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
many: 'errors'
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
level: 'warning',
|
|
53
|
-
severity: 'warning',
|
|
54
|
-
one: 'warning',
|
|
55
|
-
many: 'warnings'
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
level: 'remark',
|
|
59
|
-
severity: 'note',
|
|
60
|
-
one: 'note',
|
|
61
|
-
many: 'notes'
|
|
62
|
-
}
|
|
63
|
-
] as const;
|
|
49
|
+
const levels = $derived([
|
|
50
|
+
{ level: 'error', severity: 'error', one: t.errorCountOne, other: t.errorCountOther },
|
|
51
|
+
{ level: 'warning', severity: 'warning', one: t.warningCountOne, other: t.warningCountOther },
|
|
52
|
+
{ level: 'remark', severity: 'note', one: t.noteCountOne, other: t.noteCountOther }
|
|
53
|
+
] as const);
|
|
64
54
|
|
|
65
55
|
/**
|
|
66
56
|
* One section per level that has messages, most severe first; within a level, the order the
|
|
@@ -68,23 +58,25 @@
|
|
|
68
58
|
* branches raises the same warning 200 times, and an unstacked list buries every other message.
|
|
69
59
|
*/
|
|
70
60
|
const sections = $derived(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
byKey[key]
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
61
|
+
levels
|
|
62
|
+
.map((meta) => {
|
|
63
|
+
// A null-prototype object rather than a Map: a local built fresh on each derive, never
|
|
64
|
+
// reactive state, and the lint rule that steers Map → SvelteMap cannot tell them apart.
|
|
65
|
+
const byKey: Record<string, Item> = Object.create(null);
|
|
66
|
+
const items: Item[] = [];
|
|
67
|
+
for (const d of rows) {
|
|
68
|
+
if (d.level !== meta.level) continue;
|
|
69
|
+
const key = `${d.message} ${d.source ?? ''}`;
|
|
70
|
+
if (byKey[key]) {
|
|
71
|
+
byKey[key].count++;
|
|
72
|
+
} else {
|
|
73
|
+
byKey[key] = { level: d.level, message: d.message, source: d.source, count: 1 };
|
|
74
|
+
items.push(byKey[key]);
|
|
75
|
+
}
|
|
84
76
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
77
|
+
return { ...meta, items };
|
|
78
|
+
})
|
|
79
|
+
.filter((section) => section.items.length > 0)
|
|
88
80
|
);
|
|
89
81
|
|
|
90
82
|
const issues = $derived<Issue[]>(
|
|
@@ -106,13 +98,11 @@
|
|
|
106
98
|
|
|
107
99
|
<Dialog.Content class="max-w-lg">
|
|
108
100
|
<Dialog.Header>
|
|
109
|
-
<Dialog.Title>
|
|
101
|
+
<Dialog.Title>{t.messagesTitle}</Dialog.Title>
|
|
110
102
|
<Dialog.Description>
|
|
111
103
|
{#each sections as section, i (section.level)}
|
|
112
104
|
{#if i > 0}<span class="mx-1.5">·</span>{/if}
|
|
113
|
-
<span
|
|
114
|
-
>{section.items.length} {section.items.length === 1 ? section.one : section.many}</span
|
|
115
|
-
>
|
|
105
|
+
<span>{formatCount(section.items.length, section.one, section.other)}</span>
|
|
116
106
|
{/each}
|
|
117
107
|
</Dialog.Description>
|
|
118
108
|
</Dialog.Header>
|