@selvajs/ui 6.3.2 → 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.
- package/dist/components/compute/ComputeApp.svelte +32 -2
- package/dist/components/compute/ComputeApp.svelte.d.ts +7 -1
- package/dist/components/compute/ComputeMessagesDialog.svelte +118 -95
- package/dist/components/compute/ComputeMessagesDialog.svelte.d.ts +4 -0
- package/dist/components/compute/SolveMessages.svelte +367 -0
- package/dist/components/compute/SolveMessages.svelte.d.ts +28 -0
- package/dist/components/layout/AppShell.svelte +5 -1
- package/dist/components/layout/AppShell.svelte.d.ts +3 -0
- package/dist/components/layout/PageFooter.svelte +34 -14
- package/dist/components/layout/PageFooter.svelte.d.ts +3 -0
- package/dist/compute/useSolveSession.svelte.js +28 -4
- package/dist/i18n/messages.d.ts +9 -0
- package/dist/i18n/messages.js +18 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -1
- package/dist/public.d.ts +2 -2
- package/package.json +16 -16
- package/src/lib/components/compute/ComputeApp.svelte +32 -2
- package/src/lib/components/compute/ComputeMessagesDialog.svelte +118 -95
- package/src/lib/components/compute/SolveMessages.svelte +367 -0
- package/src/lib/components/layout/AppShell.svelte +5 -1
- package/src/lib/components/layout/PageFooter.svelte +34 -14
- package/src/lib/compute/useSolveSession.svelte.ts +28 -4
- package/src/lib/i18n/messages.ts +28 -0
- package/src/lib/index.ts +10 -2
- package/src/lib/public.ts +4 -2
|
@@ -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,
|
|
4
|
-
import
|
|
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 {
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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-
|
|
105
|
+
<Dialog.Content class="max-w-lg gap-4">
|
|
32
106
|
<Dialog.Header>
|
|
33
|
-
<Dialog.Title
|
|
34
|
-
<CircleAlert class="h-5 w-5" />
|
|
35
|
-
Compute Messages
|
|
36
|
-
</Dialog.Title>
|
|
107
|
+
<Dialog.Title>Messages</Dialog.Title>
|
|
37
108
|
<Dialog.Description>
|
|
38
|
-
{
|
|
39
|
-
|
|
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-
|
|
44
|
-
{#
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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. */
|