@happyvertical/smrt-svelte 0.55.7 → 0.55.8

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 (197) hide show
  1. package/AGENTS.md +7 -44
  2. package/README.md +112 -1
  3. package/agents/browser-ai.md +47 -0
  4. package/agents/command-palette.md +85 -0
  5. package/agents/import-export.md +62 -0
  6. package/agents/overview-surfaces.md +255 -0
  7. package/agents/screens.md +57 -0
  8. package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts +5 -0
  9. package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts.map +1 -1
  10. package/dist/browser-ai/adapters/tts/browser-synthesis.js +83 -55
  11. package/dist/components/command-palette/CommandPalette.svelte +75 -0
  12. package/dist/components/command-palette/CommandPalette.svelte.d.ts +22 -0
  13. package/dist/components/command-palette/CommandPalette.svelte.d.ts.map +1 -0
  14. package/dist/components/command-palette/CommandPaletteDialog.svelte +406 -0
  15. package/dist/components/command-palette/CommandPaletteDialog.svelte.d.ts +18 -0
  16. package/dist/components/command-palette/CommandPaletteDialog.svelte.d.ts.map +1 -0
  17. package/dist/components/command-palette/CommandPaletteTrigger.svelte +91 -0
  18. package/dist/components/command-palette/CommandPaletteTrigger.svelte.d.ts +17 -0
  19. package/dist/components/command-palette/CommandPaletteTrigger.svelte.d.ts.map +1 -0
  20. package/dist/components/command-palette/context.d.ts +18 -0
  21. package/dist/components/command-palette/context.d.ts.map +1 -0
  22. package/dist/components/command-palette/context.js +38 -0
  23. package/dist/components/command-palette/controller.svelte.d.ts +70 -0
  24. package/dist/components/command-palette/controller.svelte.d.ts.map +1 -0
  25. package/dist/components/command-palette/controller.svelte.js +379 -0
  26. package/dist/components/command-palette/hotkeys.d.ts +30 -0
  27. package/dist/components/command-palette/hotkeys.d.ts.map +1 -0
  28. package/dist/components/command-palette/hotkeys.js +161 -0
  29. package/dist/components/command-palette/index.d.ts +19 -0
  30. package/dist/components/command-palette/index.d.ts.map +1 -0
  31. package/dist/components/command-palette/index.js +17 -0
  32. package/dist/components/command-palette/match.d.ts +38 -0
  33. package/dist/components/command-palette/match.d.ts.map +1 -0
  34. package/dist/components/command-palette/match.js +172 -0
  35. package/dist/components/command-palette/providers.d.ts +159 -0
  36. package/dist/components/command-palette/providers.d.ts.map +1 -0
  37. package/dist/components/command-palette/providers.js +345 -0
  38. package/dist/components/command-palette/rank.d.ts +20 -0
  39. package/dist/components/command-palette/rank.d.ts.map +1 -0
  40. package/dist/components/command-palette/rank.js +74 -0
  41. package/dist/components/command-palette/types.d.ts +119 -0
  42. package/dist/components/command-palette/types.d.ts.map +1 -0
  43. package/dist/components/command-palette/types.js +10 -0
  44. package/dist/components/command-palette/use-provider.svelte.d.ts +14 -0
  45. package/dist/components/command-palette/use-provider.svelte.d.ts.map +1 -0
  46. package/dist/components/command-palette/use-provider.svelte.js +23 -0
  47. package/dist/components/import-export/ImportExport.svelte +678 -0
  48. package/dist/components/import-export/ImportExport.svelte.d.ts +5 -0
  49. package/dist/components/import-export/ImportExport.svelte.d.ts.map +1 -0
  50. package/dist/components/import-export/collection-adapter.d.ts +35 -0
  51. package/dist/components/import-export/collection-adapter.d.ts.map +1 -0
  52. package/dist/components/import-export/collection-adapter.js +42 -0
  53. package/dist/components/import-export/csv.d.ts +60 -0
  54. package/dist/components/import-export/csv.d.ts.map +1 -0
  55. package/dist/components/import-export/csv.js +192 -0
  56. package/dist/components/import-export/download.d.ts +4 -0
  57. package/dist/components/import-export/download.d.ts.map +1 -0
  58. package/dist/components/import-export/download.js +17 -0
  59. package/dist/components/import-export/export.d.ts +25 -0
  60. package/dist/components/import-export/export.d.ts.map +1 -0
  61. package/dist/components/import-export/export.js +68 -0
  62. package/dist/components/import-export/fields.d.ts +48 -0
  63. package/dist/components/import-export/fields.d.ts.map +1 -0
  64. package/dist/components/import-export/fields.js +103 -0
  65. package/dist/components/import-export/index.d.ts +12 -0
  66. package/dist/components/import-export/index.d.ts.map +1 -0
  67. package/dist/components/import-export/index.js +9 -0
  68. package/dist/components/import-export/mapping.d.ts +12 -0
  69. package/dist/components/import-export/mapping.d.ts.map +1 -0
  70. package/dist/components/import-export/mapping.js +38 -0
  71. package/dist/components/import-export/props.d.ts +34 -0
  72. package/dist/components/import-export/props.d.ts.map +1 -0
  73. package/dist/components/import-export/props.js +1 -0
  74. package/dist/components/import-export/report.d.ts +13 -0
  75. package/dist/components/import-export/report.d.ts.map +1 -0
  76. package/dist/components/import-export/report.js +50 -0
  77. package/dist/components/import-export/runner.d.ts +17 -0
  78. package/dist/components/import-export/runner.d.ts.map +1 -0
  79. package/dist/components/import-export/runner.js +39 -0
  80. package/dist/components/import-export/types.d.ts +100 -0
  81. package/dist/components/import-export/types.d.ts.map +1 -0
  82. package/dist/components/import-export/types.js +6 -0
  83. package/dist/components/import-export/validate.d.ts +39 -0
  84. package/dist/components/import-export/validate.d.ts.map +1 -0
  85. package/dist/components/import-export/validate.js +268 -0
  86. package/dist/components/overview/OverviewGrid.svelte +579 -0
  87. package/dist/components/overview/OverviewGrid.svelte.d.ts +5 -0
  88. package/dist/components/overview/OverviewGrid.svelte.d.ts.map +1 -0
  89. package/dist/components/overview/WidgetOptionsForm.svelte +193 -0
  90. package/dist/components/overview/WidgetOptionsForm.svelte.d.ts +5 -0
  91. package/dist/components/overview/WidgetOptionsForm.svelte.d.ts.map +1 -0
  92. package/dist/components/overview/controller.svelte.d.ts +92 -0
  93. package/dist/components/overview/controller.svelte.d.ts.map +1 -0
  94. package/dist/components/overview/controller.svelte.js +336 -0
  95. package/dist/components/overview/format.d.ts +13 -0
  96. package/dist/components/overview/format.d.ts.map +1 -0
  97. package/dist/components/overview/format.js +45 -0
  98. package/dist/components/overview/grid-types.d.ts +52 -0
  99. package/dist/components/overview/grid-types.d.ts.map +1 -0
  100. package/dist/components/overview/grid-types.js +1 -0
  101. package/dist/components/overview/index.d.ts +24 -0
  102. package/dist/components/overview/index.d.ts.map +1 -0
  103. package/dist/components/overview/index.js +22 -0
  104. package/dist/components/overview/load.d.ts +30 -0
  105. package/dist/components/overview/load.d.ts.map +1 -0
  106. package/dist/components/overview/load.js +92 -0
  107. package/dist/components/overview/markdown.d.ts +53 -0
  108. package/dist/components/overview/markdown.d.ts.map +1 -0
  109. package/dist/components/overview/markdown.js +203 -0
  110. package/dist/components/overview/model.d.ts +100 -0
  111. package/dist/components/overview/model.d.ts.map +1 -0
  112. package/dist/components/overview/model.js +460 -0
  113. package/dist/components/overview/recipe-widgets.d.ts +77 -0
  114. package/dist/components/overview/recipe-widgets.d.ts.map +1 -0
  115. package/dist/components/overview/recipe-widgets.js +151 -0
  116. package/dist/components/overview/registry.d.ts +45 -0
  117. package/dist/components/overview/registry.d.ts.map +1 -0
  118. package/dist/components/overview/registry.js +112 -0
  119. package/dist/components/overview/resize.d.ts +25 -0
  120. package/dist/components/overview/resize.d.ts.map +1 -0
  121. package/dist/components/overview/resize.js +41 -0
  122. package/dist/components/overview/schema.d.ts +34 -0
  123. package/dist/components/overview/schema.d.ts.map +1 -0
  124. package/dist/components/overview/schema.js +180 -0
  125. package/dist/components/overview/server.d.ts +14 -0
  126. package/dist/components/overview/server.d.ts.map +1 -0
  127. package/dist/components/overview/server.js +13 -0
  128. package/dist/components/overview/types.d.ts +235 -0
  129. package/dist/components/overview/types.d.ts.map +1 -0
  130. package/dist/components/overview/types.js +17 -0
  131. package/dist/components/overview/widgets/ChartWidget.svelte +111 -0
  132. package/dist/components/overview/widgets/ChartWidget.svelte.d.ts +6 -0
  133. package/dist/components/overview/widgets/ChartWidget.svelte.d.ts.map +1 -0
  134. package/dist/components/overview/widgets/MetricWidget.svelte +86 -0
  135. package/dist/components/overview/widgets/MetricWidget.svelte.d.ts +6 -0
  136. package/dist/components/overview/widgets/MetricWidget.svelte.d.ts.map +1 -0
  137. package/dist/components/overview/widgets/NoteWidget.svelte +61 -0
  138. package/dist/components/overview/widgets/NoteWidget.svelte.d.ts +5 -0
  139. package/dist/components/overview/widgets/NoteWidget.svelte.d.ts.map +1 -0
  140. package/dist/components/overview/widgets/RecordListWidget.svelte +91 -0
  141. package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts +6 -0
  142. package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts.map +1 -0
  143. package/dist/components/overview/widgets/ShortcutsWidget.svelte +80 -0
  144. package/dist/components/overview/widgets/ShortcutsWidget.svelte.d.ts +6 -0
  145. package/dist/components/overview/widgets/ShortcutsWidget.svelte.d.ts.map +1 -0
  146. package/dist/components/overview/widgets/core.d.ts +44 -0
  147. package/dist/components/overview/widgets/core.d.ts.map +1 -0
  148. package/dist/components/overview/widgets/core.js +222 -0
  149. package/dist/components/overview/widgets/data.d.ts +76 -0
  150. package/dist/components/overview/widgets/data.d.ts.map +1 -0
  151. package/dist/components/overview/widgets/data.js +40 -0
  152. package/dist/components/screens/DetailScreen.svelte +220 -0
  153. package/dist/components/screens/DetailScreen.svelte.d.ts +5 -0
  154. package/dist/components/screens/DetailScreen.svelte.d.ts.map +1 -0
  155. package/dist/components/screens/EditForm.svelte +313 -0
  156. package/dist/components/screens/EditForm.svelte.d.ts +5 -0
  157. package/dist/components/screens/EditForm.svelte.d.ts.map +1 -0
  158. package/dist/components/screens/ListScreen.svelte +191 -0
  159. package/dist/components/screens/ListScreen.svelte.d.ts +5 -0
  160. package/dist/components/screens/ListScreen.svelte.d.ts.map +1 -0
  161. package/dist/components/screens/RecipeScreens.svelte +237 -0
  162. package/dist/components/screens/RecipeScreens.svelte.d.ts +5 -0
  163. package/dist/components/screens/RecipeScreens.svelte.d.ts.map +1 -0
  164. package/dist/components/screens/fields.d.ts +54 -0
  165. package/dist/components/screens/fields.d.ts.map +1 -0
  166. package/dist/components/screens/fields.js +229 -0
  167. package/dist/components/screens/index.d.ts +8 -0
  168. package/dist/components/screens/index.d.ts.map +1 -0
  169. package/dist/components/screens/index.js +6 -0
  170. package/dist/components/screens/types.d.ts +215 -0
  171. package/dist/components/screens/types.d.ts.map +1 -0
  172. package/dist/components/screens/types.js +1 -0
  173. package/dist/components/screens/values.d.ts +62 -0
  174. package/dist/components/screens/values.d.ts.map +1 -0
  175. package/dist/components/screens/values.js +289 -0
  176. package/dist/components/sortable/controller.svelte.d.ts +10 -0
  177. package/dist/components/sortable/controller.svelte.d.ts.map +1 -1
  178. package/dist/components/sortable/controller.svelte.js +29 -7
  179. package/dist/components/workspace/manifest-nav.d.ts +20 -0
  180. package/dist/components/workspace/manifest-nav.d.ts.map +1 -1
  181. package/dist/components/workspace/manifest-nav.js +42 -13
  182. package/dist/i18n/server.d.ts +6 -0
  183. package/dist/i18n/server.d.ts.map +1 -1
  184. package/dist/i18n/server.js +6 -0
  185. package/dist/i18n/strings.import-export.d.ts +88 -0
  186. package/dist/i18n/strings.import-export.d.ts.map +1 -0
  187. package/dist/i18n/strings.import-export.js +88 -0
  188. package/dist/i18n/strings.overview.d.ts +100 -0
  189. package/dist/i18n/strings.overview.d.ts.map +1 -0
  190. package/dist/i18n/strings.overview.js +107 -0
  191. package/dist/i18n/strings.palette.d.ts +18 -0
  192. package/dist/i18n/strings.palette.d.ts.map +1 -0
  193. package/dist/i18n/strings.palette.js +18 -0
  194. package/dist/i18n/strings.screens.d.ts +31 -0
  195. package/dist/i18n/strings.screens.d.ts.map +1 -0
  196. package/dist/i18n/strings.screens.js +31 -0
  197. package/package.json +45 -16
