@selvajs/ui 6.3.1 → 6.4.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 (31) hide show
  1. package/dist/components/compute/ComputeApp.svelte +32 -2
  2. package/dist/components/compute/ComputeApp.svelte.d.ts +7 -1
  3. package/dist/components/compute/ComputeMessagesDialog.svelte +118 -95
  4. package/dist/components/compute/ComputeMessagesDialog.svelte.d.ts +4 -0
  5. package/dist/components/compute/SolveMessages.svelte +367 -0
  6. package/dist/components/compute/SolveMessages.svelte.d.ts +28 -0
  7. package/dist/components/layout/AppShell.svelte +5 -1
  8. package/dist/components/layout/AppShell.svelte.d.ts +3 -0
  9. package/dist/components/layout/PageFooter.svelte +34 -14
  10. package/dist/components/layout/PageFooter.svelte.d.ts +3 -0
  11. package/dist/components/preview/TabContent.svelte +18 -16
  12. package/dist/compute/useSolveSession.svelte.js +28 -4
  13. package/dist/i18n/messages.d.ts +9 -0
  14. package/dist/i18n/messages.js +18 -0
  15. package/dist/index.d.ts +3 -2
  16. package/dist/index.js +4 -1
  17. package/dist/public.d.ts +2 -2
  18. package/dist/schema/visibility-rules.d.ts +6 -0
  19. package/dist/schema/visibility-rules.js +9 -0
  20. package/package.json +18 -18
  21. package/src/lib/components/compute/ComputeApp.svelte +32 -2
  22. package/src/lib/components/compute/ComputeMessagesDialog.svelte +118 -95
  23. package/src/lib/components/compute/SolveMessages.svelte +367 -0
  24. package/src/lib/components/layout/AppShell.svelte +5 -1
  25. package/src/lib/components/layout/PageFooter.svelte +34 -14
  26. package/src/lib/components/preview/TabContent.svelte +18 -16
  27. package/src/lib/compute/useSolveSession.svelte.ts +28 -4
  28. package/src/lib/i18n/messages.ts +28 -0
  29. package/src/lib/index.ts +10 -2
  30. package/src/lib/public.ts +4 -2
  31. package/src/lib/schema/visibility-rules.ts +12 -1
@@ -7,7 +7,8 @@
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 { RetainedSolveResult, SolveSession } from '@selvajs/solve/client';
10
+ import type { RetainedSolveResult, SolveEventSource, SolveSession } from '@selvajs/solve/client';
11
+ import SolveMessages from './SolveMessages.svelte';
11
12
  import { meshPolicy } from '@selvajs/visualization/parse';
12
13
  import type { ThreeViewer } from '@selvajs/visualization/render';
13
14
  import { useSolveSession } from '../../compute/useSolveSession.svelte';
@@ -52,6 +53,12 @@
52
53
  * have finished.
53
54
  */
54
55
  solveDeadlineMs: number;
56
+ /**
57
+ * Live events for the solves this app runs (`createSolveEventStream`). Pair it with
58
+ * `streamId` on the `createComputeFetchSolveFn` that backs `onSolve`, or the server has
59
+ * no stream to route to. Without it the app works as before, just with no mid-solve news.
60
+ */
61
+ events?: SolveEventSource;
55
62
  footerComponent?: any;
56
63
  footerComponentProps?: () => Record<string, unknown>;
57
64
  footerItemId?: string;
@@ -105,6 +112,7 @@
105
112
  copyrightName,
106
113
  footerText,
107
114
  solveDeadlineMs,
115
+ events,
108
116
  footerComponent,
109
117
  footerComponentProps,
110
118
  footerItemId = 'footer-item',
@@ -146,7 +154,8 @@
146
154
  meshPolicy,
147
155
  // `isSolving` lives on the driver, which the session can't observe — republish so the
148
156
  // spinner and disabled states track it.
149
- onChange: () => session.notify()
157
+ onChange: () => session.notify(),
158
+ events
150
159
  });
151
160
  // svelte-ignore state_referenced_locally
