@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
@@ -41,6 +41,15 @@ const en = {
41
41
  objectFallbackName: 'Object',
42
42
  noMetadata: 'No metadata',
43
43
  loadingSchema: 'Loading schema...',
44
+ solveBlocked: 'The definition stopped this solve. No results were produced.',
45
+ solveBlockedTitle: 'Solve stopped',
46
+ solveMessagesTitle: 'Solve messages',
47
+ solveMessagesDescription: 'This solve reported the following. Continue to see the result.',
48
+ solveMessagesConfirm: 'Continue',
49
+ solveMessagesDiscard: 'Discard',
50
+ solveMessagesAbort: 'Abort',
51
+ solveMessagesDismiss: 'Dismiss',
52
+ solveSolving: 'Solving…',
44
53
  parametersTab: 'Parameters',
45
54
  moreTab: 'More',
46
55
  expandPanel: 'Expand panel',
@@ -92,6 +101,15 @@ const de = {
92
101
  objectFallbackName: 'Objekt',
93
102
  noMetadata: 'Keine Metadaten',
94
103
  loadingSchema: 'Schema wird geladen...',
104
+ solveBlocked: 'Die Definition hat diese Berechnung gestoppt. Es wurden keine Ergebnisse erzeugt.',
105
+ solveBlockedTitle: 'Berechnung gestoppt',
106
+ solveMessagesTitle: 'Meldungen zur Berechnung',
107
+ solveMessagesDescription: 'Diese Berechnung hat Folgendes gemeldet. Weiter, um das Ergebnis zu sehen.',
108
+ solveMessagesConfirm: 'Weiter',
109
+ solveMessagesDiscard: 'Verwerfen',
110
+ solveMessagesAbort: 'Abbrechen',
111
+ solveMessagesDismiss: 'Schliessen',
112
+ solveSolving: 'Berechnung läuft…',
95
113
  parametersTab: 'Parameter',
96
114
  moreTab: 'Mehr',
97
115
  expandPanel: 'Bereich aufklappen',
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './components/layout';
2
2
  export { default as AppLayout } from './components/compute/AppLayout.svelte';
3
3
  export { default as ComputeApp } from './components/compute/ComputeApp.svelte';
4
+ export { default as SolveMessages } from './components/compute/SolveMessages.svelte';
4
5
  export { default as ErrorScreen } from './components/ErrorScreen.svelte';
5
6
  export * from './components/primitives';
6
7
  export { default as StateDisplay } from './components/primitives/StateDisplay.svelte';
@@ -10,7 +11,7 @@ export * from './schema/dynamic-value-list';
10
11
  export * from './schema/traversal';
11
12
  export * from './compute/solving.svelte';
12
13
  export { useSolveSession } from './compute/useSolveSession.svelte';
13
- export { createSolveSession, createRequestResponseDriver, type SolveSession, type SolveSessionArgs, type SolveDriver, type SolveReporter } from '@selvajs/solve/client';
14
+ export { createSolveSession, createRequestResponseDriver, type SolveSession, type SolveSessionArgs, type SolveDriver, type SolveReporter, type LiveSolveState, type SolvePhase, EMPTY_LIVE_SOLVE, decodeOutcome, finalizeResult, type SolveOutcome } from '@selvajs/solve/client';
14
15
  export { writeExternalValue, readExternalValue, clearExternalValue, getExternalInputs, type ExternalValueRef, type ExternalInput } from '@selvajs/solve/client';
15
16
  export * from './contexts/footerContext.svelte';
16
17
  export * from './contexts/clientSlotContext.svelte';
@@ -18,5 +19,5 @@ export * from './composables/useFooterItem.svelte';
18
19
  export * from './utils';
19
20
  export { randomId } from './utils/randomId';
20
21
  export type { ActionButton } from './types/actionButton';
21
- export type { SolveFn, SolveResult } from '@selvajs/solve/shared';
22
+ export type { SolveFn, SolveResult, SolveDiagnostic } from '@selvajs/solve/shared';
22
23
  export { DEFAULT_PRESET_LABELS, type PresetLabels } from './types/presetLabels';
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  export * from './components/layout';
5
5
  export { default as AppLayout } from './components/compute/AppLayout.svelte';
6
6
  export { default as ComputeApp } from './components/compute/ComputeApp.svelte';
7
+ export { default as SolveMessages } from './components/compute/SolveMessages.svelte';
7
8
  export { default as ErrorScreen } from './components/ErrorScreen.svelte';
8
9
  // Design-system primitives (shadcn-svelte + custom)
9
10
  export * from './components/primitives';
@@ -17,7 +18,9 @@ export * from './compute/solving.svelte';
17
18
  // plugin-ui's WebSocket driver — can satisfy SolveDriver and drive a session.
18
19
  // See CONTEXT.md.
19
20
  export { useSolveSession } from './compute/useSolveSession.svelte';
20
- export { createSolveSession, createRequestResponseDriver } from '@selvajs/solve/client';
21
+ export { createSolveSession, createRequestResponseDriver,
22
+ // A push driver (plugin-ui's WebSocket) decodes the plugin's verdict with these.
23
+ EMPTY_LIVE_SOLVE, decodeOutcome, finalizeResult } from '@selvajs/solve/client';
21
24
  // Pre-step producer transit storage
22
25
  export { writeExternalValue, readExternalValue, clearExternalValue, getExternalInputs } from '@selvajs/solve/client';
23
26
  export * from './contexts/footerContext.svelte';
package/dist/public.d.ts CHANGED
@@ -8,12 +8,12 @@ export { VIEWER_MESSAGES, DEFAULT_LOCALE, messagesFor } from './i18n/messages';
8
8
  export { setLocaleContext, getLocaleContext, type LocaleContext } from './i18n/localeContext.svelte';
9
9
  export { default as ErrorScreen } from './components/ErrorScreen.svelte';
10
10
  export { useSolveSession } from './compute/useSolveSession.svelte';
11
- export { createSolveSession, createRequestResponseDriver, type SolveSession, type SolveSessionArgs, type SolveDriver, type SolveReporter } from '@selvajs/solve/client';
11
+ export { createSolveSession, createRequestResponseDriver, type SolveSession, type SolveSessionArgs, type SolveDriver, type SolveReporter, type LiveSolveState, type SolvePhase } from '@selvajs/solve/client';
12
12
  export type { ClientSlotArgs, ClientSlot } from './contexts/clientSlotContext.svelte';
13
13
  export { writeExternalValue, readExternalValue, clearExternalValue, getExternalInputs, type ExternalValueRef, type ExternalInput } from '@selvajs/solve/client';
14
14
  export * from './schema/defaults';
15
15
  export * from './schema/traversal';
16
16
  export * from './schema/dynamic-value-list';
17
17
  export type { ActionButton } from './types/actionButton';
18
- export type { SolveFn, SolveResult } from '@selvajs/solve/shared';
18
+ export type { SolveFn, SolveResult, SolveDiagnostic } from '@selvajs/solve/shared';
19
19
  export { DEFAULT_PRESET_LABELS, type PresetLabels } from './types/presetLabels';
@@ -17,4 +17,10 @@ export declare function itemKey(item: LayoutItem): string;
17
17
  export declare function buildVisibilityMap(items: LayoutItem[], values: Record<string, unknown>): Record<string, VisibilityResult>;
18
18
  export declare function evaluateGroupVisibility(group: {
19
19
  visibilityCondition?: GroupVisibilityCondition;
20
+ items?: LayoutItem[];
20
21
  }, values: Record<string, unknown>): boolean;
22
+ /**
23
+ * A linebreak is always "visible" but renders as a divider only, so a group left with
24
+ * nothing but separators counts as empty.
25
+ */
26
+ export declare function hasVisibleItems(items: LayoutItem[], values: Record<string, unknown>): boolean;
@@ -74,9 +74,18 @@ export function buildVisibilityMap(items, values) {
74
74
  return map;
75
75
  }
76
76
  export function evaluateGroupVisibility(group, values) {
77
+ if (group.items && !hasVisibleItems(group.items, values))
78
+ return false;
77
79
  if (!group.visibilityCondition?.rules)
78
80
  return true;
79
81
  const { action = 'show' } = group.visibilityCondition;
80
82
  const met = evaluateCondition(group.visibilityCondition, values);
81
83
  return action === 'hide' ? !met : met;
82
84
  }
85
+ /**
86
+ * A linebreak is always "visible" but renders as a divider only, so a group left with
87
+ * nothing but separators counts as empty.
88
+ */
89
+ export function hasVisibleItems(items, values) {
90
+ return items.some((item) => item.type !== 'linebreak' && evaluateVisibility(item, values).visible);
91
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/ui",
3
- "version": "6.3.1",
3
+ "version": "6.4.0",
4
4
  "description": "Shared UI components and utilities for Selva applications",
5
5
  "license": "MIT",
6
6
  "author": "VektorNode",
@@ -58,11 +58,11 @@
58
58
  "bits-ui": "^2.18.0",
59
59
  "svelte": "^5",
60
60
  "tailwind-variants": "^3.3.1",
61
- "three": "^0.185.1",
62
- "@selvajs/compute": "^4.1.1",
63
- "@selvajs/schemas": "^5.0.2",
64
- "@selvajs/visualization": "^1.3.0",
65
- "@selvajs/solve": "^1.1.0"
61
+ "three": "^0.186.1",
62
+ "@selvajs/compute": "^4.2.0",
63
+ "@selvajs/schemas": "^5.1.0",
64
+ "@selvajs/solve": "^1.2.0",
65
+ "@selvajs/visualization": "^1.4.0"
66
66
  },
67
67
  "peerDependenciesMeta": {
68
68
  "three": {
@@ -72,29 +72,29 @@
72
72
  "dependencies": {
73
73
  "@floating-ui/dom": "^1.8.0",
74
74
  "@iconify/svelte": "^5.2.1",
75
- "@lucide/svelte": "^1.35.0",
75
+ "@lucide/svelte": "^1.48.0",
76
76
  "clsx": "^2.1.1",
77
77
  "mode-watcher": "^1.1.0",
78
78
  "paneforge": "^1.0.2",
79
79
  "svelte-sonner": "^1.2.1",
80
- "tailwind-merge": "^3.5.0",
80
+ "tailwind-merge": "^3.7.0",
81
81
  "tw-animate-css": "^1.4.0"
82
82
  },
83
83
  "devDependencies": {
84
- "@internationalized/date": "^3.12.3",
84
+ "@internationalized/date": "^3.12.4",
85
85
  "@sveltejs/kit": "2.70.3",
86
- "@sveltejs/vite-plugin-svelte": "^7.3.0",
87
- "@types/three": "^0.185.4",
88
- "bits-ui": "^2.19.0",
86
+ "@sveltejs/vite-plugin-svelte": "^7.3.1",
87
+ "@types/three": "^0.186.0",
88
+ "bits-ui": "^2.19.3",
89
89
  "rimraf": "^6.1.3",
90
- "svelte": "5.56.10",
90
+ "svelte": "5.57.1",
91
91
  "tailwind-variants": "^3.3.1",
92
- "vitest": "^4.1.11",
93
- "@selvajs/compute": "4.1.1",
92
+ "vitest": "^5.0.2",
93
+ "@selvajs/compute": "4.2.0",
94
94
  "@selvajs/config": "0.0.4",
95
- "@selvajs/visualization": "1.3.0",
96
- "@selvajs/solve": "1.1.0",
97
- "@selvajs/schemas": "5.0.2"
95
+ "@selvajs/visualization": "1.4.0",
96
+ "@selvajs/solve": "1.2.0",
97
+ "@selvajs/schemas": "5.1.0"
98
98
  },
99
99
  "scripts": {
100
100
  "predev": "node ../../scripts/sync-shared-assets.js",
@@ -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
+ />
@@ -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 '$lib/components/primitives/collapsible';
10
+ import { CircleAlert, TriangleAlert, Info } from '@lucide/svelte';
11
+ import type { SolveDiagnostic } from '@selvajs/solve/shared';
5
12
  import * as Dialog from '$lib/components/primitives/dialog';
6
- import { groupMessages } from '$lib/utils/file-download';
13
+ import { Badge } from '$lib/components/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>