@selvajs/ui 6.6.0-beta.8 → 6.6.0-beta.9
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 +37 -7
- package/dist/components/compute/AppLayout.svelte.d.ts +5 -0
- package/dist/components/compute/ParameterPresetManager.svelte +8 -20
- package/dist/components/compute/SolveMessages.svelte +26 -3
- package/dist/components/compute/SolveMessages.svelte.d.ts +7 -0
- package/dist/components/layout/PageFooter.svelte +3 -1
- package/dist/components/layout/SideNav.svelte +55 -14
- package/dist/components/preview/ChartOutput.svelte +18 -13
- package/dist/components/preview/Group.svelte +6 -1
- package/dist/components/preview/ImageOutput.svelte +3 -1
- package/dist/components/preview/InputControl.svelte +108 -44
- package/dist/components/preview/InputControl.svelte.d.ts +5 -1
- package/dist/components/preview/TabContent.svelte +61 -29
- package/dist/components/preview/TabContent.svelte.d.ts +5 -1
- package/dist/components/preview/TabLayout.svelte +25 -3
- package/dist/components/preview/TabLayout.svelte.d.ts +5 -0
- package/dist/components/preview/inputs/ChecklistInput.svelte +7 -2
- package/dist/components/preview/inputs/ChecklistInput.svelte.d.ts +2 -1
- package/dist/components/preview/inputs/DropdownInput.svelte +16 -4
- package/dist/components/preview/inputs/DropdownInput.svelte.d.ts +2 -1
- package/dist/components/preview/inputs/FileInput.svelte +56 -65
- package/dist/components/preview/inputs/GeometryInput.svelte +128 -0
- package/dist/components/preview/inputs/GeometryInput.svelte.d.ts +21 -0
- package/dist/components/preview/inputs/OptionGlyph.svelte +29 -0
- package/dist/components/preview/inputs/OptionGlyph.svelte.d.ts +8 -0
- package/dist/components/preview/inputs/SegmentedInput.svelte +44 -0
- package/dist/components/preview/inputs/SegmentedInput.svelte.d.ts +12 -0
- package/dist/components/preview/inputs/TileInput.svelte +80 -0
- package/dist/components/preview/inputs/TileInput.svelte.d.ts +12 -0
- package/dist/components/preview/inputs/index.d.ts +3 -0
- package/dist/components/preview/inputs/index.js +3 -0
- package/dist/components/primitives/SolvingIndicator.svelte +28 -3
- package/dist/components/primitives/SolvingIndicator.svelte.d.ts +7 -0
- package/dist/components/primitives/button/button.svelte +3 -0
- package/dist/components/primitives/button/button.svelte.d.ts +3 -0
- package/dist/components/primitives/toggle/toggle.svelte +3 -1
- package/dist/components/primitives/toggle/toggle.svelte.d.ts +3 -0
- package/dist/components/viewer/Viewer.svelte +11 -11
- package/dist/compute/solving.svelte.d.ts +7 -1
- package/dist/compute/solving.svelte.js +12 -1
- package/dist/contexts/hostContext.svelte.d.ts +39 -0
- package/dist/contexts/hostContext.svelte.js +9 -0
- package/dist/i18n/messages.d.ts +33 -0
- package/dist/i18n/messages.js +58 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/schema/host-support.d.ts +29 -0
- package/dist/schema/host-support.js +97 -0
- package/dist/schema/option-display.d.ts +12 -0
- package/dist/schema/option-display.js +29 -0
- package/dist/schema/param-exporter.d.ts +4 -1
- package/dist/schema/param-exporter.js +10 -6
- package/dist/styles/base.css +6 -3
- package/dist/utils/file-download.js +6 -10
- package/dist/utils/host-files.d.ts +12 -0
- package/dist/utils/host-files.js +46 -0
- package/dist/utils/webp-to-png.d.ts +10 -0
- package/dist/utils/webp-to-png.js +41 -0
- package/package.json +9 -9
- package/src/lib/components/compute/AppLayout.svelte +37 -7
- package/src/lib/components/compute/ParameterPresetManager.svelte +8 -20
- package/src/lib/components/compute/SolveMessages.svelte +26 -3
- package/src/lib/components/layout/PageFooter.svelte +3 -1
- package/src/lib/components/layout/SideNav.svelte +55 -14
- package/src/lib/components/preview/ChartOutput.svelte +18 -13
- package/src/lib/components/preview/Group.svelte +6 -1
- package/src/lib/components/preview/ImageOutput.svelte +3 -1
- package/src/lib/components/preview/InputControl.svelte +108 -44
- package/src/lib/components/preview/TabContent.svelte +61 -29
- package/src/lib/components/preview/TabLayout.svelte +25 -3
- package/src/lib/components/preview/inputs/ChecklistInput.svelte +7 -2
- package/src/lib/components/preview/inputs/DropdownInput.svelte +16 -4
- package/src/lib/components/preview/inputs/FileInput.svelte +56 -65
- package/src/lib/components/preview/inputs/GeometryInput.svelte +128 -0
- package/src/lib/components/preview/inputs/OptionGlyph.svelte +29 -0
- package/src/lib/components/preview/inputs/SegmentedInput.svelte +44 -0
- package/src/lib/components/preview/inputs/TileInput.svelte +80 -0
- package/src/lib/components/preview/inputs/index.ts +3 -0
- package/src/lib/components/primitives/SolvingIndicator.svelte +28 -3
- package/src/lib/components/primitives/button/button.svelte +3 -0
- package/src/lib/components/primitives/toggle/toggle.svelte +3 -1
- package/src/lib/components/viewer/Viewer.svelte +11 -11
- package/src/lib/compute/solving.svelte.ts +16 -1
- package/src/lib/contexts/hostContext.svelte.ts +51 -0
- package/src/lib/i18n/messages.ts +96 -0
- package/src/lib/index.ts +20 -0
- package/src/lib/schema/host-support.ts +129 -0
- package/src/lib/schema/option-display.ts +44 -0
- package/src/lib/schema/param-exporter.ts +10 -7
- package/src/lib/styles/base.css +6 -3
- package/src/lib/utils/file-download.ts +7 -11
- package/src/lib/utils/host-files.ts +61 -0
- package/src/lib/utils/webp-to-png.ts +48 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
2
3
|
import type { UISchema, SupportedTypes, ParameterPreset } from '@selvajs/schemas';
|
|
3
4
|
import type { SolveSettlement } from '@selvajs/solve/client';
|
|
4
5
|
import type { ActionButton } from '../../types/actionButton';
|
|
@@ -34,6 +35,10 @@
|
|
|
34
35
|
meshes?: any[];
|
|
35
36
|
isSolving: boolean;
|
|
36
37
|
showSolvingIndicator?: boolean;
|
|
38
|
+
/** `SolvingIndicator`'s `placement`; `inline` puts it in the action row. */
|
|
39
|
+
solvingIndicatorPlacement?: 'floating' | 'inline';
|
|
40
|
+
/** Rendered above the action row, for a host that keeps the solve's messages in the flow. */
|
|
41
|
+
solveMessages?: Snippet;
|
|
37
42
|
hasPendingChanges?: boolean;
|
|
38
43
|
hasNeverSolved?: boolean;
|
|
39
44
|
isViewerFullscreen?: boolean;
|
|
@@ -63,6 +68,8 @@
|
|
|
63
68
|
meshes = [],
|
|
64
69
|
isSolving,
|
|
65
70
|
showSolvingIndicator = false,
|
|
71
|
+
solvingIndicatorPlacement = 'floating',
|
|
72
|
+
solveMessages,
|
|
66
73
|
hasPendingChanges = false,
|
|
67
74
|
hasNeverSolved = false,
|
|
68
75
|
isViewerFullscreen = $bindable(false),
|
|
@@ -248,6 +255,9 @@
|
|
|
248
255
|
<!-- One primary action and one menu. A definition that solves on every change has no Calculate,
|
|
249
256
|
so the menu sits alone at the end of the row. -->
|
|
250
257
|
{#snippet footer()}
|
|
258
|
+
{#if solveMessages}
|
|
259
|
+
<div class="px-2">{@render solveMessages()}</div>
|
|
260
|
+
{/if}
|
|
251
261
|
{#if schema.instanceSolve === false}
|
|
252
262
|
<CalculateButton
|
|
253
263
|
{hasPendingChanges}
|
|
@@ -256,9 +266,14 @@
|
|
|
256
266
|
{oncalculate}
|
|
257
267
|
trailing={hasMenu ? actionsMenu : undefined}
|
|
258
268
|
/>
|
|
259
|
-
{:else if hasMenu}
|
|
260
|
-
<div class="mt-3 pb-3 pt-2 px-2 flex
|
|
261
|
-
{
|
|
269
|
+
{:else if hasMenu || solvingIndicatorPlacement === 'inline'}
|
|
270
|
+
<div class="mt-3 pb-3 pt-2 px-2 gap-2 flex items-center border-t border-border/50">
|
|
271
|
+
{#if solvingIndicatorPlacement === 'inline'}
|
|
272
|
+
<SolvingIndicator show={showSolvingIndicator} placement="inline" />
|
|
273
|
+
{/if}
|
|
274
|
+
{#if hasMenu}
|
|
275
|
+
<div class="ml-auto">{@render actionsMenu()}</div>
|
|
276
|
+
{/if}
|
|
262
277
|
</div>
|
|
263
278
|
{/if}
|
|
264
279
|
{/snippet}
|
|
@@ -272,7 +287,7 @@
|
|
|
272
287
|
)}
|
|
273
288
|
<div class="panel-content-wrapper">
|
|
274
289
|
{#if schema.layout.type === 'tabbed'}
|
|
275
|
-
<TabLayout {schema} {values} {onValueChange} {panelFilter} {requestedTabId} />
|
|
290
|
+
<TabLayout {schema} {values} {onValueChange} {panelFilter} {requestedTabId} bare={isMobile} />
|
|
276
291
|
{/if}
|
|
277
292
|
{#if hostsFooter && !isMobile}
|
|
278
293
|
<div class="panel-footer px-1">
|
|
@@ -344,7 +359,7 @@
|
|
|
344
359
|
<!-- Inert while closed: the fields are clipped out of view, and tabbing into them
|
|
345
360
|
would scroll the drawer's insides around behind the handle. -->
|
|
346
361
|
<div class="drawer-content" inert={drawer === 'closed'}>
|
|
347
|
-
<div class="px-
|
|
362
|
+
<div class="px-2 pb-4">
|
|
348
363
|
{#if hasLeftPanel}
|
|
349
364
|
{@render panelContent(hasRightPanel ? 'left' : undefined, requestedLeftTabId, false)}
|
|
350
365
|
{/if}
|
|
@@ -509,7 +524,9 @@
|
|
|
509
524
|
</div>
|
|
510
525
|
{/if}
|
|
511
526
|
|
|
512
|
-
|
|
527
|
+
{#if solvingIndicatorPlacement === 'floating'}
|
|
528
|
+
<SolvingIndicator show={showSolvingIndicator} />
|
|
529
|
+
{/if}
|
|
513
530
|
</main>
|
|
514
531
|
|
|
515
532
|
<!-- The save, load and validation dialogs; the footer's menu opens them. -->
|
|
@@ -555,7 +572,13 @@
|
|
|
555
572
|
|
|
556
573
|
.mobile-root {
|
|
557
574
|
--drawer-closed-h: 60px;
|
|
558
|
-
--drawer-half-h:
|
|
575
|
+
--drawer-half-h: 60%;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/* On a phone the panels stack in one scroller. Held at the scroller's height, a long left
|
|
579
|
+
panel would spill over the right panel below it. */
|
|
580
|
+
.mobile-root .panel-content-wrapper {
|
|
581
|
+
height: auto;
|
|
559
582
|
}
|
|
560
583
|
|
|
561
584
|
.viewer-region {
|
|
@@ -633,6 +656,13 @@
|
|
|
633
656
|
padding: 0rem 1rem;
|
|
634
657
|
}
|
|
635
658
|
|
|
659
|
+
/* The sheet's top edge already separates it from the model; the desktop footer's extra
|
|
660
|
+
spacing only takes rows from the fields. */
|
|
661
|
+
.drawer-footer :global(> div) {
|
|
662
|
+
margin-top: 0;
|
|
663
|
+
padding-block: 0.5rem;
|
|
664
|
+
}
|
|
665
|
+
|
|
636
666
|
/* Touch gets 44px targets on the panel's controls; a fine pointer keeps the denser sizes. */
|
|
637
667
|
@media (pointer: coarse) {
|
|
638
668
|
.panel-content-wrapper
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
import type { UISchema, SupportedTypes, ParameterPreset } from '@selvajs/schemas';
|
|
2
3
|
import type { SolveSettlement } from '@selvajs/solve/client';
|
|
3
4
|
import type { ActionButton } from '../../types/actionButton';
|
|
@@ -14,6 +15,10 @@ interface Props {
|
|
|
14
15
|
meshes?: any[];
|
|
15
16
|
isSolving: boolean;
|
|
16
17
|
showSolvingIndicator?: boolean;
|
|
18
|
+
/** `SolvingIndicator`'s `placement`; `inline` puts it in the action row. */
|
|
19
|
+
solvingIndicatorPlacement?: 'floating' | 'inline';
|
|
20
|
+
/** Rendered above the action row, for a host that keeps the solve's messages in the flow. */
|
|
21
|
+
solveMessages?: Snippet;
|
|
17
22
|
hasPendingChanges?: boolean;
|
|
18
23
|
hasNeverSolved?: boolean;
|
|
19
24
|
isViewerFullscreen?: boolean;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
} from '../../schema/param-exporter';
|
|
11
11
|
import { Button, Input, Label, Textarea, Dialog, Callout, IssueList, toast } from '../primitives';
|
|
12
12
|
import { DEFAULT_PRESET_LABELS, type PresetLabels } from '../../types/presetLabels';
|
|
13
|
+
import { pickFile } from '../../utils/host-files';
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
15
16
|
schema: UISchema;
|
|
@@ -37,7 +38,6 @@
|
|
|
37
38
|
let showLoadDialog = $state(false);
|
|
38
39
|
let loadResult = $state<PresetLoadResult | null>(null);
|
|
39
40
|
let loadingPresetName = $state('');
|
|
40
|
-
let fileInputRef = $state<HTMLInputElement | null>(null);
|
|
41
41
|
|
|
42
42
|
let listedStates = $state<ParameterPreset[]>([]);
|
|
43
43
|
let isLoadingList = $state(false);
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
if (onSaveState) await onSaveState(state);
|
|
72
|
-
else exportStateAsJson(state);
|
|
72
|
+
else await exportStateAsJson(state);
|
|
73
73
|
showExportDialog = false;
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -86,19 +86,16 @@
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
async function
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
async function importFromFile() {
|
|
90
|
+
// .sps is the pre-rename extension; files with it exist on users' drives, so keep accepting it.
|
|
91
|
+
const file = await pickFile({ accept: '.slvp,.sps' });
|
|
92
|
+
if (!file) return;
|
|
92
93
|
|
|
93
94
|
try {
|
|
94
|
-
|
|
95
|
-
tryLoad(imported);
|
|
95
|
+
tryLoad(await importStateFromJson(file));
|
|
96
96
|
} catch (error) {
|
|
97
97
|
toast.error(t.loadImportError + (error as Error).message);
|
|
98
98
|
}
|
|
99
|
-
|
|
100
|
-
// Reset so re-picking the same file fires `change` again.
|
|
101
|
-
input.value = '';
|
|
102
99
|
}
|
|
103
100
|
|
|
104
101
|
function confirmLoad() {
|
|
@@ -132,8 +129,8 @@
|
|
|
132
129
|
}
|
|
133
130
|
|
|
134
131
|
function handleLoadClick() {
|
|
135
|
-
fileInputRef?.click();
|
|
136
132
|
showLoadDialog = false;
|
|
133
|
+
void importFromFile();
|
|
137
134
|
}
|
|
138
135
|
|
|
139
136
|
function selectListedState(preset: ParameterPreset) {
|
|
@@ -142,15 +139,6 @@
|
|
|
142
139
|
}
|
|
143
140
|
</script>
|
|
144
141
|
|
|
145
|
-
<!-- .sps is the pre-rename extension; files with it exist on users' drives, so keep accepting it. -->
|
|
146
|
-
<input
|
|
147
|
-
bind:this={fileInputRef}
|
|
148
|
-
type="file"
|
|
149
|
-
accept=".slvp,.sps"
|
|
150
|
-
onchange={handleImport}
|
|
151
|
-
class="hidden"
|
|
152
|
-
/>
|
|
153
|
-
|
|
154
142
|
<!-- Save Dialog -->
|
|
155
143
|
<Dialog.Root bind:open={showExportDialog}>
|
|
156
144
|
<Dialog.Content>
|
|
@@ -31,6 +31,13 @@
|
|
|
31
31
|
onabort: () => void;
|
|
32
32
|
onconfirm: () => void;
|
|
33
33
|
ondiscard: () => void;
|
|
34
|
+
/**
|
|
35
|
+
* See `createSolvingIndicator`. The live panel would only paint once the solve is over, so
|
|
36
|
+
* an Esc hint stands in for it; the result's messages still show.
|
|
37
|
+
*/
|
|
38
|
+
freezesDuringSolve?: boolean;
|
|
39
|
+
/** `floating` overlays the bottom centre; `inline` sits in the flow below the host's content. */
|
|
40
|
+
placement?: 'floating' | 'inline';
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
let {
|
|
@@ -39,7 +46,9 @@
|
|
|
39
46
|
diagnostics = [],
|
|
40
47
|
onabort,
|
|
41
48
|
onconfirm,
|
|
42
|
-
ondiscard
|
|
49
|
+
ondiscard,
|
|
50
|
+
freezesDuringSolve = false,
|
|
51
|
+
placement = 'floating'
|
|
43
52
|
}: Props = $props();
|
|
44
53
|
|
|
45
54
|
const locale = getLocaleContext();
|
|
@@ -145,6 +154,7 @@
|
|
|
145
154
|
$effect(() => {
|
|
146
155
|
if (phase === 'live') {
|
|
147
156
|
finishing = false;
|
|
157
|
+
if (freezesDuringSolve) return;
|
|
148
158
|
const timer = setTimeout(() => {
|
|
149
159
|
revealed = true;
|
|
150
160
|
revealedAt = performance.now();
|
|
@@ -247,9 +257,22 @@
|
|
|
247
257
|
|
|
248
258
|
<div class="sr-only" role="status" aria-live="polite">{announcement}</div>
|
|
249
259
|
|
|
250
|
-
{#if
|
|
260
|
+
{#if freezesDuringSolve && view === 'live'}
|
|
261
|
+
<!-- Painted before the solve takes the thread; Abort could not be clicked until it ends. Inline,
|
|
262
|
+
the solving chip carries the hint instead: a line here would resize the footer mid-solve. -->
|
|
263
|
+
{#if placement === 'floating'}
|
|
264
|
+
<p
|
|
265
|
+
class="bottom-4 text-xs fixed left-1/2 z-50 -translate-x-1/2 text-muted-foreground"
|
|
266
|
+
data-testid="solve-esc-hint"
|
|
267
|
+
>
|
|
268
|
+
{t.solveMessagesEscHint}
|
|
269
|
+
</p>
|
|
270
|
+
{/if}
|
|
271
|
+
{:else if visible}
|
|
251
272
|
<div
|
|
252
|
-
class="
|
|
273
|
+
class="p-3 border {placement === 'inline'
|
|
274
|
+
? 'mt-3 rounded-md bg-muted/40'
|
|
275
|
+
: 'bottom-4 shadow-lg w-lg fixed left-1/2 z-50 max-w-screen-inset -translate-x-1/2 rounded-lg bg-background'} {view ===
|
|
253
276
|
'blocked'
|
|
254
277
|
? 'border-destructive/50'
|
|
255
278
|
: ''}"
|
|
@@ -10,6 +10,13 @@ interface Props {
|
|
|
10
10
|
onabort: () => void;
|
|
11
11
|
onconfirm: () => void;
|
|
12
12
|
ondiscard: () => void;
|
|
13
|
+
/**
|
|
14
|
+
* See `createSolvingIndicator`. The live panel would only paint once the solve is over, so
|
|
15
|
+
* an Esc hint stands in for it; the result's messages still show.
|
|
16
|
+
*/
|
|
17
|
+
freezesDuringSolve?: boolean;
|
|
18
|
+
/** `floating` overlays the bottom centre; `inline` sits in the flow below the host's content. */
|
|
19
|
+
placement?: 'floating' | 'inline';
|
|
13
20
|
}
|
|
14
21
|
declare const SolveMessages: import("svelte").Component<Props, {}, "">;
|
|
15
22
|
type SolveMessages = ReturnType<typeof SolveMessages>;
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
<script lang="ts">
|
|
14
14
|
import type { Snippet } from 'svelte';
|
|
15
15
|
import { page } from '$app/state';
|
|
16
|
+
import ChevronsUpDown from '@lucide/svelte/icons/chevrons-up-down';
|
|
17
|
+
import Check from '@lucide/svelte/icons/check';
|
|
18
|
+
import * as DropdownMenu from '../primitives/dropdown-menu/index.js';
|
|
16
19
|
|
|
17
20
|
interface SideNavProps {
|
|
18
21
|
items: SideNavItem[];
|
|
@@ -41,10 +44,12 @@
|
|
|
41
44
|
if (item.match === 'prefix') return path.startsWith(item.href);
|
|
42
45
|
return path === item.href;
|
|
43
46
|
}
|
|
47
|
+
|
|
48
|
+
const activeItem = $derived(items.find(isActive));
|
|
44
49
|
</script>
|
|
45
50
|
|
|
46
|
-
<!-- On a phone a 240px column would leave the page ~150px, so the nav
|
|
47
|
-
|
|
51
|
+
<!-- On a phone a 240px column would leave the page ~150px, so the nav collapses to one switcher
|
|
52
|
+
naming the current section; the header and footer drop out. -->
|
|
48
53
|
<aside
|
|
49
54
|
class={`sm:h-full sm:w-60 sm:overflow-y-auto sm:border-b-0 flex w-full shrink-0 flex-col border-b border-border bg-background ${
|
|
50
55
|
border ? 'sm:border-r' : ''
|
|
@@ -64,31 +69,67 @@
|
|
|
64
69
|
</div>
|
|
65
70
|
{/if}
|
|
66
71
|
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
<div class="py-2 sm:hidden px-(--page-px)">
|
|
73
|
+
<DropdownMenu.Root>
|
|
74
|
+
<DropdownMenu.Trigger>
|
|
75
|
+
{#snippet child({ props })}
|
|
76
|
+
<button
|
|
77
|
+
{...props}
|
|
78
|
+
class="min-h-11 gap-3 px-3 text-sm font-medium flex w-full items-center rounded-md border border-border bg-card text-left"
|
|
79
|
+
>
|
|
80
|
+
{#if activeItem?.icon}
|
|
81
|
+
{@const Icon = activeItem.icon}
|
|
82
|
+
<Icon class="size-4 shrink-0 opacity-70" />
|
|
83
|
+
{/if}
|
|
84
|
+
<span class="min-w-0 flex-1 truncate">
|
|
85
|
+
{#if eyebrow}<span class="text-muted-foreground">{eyebrow} /</span>{/if}
|
|
86
|
+
{activeItem?.label ?? ''}
|
|
87
|
+
</span>
|
|
88
|
+
<ChevronsUpDown class="size-4 shrink-0 opacity-60" />
|
|
89
|
+
</button>
|
|
90
|
+
{/snippet}
|
|
91
|
+
</DropdownMenu.Trigger>
|
|
92
|
+
<DropdownMenu.Content sideOffset={4}>
|
|
93
|
+
{#each items as item (item.href)}
|
|
94
|
+
{@const active = isActive(item)}
|
|
95
|
+
{@const Icon = item.icon}
|
|
96
|
+
<DropdownMenu.Item class="min-h-11">
|
|
97
|
+
{#snippet child({ props })}
|
|
98
|
+
<a href={item.href} aria-current={active ? 'page' : undefined} {...props}>
|
|
99
|
+
{#if Icon}<Icon class="size-4 opacity-70" />{/if}
|
|
100
|
+
<span class="flex-1">{item.label}</span>
|
|
101
|
+
{#if item.badge !== undefined}
|
|
102
|
+
<span class="font-mono text-2xs tabular-nums opacity-60">{item.badge}</span>
|
|
103
|
+
{/if}
|
|
104
|
+
{#if active}<Check class="size-4" aria-hidden="true" />{/if}
|
|
105
|
+
</a>
|
|
106
|
+
{/snippet}
|
|
107
|
+
</DropdownMenu.Item>
|
|
108
|
+
{/each}
|
|
109
|
+
</DropdownMenu.Content>
|
|
110
|
+
</DropdownMenu.Root>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<nav class="pb-4 pr-2 sm:block hidden flex-1 space-y-px">
|
|
70
114
|
{#each items as item (item.href)}
|
|
71
115
|
{@const active = isActive(item)}
|
|
72
116
|
{@const Icon = item.icon}
|
|
73
117
|
<a
|
|
74
118
|
href={item.href}
|
|
75
|
-
|
|
119
|
+
aria-current={active ? 'page' : undefined}
|
|
120
|
+
class={`group gap-2 py-1.5 pr-2.5 relative flex w-full items-center text-left transition-colors ${
|
|
76
121
|
active
|
|
77
|
-
? 'pl-
|
|
78
|
-
: '
|
|
122
|
+
? 'pl-4 rounded-r-md bg-accent text-accent-foreground'
|
|
123
|
+
: 'mx-2 pl-2.5 rounded-md text-muted-foreground hover:bg-muted/60 hover:text-foreground'
|
|
79
124
|
}`}
|
|
80
125
|
>
|
|
81
126
|
{#if active}
|
|
82
|
-
<span
|
|
83
|
-
class="left-0 top-0 bottom-0 w-0.75 sm:block absolute hidden bg-primary"
|
|
84
|
-
aria-hidden="true"
|
|
85
|
-
></span>
|
|
127
|
+
<span class="left-0 top-0 bottom-0 w-0.75 absolute bg-primary" aria-hidden="true"></span>
|
|
86
128
|
{/if}
|
|
87
129
|
{#if Icon}
|
|
88
130
|
<Icon class="h-3.5 w-3.5 shrink-0 opacity-70" />
|
|
89
131
|
{/if}
|
|
90
|
-
<span class="text-sm font-medium
|
|
91
|
-
>
|
|
132
|
+
<span class="text-sm font-medium flex-1 truncate">{item.label}</span>
|
|
92
133
|
{#if item.badge !== undefined}
|
|
93
134
|
<span class="font-mono shrink-0 text-2xs tabular-nums opacity-60">{item.badge}</span>
|
|
94
135
|
{/if}
|
|
@@ -54,6 +54,9 @@
|
|
|
54
54
|
return window.Plotly;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// Some embedded WebViews (WKWebView in the Rhino panel) ship without the Fullscreen API at all.
|
|
58
|
+
const fullscreenSupported = typeof document !== 'undefined' && !!document.fullscreenEnabled;
|
|
59
|
+
|
|
57
60
|
function toggleFullscreen() {
|
|
58
61
|
if (!wrapperEl) return;
|
|
59
62
|
if (!document.fullscreenElement) {
|
|
@@ -211,19 +214,21 @@
|
|
|
211
214
|
</div>
|
|
212
215
|
{/if}
|
|
213
216
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
217
|
+
{#if fullscreenSupported}
|
|
218
|
+
<button
|
|
219
|
+
onclick={toggleFullscreen}
|
|
220
|
+
disabled={loading}
|
|
221
|
+
title={isFullscreen ? t.exitFullscreen : t.fullscreen}
|
|
222
|
+
aria-label={isFullscreen ? t.exitFullscreen : t.fullscreen}
|
|
223
|
+
class="rounded p-1.5 backdrop-blur-sm flex items-center border border-border bg-background/80 text-foreground transition-colors hover:bg-background disabled:cursor-not-allowed disabled:opacity-50"
|
|
224
|
+
>
|
|
225
|
+
{#if isFullscreen}
|
|
226
|
+
<Minimize size={14} />
|
|
227
|
+
{:else}
|
|
228
|
+
<Maximize size={14} />
|
|
229
|
+
{/if}
|
|
230
|
+
</button>
|
|
231
|
+
{/if}
|
|
227
232
|
</div>
|
|
228
233
|
{/if}
|
|
229
234
|
</div>
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
</div>
|
|
70
70
|
{/snippet}
|
|
71
71
|
|
|
72
|
-
<section class="shadow-xs rounded-xl bg-card">
|
|
72
|
+
<section class="shadow-xs rounded-xl border bg-card">
|
|
73
73
|
{#if flat}
|
|
74
74
|
<div class="p-4">
|
|
75
75
|
{@render grid()}
|
|
@@ -160,6 +160,11 @@
|
|
|
160
160
|
grid-column: span var(--span) / span var(--span);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/* A control with no label above it lines up with the controls beside it, not their labels. */
|
|
164
|
+
.grid-cell:has(:global([data-control-line])) {
|
|
165
|
+
align-self: end;
|
|
166
|
+
}
|
|
167
|
+
|
|
163
168
|
/* An empty full-width row: the two row gaps around it already read as a break. */
|
|
164
169
|
.grid-break {
|
|
165
170
|
grid-column: 1 / -1;
|
|
@@ -62,7 +62,9 @@
|
|
|
62
62
|
|
|
63
63
|
const cfg = $derived(item.config ?? {});
|
|
64
64
|
const allowDownload = $derived(cfg.allowDownload ?? true);
|
|
65
|
-
|
|
65
|
+
// Some embedded WebViews (WKWebView in the Rhino panel) ship without the Fullscreen API at all.
|
|
66
|
+
const fullscreenSupported = typeof document !== 'undefined' && !!document.fullscreenEnabled;
|
|
67
|
+
const allowFullscreen = $derived((cfg.allowFullscreen ?? true) && fullscreenSupported);
|
|
66
68
|
const backgroundColor = $derived(cfg.backgroundColor);
|
|
67
69
|
|
|
68
70
|
let wrapperEl = $state<HTMLDivElement | null>(null);
|