152
161
  const session = useSolveSession({
@@ -231,6 +240,7 @@
231
240
  rightContent={headerRight}
232
241
  errors={session.computeErrors}
233
242
  warnings={session.computeWarnings}
243
+ diagnostics={session.diagnostics}
234
244
  >
235
245
  {#if session.error}
236
246
  <div class="min-h-100 p-8 flex items-center justify-center">
@@ -268,3 +278,23 @@
268
278
  {/if}
269
279
  </AppShell>
270
280
  </div>
281
+
282
+ <SolveMessages
283
+ live={session.live}
284
+ phase={session.phase}
285
+ diagnostics={session.diagnostics}
286
+ onabort={() => session.abort()}
287
+ onconfirm={() => session.acknowledge()}
288
+ 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
+ />
@@ -2,7 +2,7 @@ import type { UISchema, ParameterPreset } from '@selvajs/schemas';
2
2
  import type { ActionButton } from '../../types/actionButton';
3
3
  import type { SolveFn } from '@selvajs/solve/shared';
4
4
  import type { PresetLabels } from '../../types/presetLabels';
5
- import type { RetainedSolveResult, SolveSession } from '@selvajs/solve/client';
5
+ import type { RetainedSolveResult, SolveEventSource, SolveSession } from '@selvajs/solve/client';
6
6
  import type { ThreeViewer } from '@selvajs/visualization/render';
7
7
  import { type ViewerConfig } from '../viewer/Viewer.svelte';
8
8
  import { type ClientSlot } from '../../contexts/clientSlotContext.svelte';
@@ -37,6 +37,12 @@ interface Props {
37
37
  * have finished.
38
38
  */
39
39
  solveDeadlineMs: number;
40
+ /**
41
+ * Live events for the solves this app runs (`createSolveEventStream`). Pair it with
42
+ * `streamId` on the `createComputeFetchSolveFn` that backs `onSolve`, or the server has
43
+ * no stream to route to. Without it the app works as before, just with no mid-solve news.
44
+ */
45
+ events?: SolveEventSource;
40
46
  footerComponent?: any;
41
47
  footerComponentProps?: () => Record<string, unknown>;
42
48
  footerItemId?: string;
@@ -1,26 +1,100 @@
1
1
  <script lang="ts">
2
+ // The message centre: every message the last solve raised, reached from the footer badge.
3
+ //
4
+ // The counterpart to `SolveMessages`, which interrupts only for what the definition's author
5
+ // wrote. Everything else — Grasshopper complaining about its own components — lands here,
6
+ // where it is available without being in the way. One section per level, most severe first, with the
7
+ // component that raised each message shown as a label rather than spliced into its text.
8
+
2
9
  import type { Snippet } from 'svelte';
3
- import { CircleAlert, TriangleAlert, ChevronDown, ChevronRight } from '@lucide/svelte';
4
- import * as Collapsible from '../primitives/collapsible';
10
+ import { CircleAlert, TriangleAlert, Info } from '@lucide/svelte';
11
+ import type { SolveDiagnostic } from '@selvajs/solve/shared';
5
12
  import * as Dialog from '../primitives/dialog';
6
- import { groupMessages } from '../../utils/file-download';
13
+ import { Badge } from '../primitives/badge';
7
14
 
8
15
  interface Props {
16
+ /** The last solve's messages. Preferred over `errors`/`warnings` when both are given. */
17
+ diagnostics?: SolveDiagnostic[];
18
+ /** Fallback for hosts that only have the flat text. */
9
19
  errors?: string[];
10
20
  warnings?: string[];
11
21
  /** Renders inside Dialog.Trigger. */
12
22
  trigger: Snippet;
13
23
  }
14
24
 
15
- let { errors = [], warnings = [], trigger }: Props = $props();
25
+ let { diagnostics, errors = [], warnings = [], trigger }: Props = $props();
16
26
 
17
27
  let open = $state(false);
18
- let showErrors = $state(true);
19
- let showWarnings = $state(false);
20
28
 
21
- const groupedErrors = $derived(groupMessages(errors));
22
- const groupedWarnings = $derived(groupMessages(warnings));
23
- const totalCount = $derived(errors.length + warnings.length);
29
+ const rows = $derived<SolveDiagnostic[]>(
30
+ diagnostics?.length
31
+ ? diagnostics
32
+ : [
33
+ ...errors.map((message) => ({ level: 'error' as const, message })),
34
+ ...warnings.map((message) => ({ level: 'warning' as const, message }))
35
+ ]
36
+ );
37
+
38
+ interface Item {
39
+ level: SolveDiagnostic['level'];
40
+ message: string;
41
+ source?: string;
42
+ count: number;
43
+ }
44
+
45
+ // Tone classes are written out, never interpolated: Tailwind scans source text, so a
46
+ // constructed class name is not emitted and the icon would render colourless.
47
+ const LEVELS = [
48
+ {
49
+ level: 'error',
50
+ heading: 'Errors',
51
+ one: 'error',
52
+ many: 'errors',
53
+ icon: CircleAlert,
54
+ tone: 'text-destructive'
55
+ },
56
+ {
57
+ level: 'warning',
58
+ heading: 'Warnings',
59
+ one: 'warning',
60
+ many: 'warnings',
61
+ icon: TriangleAlert,
62
+ tone: 'text-amber-500'
63
+ },
64
+ {
65
+ level: 'remark',
66
+ heading: 'Notes',
67
+ one: 'note',
68
+ many: 'notes',
69
+ icon: Info,
70
+ tone: 'text-muted-foreground'
71
+ }
72
+ ] as const;
73
+
74
+ /**
75
+ * One section per level that has messages, most severe first; within a level, the order the
76
+ * solve raised them. Repeats collapse into one row with a count: a definition fanning over 200
77
+ * branches raises the same warning 200 times, and an unstacked list buries every other message.
78
+ */
79
+ const sections = $derived(
80
+ LEVELS.map((meta) => {
81
+ // A null-prototype object rather than a Map: a local built fresh on each derive, never
82
+ // reactive state, and the lint rule that steers Map → SvelteMap cannot tell them apart.
83
+ const byKey: Record<string, Item> = Object.create(null);
84
+ const items: Item[] = [];
85
+ for (const d of rows) {
86
+ if (d.level !== meta.level) continue;
87
+ const key = `${d.message} ${d.source ?? ''}`;
88
+ if (byKey[key]) {
89
+ byKey[key].count++;
90
+ } else {
91
+ byKey[key] = { level: d.level, message: d.message, source: d.source, count: 1 };
92
+ items.push(byKey[key]);
93
+ }
94
+ }
95
+ return { ...meta, items };
96
+ }).filter((section) => section.items.length > 0)
97
+ );
24
98
  </script>
25
99
 
26
100
  <Dialog.Root bind:open>
@@ -28,98 +102,47 @@
28
102
  {@render trigger()}
29
103
  </Dialog.Trigger>
30
104
 
31
- <Dialog.Content class="max-w-2xl max-h-[80vh]">
105
+ <Dialog.Content class="max-w-lg gap-4">
32
106
  <Dialog.Header>
33
- <Dialog.Title class="gap-2 flex items-center">
34
- <CircleAlert class="h-5 w-5" />
35
- Compute Messages
36
- </Dialog.Title>
107
+ <Dialog.Title>Messages</Dialog.Title>
37
108
  <Dialog.Description>
38
- {totalCount}
39
- {totalCount === 1 ? 'issue' : 'issues'} detected during solve
109
+ {#each sections as section, i (section.level)}
110
+ {#if i > 0}<span class="mx-1.5">·</span>{/if}
111
+ <span
112
+ >{section.items.length} {section.items.length === 1 ? section.one : section.many}</span
113
+ >
114
+ {/each}
40
115
  </Dialog.Description>
41
116
  </Dialog.Header>
42
117
 
43
- <div class="space-y-3 pr-2 overflow-y-auto" style="max-height: calc(80vh - 180px);">
44
- {#if errors.length > 0}
45
- <Collapsible.Root bind:open={showErrors}>
46
- <div class="overflow-hidden rounded-lg border border-destructive bg-card">
47
- <div class="px-4 py-3 flex items-center">
48
- <Collapsible.Trigger
49
- class="-mx-4 -my-3 gap-3 px-4 py-3 flex flex-1 items-center text-left transition-colors hover:bg-destructive/5"
50
- >
51
- {#if showErrors}
52
- <ChevronDown class="h-4 w-4 shrink-0 text-destructive" />
53
- {:else}
54
- <ChevronRight class="h-4 w-4 shrink-0 text-destructive" />
118
+ <div class="space-y-4 max-h-[60vh] overflow-y-auto">
119
+ {#each sections as section (section.level)}
120
+ <section>
121
+ <h3 class="mb-1.5 gap-1.5 text-xs font-medium flex items-center text-muted-foreground">
122
+ <section.icon class="h-3.5 w-3.5 {section.tone}" />
123
+ {section.heading}
124
+ </h3>
125
+ <ul class="divide-y rounded-lg border">
126
+ {#each section.items as item (item.message + (item.source ?? ''))}
127
+ <li class="gap-3 px-3 py-2 text-sm flex items-start justify-between">
128
+ <p class="min-w-0 wrap-anywhere">
129
+ {item.message}
130
+ {#if item.count > 1}
131
+ <span class="ml-1 text-xs text-muted-foreground tabular-nums"
132
+ >×{item.count}</span
133
+ >
134
+ {/if}
135
+ </p>
136
+ {#if item.source}
137
+ <Badge variant="secondary" class="font-normal max-w-[40%] shrink-0 truncate">
138
+ {item.source}
139
+ </Badge>
55
140
  {/if}
56
- <CircleAlert class="h-4 w-4 shrink-0 text-destructive" />
57
- <span class="text-sm font-medium text-destructive">
58
- {errors.length === 1 ? '1 Error' : `${errors.length} Errors`}
59
- </span>
60
- </Collapsible.Trigger>
61
- </div>
62
-
63
- <Collapsible.Content class="space-y-0">
64
- <div class="max-h-60 px-4 py-3 overflow-y-auto border-t border-destructive bg-card">
65
- <ul class="space-y-2">
66
- {#each groupedErrors as { message, count } (message)}
67
- <li class="gap-2 text-sm flex text-destructive/90">
68
- <span class="shrink-0">•</span>
69
- <span class="flex-1">
70
- {message}
71
- {#if count > 1}
72
- <span class="ml-1 font-medium text-destructive/70">×{count} </span>
73
- {/if}
74
- </span>
75
- </li>
76
- {/each}
77
- </ul>
78
- </div>
79
- </Collapsible.Content>
80
- </div>
81
- </Collapsible.Root>
82
- {/if}
83
-
84
- {#if warnings.length > 0}
85
- <Collapsible.Root bind:open={showWarnings}>
86
- <div class="overflow-hidden rounded-lg border border-warning/50 bg-card">
87
- <div class="px-4 py-3 flex items-center">
88
- <Collapsible.Trigger
89
- class="-mx-4 -my-3 gap-3 px-4 py-3 flex flex-1 items-center text-left transition-colors hover:bg-warning/5"
90
- >
91
- {#if showWarnings}
92
- <ChevronDown class="h-4 w-4 shrink-0 text-warning" />
93
- {:else}
94
- <ChevronRight class="h-4 w-4 shrink-0 text-warning" />
95
- {/if}
96
- <TriangleAlert class="h-4 w-4 shrink-0 text-warning" />
97
- <span class="text-sm font-medium text-warning">
98
- {warnings.length === 1 ? '1 Warning' : `${warnings.length} Warnings`}
99
- </span>
100
- </Collapsible.Trigger>
101
- </div>
102
-
103
- <Collapsible.Content class="space-y-0">
104
- <div class="max-h-60 px-4 py-3 overflow-y-auto border-t border-warning/50 bg-card">
105
- <ul class="space-y-2">
106
- {#each groupedWarnings as { message, count } (message)}
107
- <li class="gap-2 text-sm flex text-muted-foreground">
108
- <span class="shrink-0">•</span>
109
- <span class="flex-1">
110
- {message}
111
- {#if count > 1}
112
- <span class="ml-1 font-medium text-warning/70">×{count}</span>
113
- {/if}
114
- </span>
115
- </li>
116
- {/each}
117
- </ul>
118
- </div>
119
- </Collapsible.Content>
120
- </div>
121
- </Collapsible.Root>
122
- {/if}
141
+ </li>
142
+ {/each}
143
+ </ul>
144
+ </section>
145
+ {/each}
123
146
  </div>
124
147
  </Dialog.Content>
125
148
  </Dialog.Root>
@@ -1,5 +1,9 @@
1
1
  import type { Snippet } from 'svelte';
2
+ import type { SolveDiagnostic } from '@selvajs/solve/shared';
2
3
  interface Props {
4
+ /** The last solve's messages. Preferred over `errors`/`warnings` when both are given. */
5
+ diagnostics?: SolveDiagnostic[];
6
+ /** Fallback for hosts that only have the flat text. */
3
7
  errors?: string[];
4
8
  warnings?: string[];
5
9
  /** Renders inside Dialog.Trigger. */