@@ -0,0 +1,579 @@
1
+ <script lang="ts">
2
+ /**
3
+ * OverviewGrid - a customizable overview as a flow grid of widgets (#3727).
4
+ *
5
+ * Widgets keep an order and a column span of 1 to 4 (the grid draws 4, 2 or 1
6
+ * columns by its own width); there is no free x/y. Rendering needs nothing but
7
+ * the controller: widget data arrives with the page's server load, so a
8
+ * server render and the hydrated client are the same tree.
9
+ *
10
+ * Edit mode is the shell's: inside an `AppShell` with `layoutEditing`, the
11
+ * same pencil that edits the navigation edits the grid (pass `editing` to
12
+ * drive it without a shell). While editing, each widget shows icon-only
13
+ * controls (a drag handle, configure, remove), is rendered exactly as it is
14
+ * on the site inside an `inert` wrapper, and has a resize handle on its end
15
+ * edge. Everything works from the keyboard: Space on the handle picks a widget
16
+ * up, the arrow keys move it, Enter drops, Escape cancels; the resize handle
17
+ * is a slider (arrows, Home, End).
18
+ */
19
+ import { Modal } from '@happyvertical/smrt-ui/feedback';
20
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
21
+ import { Button } from '@happyvertical/smrt-ui/ui';
22
+ import { untrack } from 'svelte';
23
+ import { M } from '../../i18n/strings.overview.js';
24
+ import {
25
+ createSortable,
26
+ type SortableAnnouncement,
27
+ } from '../sortable/controller.svelte.js';
28
+ import { tryUseShellLayout } from '../workspace/admin-shell/layout-context.js';
29
+ import ShellIconButton from '../workspace/admin-shell/ShellIconButton.svelte';
30
+ import ShellSectionIcon from '../workspace/admin-shell/ShellSectionIcon.svelte';
31
+ import type { OverviewOpResult } from './controller.svelte.js';
32
+ import type { OverviewGridProps } from './grid-types.js';
33
+ import { spanFromKey, spanFromPointer } from './resize.js';
34
+ import { defaultWidgetOptions } from './schema.js';
35
+ import type { OverviewOptions, OverviewWidget } from './types.js';
36
+ import WidgetOptionsForm from './WidgetOptionsForm.svelte';
37
+
38
+ let {
39
+ controller,
40
+ components,
41
+ editing: editingProp,
42
+ label,
43
+ headingLevel = 2,
44
+ models,
45
+ iconComponent,
46
+ }: OverviewGridProps = $props();
47
+
48
+ const { t, locale } = useI18n();
49
+ const uid = $props.id();
50
+ const LIST = '__smrt-overview__';
51
+ const shellLayout = tryUseShellLayout();
52
+
53
+ const editing = $derived(
54
+ controller.canCustomize && (editingProp ?? shellLayout?.editing ?? false),
55
+ );
56
+ const gridLabel = $derived(label ?? t(M['ui.overview.label']));
57
+ const registry = $derived(controller.registry);
58
+
59
+ interface Row {
60
+ widget: OverviewWidget;
61
+ title: string;
62
+ status: 'ready' | 'loading' | 'error';
63
+ data: unknown;
64
+ def: ReturnType<typeof registry.get>;
65
+ }
66
+
67
+ const rows = $derived<Row[]>(
68
+ controller.document.widgets.map((widget) => {
69
+ const def = registry.get(widget.type);
70
+ const custom =
71
+ typeof widget.options.title === 'string'
72
+ ? widget.options.title.trim()
73
+ : '';
74
+ const entry = controller.entry(widget);
75
+ return {
76
+ widget,
77
+ def,
78
+ title: custom || (def ? t(def.title) : widget.type),
79
+ status: entry.status,
80
+ data: entry.data,
81
+ };
82
+ }),
83
+ );
84
+ const rowById = $derived(new Map(rows.map((row) => [row.widget.id, row])));
85
+
86
+ // Data for widgets the entries do not cover yet (an add, a host-owned change).
87
+ // Client only: effects never run during server render.
88
+ $effect(() => {
89
+ controller.document;
90
+ untrack(() => controller.sync());
91
+ });
92
+
93
+ let rootEl: HTMLElement | undefined = $state();
94
+ let listEl: HTMLElement | undefined = $state();
95
+ let liveMessage = $state('');
96
+ let resizing = $state<{ id: string; span: number } | null>(null);
97
+ let addOpen = $state(false);
98
+ let configure = $state<{
99
+ id: string | null;
100
+ type: string;
101
+ initial: OverviewOptions;
102
+ } | null>(null);
103
+
104
+ function say(message: string): void {
105
+ liveMessage = message;
106
+ }
107
+
108
+ function sortableMessage(announcement: SortableAnnouncement): string {
109
+ switch (announcement.type) {
110
+ case 'pickup':
111
+ return t(M['ui.overview.announce_pickup'], { widget: announcement.item });
112
+ case 'position':
113
+ return t(M['ui.overview.announce_position'], {
114
+ widget: announcement.item,
115
+ position: announcement.position,
116
+ count: announcement.count,
117
+ });
118
+ case 'drop':
119
+ return t(M['ui.overview.announce_drop'], {
120
+ widget: announcement.item,
121
+ position: announcement.position,
122
+ count: announcement.count,
123
+ });
124
+ case 'cancel':
125
+ return t(M['ui.overview.announce_cancel'], { widget: announcement.item });
126
+ case 'failed':
127
+ return t(M['ui.overview.announce_failed'], { widget: announcement.item });
128
+ default:
129
+ return '';
130
+ }
131
+ }
132
+
133
+ const sortable = createSortable({
134
+ root: () => rootEl,
135
+ selectors: {
136
+ container: '[data-smrt-overview-list]',
137
+ containerKey: 'smrtOverviewList',
138
+ item: '[data-smrt-overview-item]',
139
+ itemKey: 'smrtOverviewItem',
140
+ },
141
+ containers: () => [{ id: LIST, label: gridLabel }],
142
+ itemIds: () => rows.map((row) => row.widget.id),
143
+ itemLabel: (id) => rowById.get(id)?.title,
144
+ allowSameContainerReorder: () => true,
145
+ enabled: () => editing,
146
+ orientation: () => 'vertical',
147
+ flow: () => true,
148
+ announce: sortableMessage,
149
+ focusTarget: (element) =>
150
+ element.querySelector<HTMLElement>('[data-smrt-overview-handle]'),
151
+ commit(move) {
152
+ const result = controller.move(move.itemId, move.target.index);
153
+ if (!result.ok && result.reason !== 'unchanged') {
154
+ throw new Error(result.reason);
155
+ }
156
+ },
157
+ });
158
+
159
+ // Where a drag would drop: before the item at this rank, or after the last.
160
+ const dragged = $derived(sortable.drag?.itemId);
161
+ const dropAt = $derived(sortable.drag?.target.index);
162
+ const restCount = $derived(
163
+ rows.filter((row) => row.widget.id !== dragged).length,
164
+ );
165
+
166
+ function dropMark(index: number): 'before' | 'after' | undefined {
167
+ if (dropAt === undefined || dragged === undefined) return undefined;
168
+ const id = rows[index].widget.id;
169
+ if (id === dragged) return undefined;
170
+ let rank = 0;
171
+ for (let i = 0; i < index; i += 1)
172
+ if (rows[i].widget.id !== dragged) rank += 1;
173
+ if (rank === dropAt) return 'before';
174
+ if (dropAt >= restCount && rank === restCount - 1) return 'after';
175
+ return undefined;
176
+ }
177
+
178
+ function spanOf(row: Row): number {
179
+ return resizing?.id === row.widget.id ? resizing.span : row.widget.span;
180
+ }
181
+
182
+ function isRtl(): boolean {
183
+ return rootEl !== undefined && getComputedStyle(rootEl).direction === 'rtl';
184
+ }
185
+
186
+ function commitResize(row: Row, span: number): void {
187
+ const result = controller.resize(row.widget.id, span);
188
+ if (result.ok) {
189
+ say(
190
+ t(M['ui.overview.announce_resized'], {
191
+ widget: row.title,
192
+ span,
193
+ max: row.def?.maxSpan ?? 4,
194
+ }),
195
+ );
196
+ } else if (result.reason === 'not_allowed') {
197
+ say(t(M['ui.overview.announce_denied']));
198
+ }
199
+ }
200
+
201
+ function resizeKey(event: KeyboardEvent, row: Row): void {
202
+ if (!row.def) return;
203
+ const next = spanFromKey(
204
+ event.key,
205
+ row.widget.span,
206
+ row.def.minSpan,
207
+ row.def.maxSpan,
208
+ isRtl(),
209
+ );
210
+ if (next === null) return;
211
+ event.preventDefault();
212
+ if (next !== row.widget.span) commitResize(row, next);
213
+ }
214
+
215
+ function resizeDown(event: PointerEvent, row: Row): void {
216
+ if (event.button !== 0 || !row.def) return;
217
+ event.preventDefault();
218
+ (event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId);
219
+ resizing = { id: row.widget.id, span: row.widget.span };
220
+ }
221
+
222
+ function resizeMove(event: PointerEvent, row: Row): void {
223
+ if (resizing?.id !== row.widget.id || !row.def || !listEl) return;
224
+ const item = (event.currentTarget as HTMLElement).closest(
225
+ '[data-smrt-overview-item]',
226
+ );
227
+ if (!item) return;
228
+ const list = listEl.getBoundingClientRect();
229
+ const tile = item.getBoundingClientRect();
230
+ const rtl = isRtl();
231
+ const columns = getComputedStyle(listEl)
232
+ .gridTemplateColumns.split(' ')
233
+ .filter(Boolean).length;
234
+ const span = spanFromPointer({
235
+ pointer: event.clientX,
236
+ start: rtl ? tile.right : tile.left,
237
+ rtl,
238
+ gridWidth: list.width,
239
+ columns: columns || 1,
240
+ gap: 0,
241
+ min: row.def.minSpan,
242
+ max: row.def.maxSpan,
243
+ });
244
+ if (span !== resizing.span) resizing = { id: row.widget.id, span };
245
+ }
246
+
247
+ function resizeUp(event: PointerEvent, row: Row): void {
248
+ if (resizing?.id !== row.widget.id) return;
249
+ const span = resizing.span;
250
+ resizing = null;
251
+ const target = event.currentTarget as HTMLElement;
252
+ if (target.hasPointerCapture?.(event.pointerId)) {
253
+ target.releasePointerCapture(event.pointerId);
254
+ }
255
+ if (span !== row.widget.span) commitResize(row, span);
256
+ }
257
+
258
+ function remove(row: Row): void {
259
+ const result = controller.remove(row.widget.id);
260
+ if (result.ok)
261
+ say(t(M['ui.overview.announce_removed'], { widget: row.title }));
262
+ }
263
+
264
+ function reset(): void {
265
+ const result = controller.reset();
266
+ if (result.ok) say(t(M['ui.overview.announce_reset']));
267
+ }
268
+
269
+ function startConfigure(row: Row): void {
270
+ configure = {
271
+ id: row.widget.id,
272
+ type: row.widget.type,
273
+ initial: row.widget.options,
274
+ };
275
+ }
276
+
277
+ function titleFor(type: string): string {
278
+ const def = registry.get(type);
279
+ return def ? t(def.title) : type;
280
+ }
281
+
282
+ function chooseType(type: string): void {
283
+ const result = controller.add(type);
284
+ addOpen = false;
285
+ if (result.ok) {
286
+ say(t(M['ui.overview.announce_added'], { widget: titleFor(type) }));
287
+ return;
288
+ }
289
+ if (result.reason === 'invalid_options') {
290
+ const def = registry.get(type);
291
+ configure = {
292
+ id: null,
293
+ type,
294
+ initial: def ? defaultWidgetOptions(def.options) : {},
295
+ };
296
+ }
297
+ }
298
+
299
+ function saveOptions(
300
+ options: OverviewOptions,
301
+ ): { key: string; code: string }[] | null {
302
+ const current = configure;
303
+ if (!current) return null;
304
+ const result: OverviewOpResult =
305
+ current.id === null
306
+ ? controller.add(current.type, options)
307
+ : controller.setOptions(current.id, options);
308
+ if (result.ok) {
309
+ say(
310
+ t(
311
+ M[
312
+ current.id === null
313
+ ? 'ui.overview.announce_added'
314
+ : 'ui.overview.announce_configured'
315
+ ],
316
+ { widget: titleFor(current.type) },
317
+ ),
318
+ );
319
+ configure = null;
320
+ return null;
321
+ }
322
+ if (result.reason === 'invalid_options') return result.issues ?? [];
323
+ if (result.reason === 'not_allowed') say(t(M['ui.overview.announce_denied']));
324
+ configure = null;
325
+ return null;
326
+ }
327
+
328
+ const configureDef = $derived(
329
+ configure ? registry.get(configure.type) : undefined,
330
+ );
331
+ const configureTitle = $derived(
332
+ configure
333
+ ? t(
334
+ M[
335
+ configure.id === null
336
+ ? 'ui.overview.options_new_title'
337
+ : 'ui.overview.options_title'
338
+ ],
339
+ {
340
+ widget: titleFor(configure.type),
341
+ },
342
+ )
343
+ : '',
344
+ );
345
+ const headingTag = $derived(`h${headingLevel}`);
346
+ </script>
347
+
348
+ {#snippet body(row: Row)}
349
+ {@const Widget = components?.get(row.widget.type) ?? row.def?.component}
350
+ <article class="smrt-overview__widget" aria-labelledby="{uid}-title-{row.widget.id}">
351
+ <svelte:element this={headingTag} class="smrt-overview__title" id="{uid}-title-{row.widget.id}">{row.title}</svelte:element>
352
+ <div class="smrt-overview__content" aria-busy={row.status === 'loading'}>
353
+ {#if !row.def}
354
+ <p class="smrt-overview__state">{t(M['ui.overview.unavailable'])}</p>
355
+ {:else if row.status === 'loading'}
356
+ <p class="smrt-overview__state">{t(M['ui.overview.loading'])}</p>
357
+ {:else if row.status === 'error'}
358
+ <p class="smrt-overview__state" role="status">{t(M['ui.overview.error'])}</p>
359
+ <Button type="button" variant="ghost" size="sm" onclick={() => controller.reload(row.widget.id)}>
360
+ {t(M['ui.overview.retry'])}
361
+ </Button>
362
+ {:else if Widget}
363
+ <Widget
364
+ id={row.widget.id}
365
+ options={row.widget.options}
366
+ data={row.data}
367
+ span={spanOf(row)}
368
+ title={row.title}
369
+ {locale}
370
+ {iconComponent}
371
+ />
372
+ {:else}
373
+ <p class="smrt-overview__state">{t(M['ui.overview.unavailable'])}</p>
374
+ {/if}
375
+ </div>
376
+ </article>
377
+ {/snippet}
378
+
379
+ <div class="smrt-overview" bind:this={rootEl} data-editing={editing ? '' : undefined}>
380
+ {#if editing}
381
+ <div class="smrt-overview__toolbar" role="toolbar" aria-label={t(M['ui.overview.toolbar'])}>
382
+ <ShellIconButton
383
+ icon="add"
384
+ label={t(M['ui.overview.add'])}
385
+ disabled={controller.full || controller.addable.length === 0}
386
+ onclick={() => (addOpen = true)}
387
+ />
388
+ <ShellIconButton
389
+ icon="undo"
390
+ label={t(M['ui.overview.reset'])}
391
+ disabled={!controller.customized}
392
+ onclick={reset}
393
+ />
394
+ </div>
395
+ {/if}
396
+
397
+ {#if rows.length === 0}
398
+ <p class="smrt-overview__empty">{t(M['ui.overview.empty'])}</p>
399
+ {/if}
400
+ <div
401
+ bind:this={listEl}
402
+ class="smrt-overview__list"
403
+ role="list"
404
+ aria-label={gridLabel}
405
+ data-smrt-overview-list={LIST}
406
+ ondragover={(event) => event.preventDefault()}
407
+ ondrop={(event) => {
408
+ if (sortable.drag) sortable.dropOnContainer(event, LIST);
409
+ }}
410
+ >
411
+ {#each rows as row, index (row.widget.id)}
412
+ {@const mark = dropMark(index)}
413
+ <div
414
+ role="listitem"
415
+ class="smrt-overview__item"
416
+ data-span={spanOf(row)}
417
+ data-smrt-overview-item={row.widget.id}
418
+ data-dragging={dragged === row.widget.id ? '' : undefined}
419
+ data-drop={mark}
420
+ style:--span={spanOf(row)}
421
+ ondragover={(event) => event.preventDefault()}
422
+ ondrop={(event) => {
423
+ if (!sortable.drag) return;
424
+ event.stopPropagation();
425
+ sortable.dropOnItem(event, LIST, index);
426
+ }}
427
+ >
428
+ <div class="smrt-overview__tile">
429
+ {#if editing}
430
+ <div class="smrt-overview__chrome">
431
+ <!-- raw-primitive-allow: native button owns keyboard pickup and HTML drag/drop -->
432
+ <button
433
+ type="button"
434
+ class="smrt-overview__handle"
435
+ data-smrt-overview-handle
436
+ draggable={sortable.nativeDraggable}
437
+ aria-label={t(M['ui.overview.move'], { widget: row.title })}
438
+ aria-pressed={dragged === row.widget.id}
439
+ onclick={() => sortable.consumeClick(row.widget.id)}
440
+ onkeydown={(event) => sortable.keydown(event, row.widget.id)}
441
+ onblur={(event) => sortable.blur(event, row.widget.id)}
442
+ onpointerdown={(event) => sortable.pointerDown(event, row.widget.id)}
443
+ onpointermove={(event) => sortable.pointerMove(event)}
444
+ onpointerup={(event) => sortable.pointerUp(event)}
445
+ onpointercancel={(event) => sortable.pointerCancel(event)}
446
+ ondragstart={(event) => sortable.dragStart(event, row.widget.id)}
447
+ ondragend={() => sortable.dragEnd()}
448
+ ><span aria-hidden="true">⠿</span></button>
449
+ <span class="smrt-overview__chrome-title" aria-hidden="true">{row.title}</span>
450
+ {#if row.def && row.def.options.length > 0}
451
+ <ShellIconButton
452
+ icon="settings"
453
+ label={t(M['ui.overview.configure'], { widget: row.title })}
454
+ onclick={() => startConfigure(row)}
455
+ />
456
+ {/if}
457
+ <ShellIconButton
458
+ icon="trash"
459
+ label={t(M['ui.overview.remove'], { widget: row.title })}
460
+ onclick={() => remove(row)}
461
+ />
462
+ </div>
463
+ <div class="smrt-overview__inert" inert>
464
+ {@render body(row)}
465
+ </div>
466
+ {#if row.def}
467
+ <div
468
+ class="smrt-overview__resize"
469
+ role="slider"
470
+ tabindex="0"
471
+ aria-label={t(M['ui.overview.resize'], { widget: row.title })}
472
+ aria-orientation="horizontal"
473
+ aria-valuemin={row.def.minSpan}
474
+ aria-valuemax={row.def.maxSpan}
475
+ aria-valuenow={spanOf(row)}
476
+ aria-valuetext={t(M['ui.overview.resize_value'], { span: spanOf(row), max: row.def.maxSpan })}
477
+ onkeydown={(event) => resizeKey(event, row)}
478
+ onpointerdown={(event) => resizeDown(event, row)}
479
+ onpointermove={(event) => resizeMove(event, row)}
480
+ onpointerup={(event) => resizeUp(event, row)}
481
+ onpointercancel={(event) => resizeUp(event, row)}
482
+ ></div>
483
+ {/if}
484
+ {:else}
485
+ {@render body(row)}
486
+ {/if}
487
+ </div>
488
+ </div>
489
+ {/each}
490
+ </div>
491
+
492
+ <div class="smrt-overview__live" role="status" aria-live="polite" aria-atomic="true">{liveMessage}{sortable.announcement ? ` ${sortable.announcement}` : ''}</div>
493
+ </div>
494
+
495
+ {#if editing}
496
+ <Modal bind:open={() => addOpen, (v) => (addOpen = v)} title={t(M['ui.overview.add_title'])} size="sm">
497
+ {#if controller.addable.length === 0 || controller.full}
498
+ <p>{t(M['ui.overview.add_none'])}</p>
499
+ {:else}
500
+ <ul class="smrt-overview__choices">
501
+ {#each controller.addable as def (def.type)}
502
+ <li>
503
+ <Button type="button" variant="ghost" fullWidth onclick={() => chooseType(def.type)}>
504
+ <span class="smrt-overview__choice">
505
+ {#if def.icon}<ShellSectionIcon name={def.icon} {iconComponent} />{/if}
506
+ <span class="smrt-overview__choice-text">
507
+ <strong>{t(def.title)}</strong>
508
+ {#if def.description}<small>{t(def.description)}</small>{/if}
509
+ </span>
510
+ </span>
511
+ </Button>
512
+ </li>
513
+ {/each}
514
+ </ul>
515
+ {/if}
516
+ </Modal>
517
+
518
+ <Modal
519
+ bind:open={() => configure !== null, (v) => { if (!v) configure = null; }}
520
+ title={configureTitle}
521
+ size="md"
522
+ >
523
+ {#if configure && configureDef}
524
+ {#key `${configure.id ?? 'new'}:${configure.type}`}
525
+ <WidgetOptionsForm
526
+ definition={configureDef}
527
+ initial={configure.initial}
528
+ {models}
529
+ onsave={saveOptions}
530
+ oncancel={() => (configure = null)}
531
+ />
532
+ {/key}
533
+ {/if}
534
+ </Modal>
535
+ {/if}
536
+
537
+ <style>
538
+ .smrt-overview { container-type: inline-size; min-inline-size: 0; }
539
+ .smrt-overview__toolbar { display: flex; gap: var(--smrt-spacing-1); margin-block-end: var(--smrt-spacing-2); }
540
+ .smrt-overview__empty { margin: 0; color: var(--smrt-color-on-surface-variant); }
541
+ /* Gaps are tile padding, not grid gap, so a pointer never crosses a dead
542
+ zone between tiles while dragging. */
543
+ .smrt-overview__list { --_pad: var(--smrt-spacing-2); display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: calc(-1 * var(--_pad)); }
544
+ .smrt-overview__item { position: relative; grid-column: span var(--span, 1); min-inline-size: 0; padding: var(--_pad); }
545
+ .smrt-overview__item[data-dragging] { opacity: 0.5; }
546
+ .smrt-overview__item[data-drop]::before { content: ''; position: absolute; inset-block: var(--_pad); inline-size: 0.1875rem; border-radius: var(--smrt-radius-full, 9999px); background: var(--smrt-color-primary); }
547
+ .smrt-overview__item[data-drop='before']::before { inset-inline-start: 0; }
548
+ .smrt-overview__item[data-drop='after']::before { inset-inline-end: 0; }
549
+ .smrt-overview__tile { position: relative; block-size: 100%; box-sizing: border-box; padding: var(--smrt-spacing-3); border: 1px solid var(--smrt-color-outline-variant); border-radius: var(--smrt-radius-large, var(--smrt-radius-medium)); background: var(--smrt-color-surface); color: var(--smrt-color-on-surface); }
550
+ [data-editing] .smrt-overview__tile { border-style: dashed; }
551
+ .smrt-overview__widget { display: grid; gap: var(--smrt-spacing-2); align-content: start; min-inline-size: 0; }
552
+ .smrt-overview__title { margin: 0; color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-title-small-size, 0.875rem); font-weight: var(--smrt-typography-weight-medium, 500); }
553
+ .smrt-overview__state { margin: 0; color: var(--smrt-color-on-surface-variant); }
554
+ .smrt-overview__chrome { display: flex; align-items: center; gap: var(--smrt-spacing-1); margin-block-end: var(--smrt-spacing-2); }
555
+ .smrt-overview__chrome-title { flex: 1 1 auto; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-label-large-size, 0.875rem); }
556
+ .smrt-overview__handle { display: inline-grid; place-items: center; flex: 0 0 auto; inline-size: 2rem; block-size: 2rem; padding: 0; border: 0; border-radius: var(--smrt-radius-small); background: transparent; color: var(--smrt-color-on-surface-variant); cursor: grab; touch-action: none; }
557
+ .smrt-overview__handle:active { cursor: grabbing; }
558
+ .smrt-overview__handle:hover { background: var(--smrt-color-surface-container-high); }
559
+ .smrt-overview__handle:focus-visible { outline: 2px solid var(--smrt-color-primary); outline-offset: 2px; }
560
+ .smrt-overview__handle[aria-pressed='true'] { background: var(--smrt-color-primary-container); color: var(--smrt-color-on-primary-container); }
561
+ .smrt-overview__resize { position: absolute; inset-block-start: 50%; inset-inline-end: -0.4375rem; inline-size: 0.875rem; block-size: 2.75rem; transform: translateY(-50%); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-full, 9999px); background: var(--smrt-color-surface-container-high); cursor: ew-resize; touch-action: none; }
562
+ .smrt-overview__resize:hover { background: var(--smrt-color-primary-container); }
563
+ .smrt-overview__resize:focus-visible { outline: 2px solid var(--smrt-color-primary); outline-offset: 2px; }
564
+ .smrt-overview__choices { display: grid; gap: var(--smrt-spacing-1); margin: 0; padding: 0; list-style: none; }
565
+ .smrt-overview__choice { display: flex; align-items: center; gap: var(--smrt-spacing-3); text-align: start; }
566
+ .smrt-overview__choice-text { display: grid; }
567
+ .smrt-overview__choice-text small { color: var(--smrt-color-on-surface-variant); }
568
+ .smrt-overview__live { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
569
+
570
+ /* Responsive columns follow the grid's own width, not the viewport. */
571
+ @container (max-width: 40rem) {
572
+ .smrt-overview__list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
573
+ .smrt-overview__item[data-span='3'], .smrt-overview__item[data-span='4'] { grid-column: span 2; }
574
+ }
575
+ @container (max-width: 22rem) {
576
+ .smrt-overview__list { grid-template-columns: minmax(0, 1fr); }
577
+ .smrt-overview__list .smrt-overview__item[data-span] { grid-column: span 1; }
578
+ }
579
+ </style>
@@ -0,0 +1,5 @@
1
+ import type { OverviewGridProps } from './grid-types.js';
2
+ declare const OverviewGrid: import("svelte").Component<OverviewGridProps, {}, "">;
3
+ type OverviewGrid = ReturnType<typeof OverviewGrid>;
4
+ export default OverviewGrid;
5
+ //# sourceMappingURL=OverviewGrid.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OverviewGrid.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/overview/OverviewGrid.svelte.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AA0bzD,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}