@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.
Files changed (32) hide show
  1. package/dist/components/ConfirmDialog.svelte +59 -0
  2. package/dist/components/ConfirmDialog.svelte.d.ts +13 -0
  3. package/dist/components/compute/ComputeApp.svelte +10 -0
  4. package/dist/components/compute/ComputeApp.svelte.d.ts +3 -0
  5. package/dist/components/compute/ParameterPresetManager.svelte +3 -3
  6. package/dist/components/layout/AppShell.svelte +19 -2
  7. package/dist/components/layout/SideNav.svelte +16 -2
  8. package/dist/components/layout/SideNav.svelte.d.ts +5 -0
  9. package/dist/components/preview/inputs/FileInput.svelte +3 -2
  10. package/dist/components/primitives/Callout.svelte +73 -0
  11. package/dist/components/primitives/Callout.svelte.d.ts +40 -0
  12. package/dist/components/primitives/index.d.ts +2 -0
  13. package/dist/components/primitives/index.js +2 -0
  14. package/dist/components/viewer/MeshMetadataDialog.svelte +1 -1
  15. package/dist/components/viewer/SceneManager.svelte +274 -103
  16. package/dist/components/viewer/SceneManager.svelte.d.ts +6 -0
  17. package/dist/components/viewer/Viewer.svelte +104 -32
  18. package/dist/i18n/messages.d.ts +4 -2
  19. package/dist/i18n/messages.js +8 -4
  20. package/package.json +14 -14
  21. package/src/lib/components/ConfirmDialog.svelte +59 -0
  22. package/src/lib/components/compute/ComputeApp.svelte +10 -0
  23. package/src/lib/components/compute/ParameterPresetManager.svelte +3 -3
  24. package/src/lib/components/layout/AppShell.svelte +19 -2
  25. package/src/lib/components/layout/SideNav.svelte +16 -2
  26. package/src/lib/components/preview/inputs/FileInput.svelte +3 -2
  27. package/src/lib/components/primitives/Callout.svelte +73 -0
  28. package/src/lib/components/primitives/index.ts +2 -0
  29. package/src/lib/components/viewer/MeshMetadataDialog.svelte +1 -1
  30. package/src/lib/components/viewer/SceneManager.svelte +274 -103
  31. package/src/lib/components/viewer/Viewer.svelte +104 -32
  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
- alert(t.saveNameRequired);
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
- alert(t.loadImportError + (error as Error).message);
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
- {@render sidenav()}
114
- <main class="flex-1 overflow-y-auto">
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 { items, eyebrow, header, footer, class: className = '' }: SideNavProps = $props();
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 border-r border-border bg-background ${className}`}
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
- alert(`File format not accepted: ${fileEnding}`);
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
- alert(
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';
@@ -13,7 +13,7 @@
13
13
  isFullscreen?: boolean;
14
14
  }
15
15
 
16
- const EXCLUDED_KEYS = new Set(['name', 'layer', 'originalIndex', 'sourceComponentId']);
16
+ const EXCLUDED_KEYS = new Set(['name', 'layer', 'id']);
17
17
 
18
18
  let {
19
19
  open = $bindable(),