@selvajs/ui 6.1.2 → 6.3.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/ConfirmDialog.svelte +59 -0
- package/dist/components/ConfirmDialog.svelte.d.ts +13 -0
- package/dist/components/compute/ComputeApp.svelte +10 -0
- package/dist/components/compute/ComputeApp.svelte.d.ts +3 -0
- package/dist/components/compute/ParameterPresetManager.svelte +3 -3
- package/dist/components/layout/AppShell.svelte +19 -2
- package/dist/components/layout/SideNav.svelte +16 -2
- package/dist/components/layout/SideNav.svelte.d.ts +5 -0
- package/dist/components/preview/inputs/FileInput.svelte +3 -2
- package/dist/components/primitives/Callout.svelte +73 -0
- package/dist/components/primitives/Callout.svelte.d.ts +40 -0
- package/dist/components/primitives/index.d.ts +2 -0
- package/dist/components/primitives/index.js +2 -0
- package/dist/components/viewer/MeshMetadataDialog.svelte +1 -1
- package/dist/components/viewer/SceneManager.svelte +274 -103
- package/dist/components/viewer/SceneManager.svelte.d.ts +6 -0
- package/dist/components/viewer/Viewer.svelte +104 -32
- package/dist/i18n/messages.d.ts +4 -2
- package/dist/i18n/messages.js +8 -4
- package/package.json +14 -14
- package/src/lib/components/ConfirmDialog.svelte +59 -0
- package/src/lib/components/compute/ComputeApp.svelte +10 -0
- package/src/lib/components/compute/ParameterPresetManager.svelte +3 -3
- package/src/lib/components/layout/AppShell.svelte +19 -2
- package/src/lib/components/layout/SideNav.svelte +16 -2
- package/src/lib/components/preview/inputs/FileInput.svelte +3 -2
- package/src/lib/components/primitives/Callout.svelte +73 -0
- package/src/lib/components/primitives/index.ts +2 -0
- package/src/lib/components/viewer/MeshMetadataDialog.svelte +1 -1
- package/src/lib/components/viewer/SceneManager.svelte +274 -103
- package/src/lib/components/viewer/Viewer.svelte +104 -32
- package/src/lib/i18n/messages.ts +12 -6
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as AlertDialog from './primitives/alert-dialog/index.js';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
open = $bindable(false),
|
|
6
|
+
title,
|
|
7
|
+
description,
|
|
8
|
+
confirmLabel = 'Continue',
|
|
9
|
+
pendingLabel,
|
|
10
|
+
cancelLabel = 'Cancel',
|
|
11
|
+
variant = 'default',
|
|
12
|
+
onConfirm
|
|
13
|
+
}: {
|
|
14
|
+
open?: boolean;
|
|
15
|
+
title: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
confirmLabel?: string;
|
|
18
|
+
pendingLabel?: string;
|
|
19
|
+
cancelLabel?: string;
|
|
20
|
+
variant?: 'default' | 'destructive';
|
|
21
|
+
onConfirm: () => void | Promise<void>;
|
|
22
|
+
} = $props();
|
|
23
|
+
|
|
24
|
+
let pending = $state(false);
|
|
25
|
+
|
|
26
|
+
async function handleConfirm() {
|
|
27
|
+
pending = true;
|
|
28
|
+
try {
|
|
29
|
+
await onConfirm();
|
|
30
|
+
} finally {
|
|
31
|
+
pending = false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<AlertDialog.Root bind:open onOpenChange={(next) => !pending && (open = next)}>
|
|
37
|
+
<AlertDialog.Content>
|
|
38
|
+
<AlertDialog.Header>
|
|
39
|
+
<AlertDialog.Title>{title}</AlertDialog.Title>
|
|
40
|
+
{#if description}
|
|
41
|
+
<AlertDialog.Description class={variant === 'destructive' ? 'text-destructive' : undefined}>
|
|
42
|
+
{description}
|
|
43
|
+
</AlertDialog.Description>
|
|
44
|
+
{/if}
|
|
45
|
+
</AlertDialog.Header>
|
|
46
|
+
<AlertDialog.Footer>
|
|
47
|
+
<AlertDialog.Cancel disabled={pending}>{cancelLabel}</AlertDialog.Cancel>
|
|
48
|
+
<AlertDialog.Action
|
|
49
|
+
onclick={handleConfirm}
|
|
50
|
+
disabled={pending}
|
|
51
|
+
class={variant === 'destructive'
|
|
52
|
+
? 'text-destructive-foreground bg-destructive hover:bg-destructive/90'
|
|
53
|
+
: undefined}
|
|
54
|
+
>
|
|
55
|
+
{pending ? (pendingLabel ?? confirmLabel) : confirmLabel}
|
|
56
|
+
</AlertDialog.Action>
|
|
57
|
+
</AlertDialog.Footer>
|
|
58
|
+
</AlertDialog.Content>
|
|
59
|
+
</AlertDialog.Root>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
open?: boolean;
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
confirmLabel?: string;
|
|
6
|
+
pendingLabel?: string;
|
|
7
|
+
cancelLabel?: string;
|
|
8
|
+
variant?: 'default' | 'destructive';
|
|
9
|
+
onConfirm: () => void | Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
declare const ConfirmDialog: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
12
|
+
type ConfirmDialog = ReturnType<typeof ConfirmDialog>;
|
|
13
|
+
export default ConfirmDialog;
|
|
@@ -72,6 +72,10 @@
|
|
|
72
72
|
/** Viewer chrome and defaults. `backgroundColor` and `showSceneManager` are set by the layout. */
|
|
73
73
|
viewerConfig?: ViewerConfig;
|
|
74
74
|
headerRight?: Snippet;
|
|
75
|
+
// Primary nav rendered next to the brand, so the viewer keeps the app-wide nav.
|
|
76
|
+
navItems?: Snippet;
|
|
77
|
+
homeUrl?: string;
|
|
78
|
+
brandName?: string;
|
|
75
79
|
// Replaces the built-in header; takes precedence over `headerRight`.
|
|
76
80
|
header?: Snippet;
|
|
77
81
|
// Scopes sessionStorage for external-input values; falls back to definitionKey then schema.id.
|
|
@@ -106,6 +110,9 @@
|
|
|
106
110
|
footerItemId = 'footer-item',
|
|
107
111
|
footerItemPriority = 0,
|
|
108
112
|
headerRight,
|
|
113
|
+
navItems,
|
|
114
|
+
homeUrl,
|
|
115
|
+
brandName,
|
|
109
116
|
header,
|
|
110
117
|
onReady,
|
|
111
118
|
onViewerReady,
|
|
@@ -215,6 +222,9 @@
|
|
|
215
222
|
showFooter
|
|
216
223
|
title={pageTitle}
|
|
217
224
|
{showModeToggle}
|
|
225
|
+
{navItems}
|
|
226
|
+
{homeUrl}
|
|
227
|
+
{brandName}
|
|
218
228
|
{copyrightName}
|
|
219
229
|
{footerText}
|
|
220
230
|
{header}
|
|
@@ -56,6 +56,9 @@ interface Props {
|
|
|
56
56
|
/** Viewer chrome and defaults. `backgroundColor` and `showSceneManager` are set by the layout. */
|
|
57
57
|
viewerConfig?: ViewerConfig;
|
|
58
58
|
headerRight?: Snippet;
|
|
59
|
+
navItems?: Snippet;
|
|
60
|
+
homeUrl?: string;
|
|
61
|
+
brandName?: string;
|
|
59
62
|
header?: Snippet;
|
|
60
63
|
externalScopeKey?: string;
|
|
61
64
|
clientSlot?: ClientSlot;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
exportStateAsJson,
|
|
9
9
|
importStateFromJson
|
|
10
10
|
} from '../../schema/param-exporter';
|
|
11
|
-
import { Button, Input, Label, Textarea, Dialog, Card } from '../primitives';
|
|
11
|
+
import { Button, Input, Label, Textarea, Dialog, Card, toast } from '../primitives';
|
|
12
12
|
|
|
13
13
|
import type { ActionButton } from '../../types/actionButton';
|
|
14
14
|
import { DEFAULT_PRESET_LABELS, type PresetLabels } from '../../types/presetLabels';
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
|
|
69
69
|
async function handleExport() {
|
|
70
70
|
if (!exportName.trim()) {
|
|
71
|
-
|
|
71
|
+
toast.error(t.saveNameRequired);
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
const imported = await importStateFromJson(input.files[0]);
|
|
109
109
|
tryLoad(imported);
|
|
110
110
|
} catch (error) {
|
|
111
|
-
|
|
111
|
+
toast.error(t.loadImportError + (error as Error).message);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
// Reset so re-picking the same file fires `change` again.
|
|
@@ -81,6 +81,21 @@
|
|
|
81
81
|
);
|
|
82
82
|
|
|
83
83
|
const bodyShellStyle = '';
|
|
84
|
+
|
|
85
|
+
// In scroll mode the page itself scrolls, so the sidenav has to be pinned below the
|
|
86
|
+
// header or it scrolls away with the content. Fixed mode already gives the row its own
|
|
87
|
+
// height and the aside its own scroll — sticky there would only shrink it to content.
|
|
88
|
+
const sidenavWrapClass = $derived(
|
|
89
|
+
_mode === 'fixed'
|
|
90
|
+
? 'flex shrink-0'
|
|
91
|
+
: 'flex shrink-0 self-start sticky top-(--header-h) max-h-[calc(100svh-var(--header-h))]'
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// The sticky wrapper is only as tall as its content, so in scroll mode the divider comes
|
|
95
|
+
// from the main column's left edge — that one spans the full body height.
|
|
96
|
+
const mainClass = $derived(
|
|
97
|
+
_mode === 'fixed' ? 'flex-1 overflow-y-auto' : 'flex-1 overflow-y-auto border-l border-border'
|
|
98
|
+
);
|
|
84
99
|
</script>
|
|
85
100
|
|
|
86
101
|
<div class={rootClass}>
|
|
@@ -110,8 +125,10 @@
|
|
|
110
125
|
|
|
111
126
|
<div class={bodyShellClass} style={bodyShellStyle}>
|
|
112
127
|
{#if sidenav}
|
|
113
|
-
{
|
|
114
|
-
|
|
128
|
+
<div class={sidenavWrapClass}>
|
|
129
|
+
{@render sidenav()}
|
|
130
|
+
</div>
|
|
131
|
+
<main class={mainClass}>
|
|
115
132
|
{@render children()}
|
|
116
133
|
</main>
|
|
117
134
|
{:else}
|
|
@@ -19,10 +19,22 @@
|
|
|
19
19
|
eyebrow?: string;
|
|
20
20
|
header?: Snippet;
|
|
21
21
|
footer?: Snippet;
|
|
22
|
+
/**
|
|
23
|
+
* Off when the divider is drawn by the surrounding layout instead — a sticky sidenav
|
|
24
|
+
* only reaches as far as its own content, so its border would stop mid-page.
|
|
25
|
+
*/
|
|
26
|
+
border?: boolean;
|
|
22
27
|
class?: string;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
let {
|
|
30
|
+
let {
|
|
31
|
+
items,
|
|
32
|
+
eyebrow,
|
|
33
|
+
header,
|
|
34
|
+
footer,
|
|
35
|
+
border = true,
|
|
36
|
+
class: className = ''
|
|
37
|
+
}: SideNavProps = $props();
|
|
26
38
|
|
|
27
39
|
function isActive(item: SideNavItem): boolean {
|
|
28
40
|
const path = page.url.pathname;
|
|
@@ -32,7 +44,9 @@
|
|
|
32
44
|
</script>
|
|
33
45
|
|
|
34
46
|
<aside
|
|
35
|
-
class={`w-60 flex shrink-0 flex-col overflow-y-auto
|
|
47
|
+
class={`w-60 flex h-full shrink-0 flex-col overflow-y-auto bg-background ${
|
|
48
|
+
border ? 'border-r border-border' : ''
|
|
49
|
+
} ${className}`}
|
|
36
50
|
>
|
|
37
51
|
{#if eyebrow}
|
|
38
52
|
<div class="px-4 pt-5 pb-3">
|
|
@@ -12,6 +12,11 @@ interface SideNavProps {
|
|
|
12
12
|
eyebrow?: string;
|
|
13
13
|
header?: Snippet;
|
|
14
14
|
footer?: Snippet;
|
|
15
|
+
/**
|
|
16
|
+
* Off when the divider is drawn by the surrounding layout instead — a sticky sidenav
|
|
17
|
+
* only reaches as far as its own content, so its border would stop mid-page.
|
|
18
|
+
*/
|
|
19
|
+
border?: boolean;
|
|
15
20
|
class?: string;
|
|
16
21
|
}
|
|
17
22
|
declare const SideNav: Component<SideNavProps, {}, "">;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Input } from '../../primitives/input';
|
|
3
3
|
import { Button } from '../../primitives/button';
|
|
4
4
|
import { Label } from '../../primitives/label';
|
|
5
|
+
import { toast } from '../../primitives/sonner';
|
|
5
6
|
import { FileUp, Link, CircleAlert, CircleCheck } from '@lucide/svelte';
|
|
6
7
|
import { APP_DEFAULTS } from '../../../constants';
|
|
7
8
|
|
|
@@ -193,7 +194,7 @@
|
|
|
193
194
|
const fileEnding = getFileExtension(file.name);
|
|
194
195
|
|
|
195
196
|
if (!isValidFileExtension(fileEnding)) {
|
|
196
|
-
|
|
197
|
+
toast.error(`File format not accepted: ${fileEnding}`);
|
|
197
198
|
return;
|
|
198
199
|
}
|
|
199
200
|
|
|
@@ -203,7 +204,7 @@
|
|
|
203
204
|
// note in constants.ts) — some files pass here and still 413. The URL import
|
|
204
205
|
// path applies the same cap.
|
|
205
206
|
if (file.size > APP_DEFAULTS.FILE_UPLOAD.MAX_SIZE_BYTES) {
|
|
206
|
-
|
|
207
|
+
toast.error(
|
|
207
208
|
`File too large: ${(file.size / 1024 / 1024).toFixed(2)}MB (max ${APP_DEFAULTS.FILE_UPLOAD.MAX_SIZE_MB}MB).`
|
|
208
209
|
);
|
|
209
210
|
return;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { type VariantProps, tv } from 'tailwind-variants';
|
|
3
|
+
import { Info, TriangleAlert, CircleCheck, CircleX, Lightbulb } from '@lucide/svelte';
|
|
4
|
+
import type { Component } from 'svelte';
|
|
5
|
+
|
|
6
|
+
export const calloutVariants = tv({
|
|
7
|
+
base: 'flex items-start gap-2.5 rounded-lg border px-3.5 py-3 text-sm',
|
|
8
|
+
variants: {
|
|
9
|
+
tone: {
|
|
10
|
+
info: 'border-info/40 bg-info/10',
|
|
11
|
+
tip: 'border-border bg-muted/40',
|
|
12
|
+
success: 'border-success/40 bg-success/10',
|
|
13
|
+
warning: 'border-warning/40 bg-warning/10',
|
|
14
|
+
danger: 'border-destructive/40 bg-destructive/10'
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: { tone: 'info' }
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type CalloutTone = NonNullable<VariantProps<typeof calloutVariants>['tone']>;
|
|
21
|
+
|
|
22
|
+
const TONE_ICON: Record<CalloutTone, Component> = {
|
|
23
|
+
info: Info,
|
|
24
|
+
tip: Lightbulb,
|
|
25
|
+
success: CircleCheck,
|
|
26
|
+
warning: TriangleAlert,
|
|
27
|
+
danger: CircleX
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const TONE_ACCENT: Record<CalloutTone, string> = {
|
|
31
|
+
info: 'text-info',
|
|
32
|
+
tip: 'text-muted-foreground',
|
|
33
|
+
success: 'text-success',
|
|
34
|
+
warning: 'text-warning',
|
|
35
|
+
danger: 'text-destructive'
|
|
36
|
+
};
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<script lang="ts">
|
|
40
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
41
|
+
import type { Snippet } from 'svelte';
|
|
42
|
+
import { cn } from '../../utils.js';
|
|
43
|
+
|
|
44
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
45
|
+
tone?: CalloutTone;
|
|
46
|
+
title?: string;
|
|
47
|
+
/** Replaces the tone's default icon. `null` drops the icon entirely. */
|
|
48
|
+
icon?: Component | null;
|
|
49
|
+
children: Snippet;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let { tone = 'info', title, icon, class: className, children, ...restProps }: Props = $props();
|
|
53
|
+
|
|
54
|
+
const Icon = $derived(icon === undefined ? TONE_ICON[tone] : icon);
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<div
|
|
58
|
+
class={cn(calloutVariants({ tone }), className)}
|
|
59
|
+
role={tone === 'warning' || tone === 'danger' ? 'alert' : undefined}
|
|
60
|
+
{...restProps}
|
|
61
|
+
>
|
|
62
|
+
{#if Icon}
|
|
63
|
+
<Icon class={cn('mt-0.5 h-4 w-4 shrink-0', TONE_ACCENT[tone])} />
|
|
64
|
+
{/if}
|
|
65
|
+
<div class="min-w-0 space-y-1">
|
|
66
|
+
{#if title}
|
|
67
|
+
<p class={cn('leading-tight font-medium', TONE_ACCENT[tone])}>{title}</p>
|
|
68
|
+
{/if}
|
|
69
|
+
<div class="leading-relaxed text-muted-foreground [&_a]:underline [&_strong]:text-foreground">
|
|
70
|
+
{@render children()}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type VariantProps } from 'tailwind-variants';
|
|
2
|
+
import type { Component } from 'svelte';
|
|
3
|
+
export declare const calloutVariants: import("tailwind-variants").TVReturnType<{
|
|
4
|
+
tone: {
|
|
5
|
+
info: "border-info/40 bg-info/10";
|
|
6
|
+
tip: "border-border bg-muted/40";
|
|
7
|
+
success: "border-success/40 bg-success/10";
|
|
8
|
+
warning: "border-warning/40 bg-warning/10";
|
|
9
|
+
danger: "border-destructive/40 bg-destructive/10";
|
|
10
|
+
};
|
|
11
|
+
}, undefined, "flex items-start gap-2.5 rounded-lg border px-3.5 py-3 text-sm", {
|
|
12
|
+
tone: {
|
|
13
|
+
info: "border-info/40 bg-info/10";
|
|
14
|
+
tip: "border-border bg-muted/40";
|
|
15
|
+
success: "border-success/40 bg-success/10";
|
|
16
|
+
warning: "border-warning/40 bg-warning/10";
|
|
17
|
+
danger: "border-destructive/40 bg-destructive/10";
|
|
18
|
+
};
|
|
19
|
+
}, undefined, import("tailwind-variants").TVReturnTypeLike<{
|
|
20
|
+
tone: {
|
|
21
|
+
info: "border-info/40 bg-info/10";
|
|
22
|
+
tip: "border-border bg-muted/40";
|
|
23
|
+
success: "border-success/40 bg-success/10";
|
|
24
|
+
warning: "border-warning/40 bg-warning/10";
|
|
25
|
+
danger: "border-destructive/40 bg-destructive/10";
|
|
26
|
+
};
|
|
27
|
+
}, undefined>>;
|
|
28
|
+
export type CalloutTone = NonNullable<VariantProps<typeof calloutVariants>['tone']>;
|
|
29
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
30
|
+
import type { Snippet } from 'svelte';
|
|
31
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
32
|
+
tone?: CalloutTone;
|
|
33
|
+
title?: string;
|
|
34
|
+
/** Replaces the tone's default icon. `null` drops the icon entirely. */
|
|
35
|
+
icon?: Component | null;
|
|
36
|
+
children: Snippet;
|
|
37
|
+
}
|
|
38
|
+
declare const Callout: Component<Props, {}, "">;
|
|
39
|
+
type Callout = ReturnType<typeof Callout>;
|
|
40
|
+
export default Callout;
|
|
@@ -22,9 +22,11 @@ export { Switch } from './switch';
|
|
|
22
22
|
export { Toaster, toast } from './sonner';
|
|
23
23
|
export { ThemeSwitcher } from './theme-switcher';
|
|
24
24
|
export { default as StateDisplay } from './StateDisplay.svelte';
|
|
25
|
+
export { default as Callout, calloutVariants, type CalloutTone } from './Callout.svelte';
|
|
25
26
|
export { default as CalculateButton } from './CalculateButton.svelte';
|
|
26
27
|
export { ModeToggle } from './mode-toggle';
|
|
27
28
|
export { default as ViewToggle } from './ViewToggle.svelte';
|
|
28
29
|
export { default as ImageUploadField } from './ImageUploadField.svelte';
|
|
29
30
|
export { default as DataTable, type DataTableColumn } from './DataTable.svelte';
|
|
30
31
|
export { default as FilterableDropdown, type FilterableDropdownItem } from './FilterableDropdown.svelte';
|
|
32
|
+
export { default as ConfirmDialog } from '../ConfirmDialog.svelte';
|
|
@@ -24,9 +24,11 @@ export { Toaster, toast } from './sonner';
|
|
|
24
24
|
export { ThemeSwitcher } from './theme-switcher';
|
|
25
25
|
// Custom components (not replaced by shadcn)
|
|
26
26
|
export { default as StateDisplay } from './StateDisplay.svelte';
|
|
27
|
+
export { default as Callout, calloutVariants } from './Callout.svelte';
|
|
27
28
|
export { default as CalculateButton } from './CalculateButton.svelte';
|
|
28
29
|
export { ModeToggle } from './mode-toggle';
|
|
29
30
|
export { default as ViewToggle } from './ViewToggle.svelte';
|
|
30
31
|
export { default as ImageUploadField } from './ImageUploadField.svelte';
|
|
31
32
|
export { default as DataTable } from './DataTable.svelte';
|
|
32
33
|
export { default as FilterableDropdown } from './FilterableDropdown.svelte';
|
|
34
|
+
export { default as ConfirmDialog } from '../ConfirmDialog.svelte';
|