@happyvertical/smrt-svelte 0.38.26 → 0.39.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 (48) hide show
  1. package/AGENTS.md +22 -0
  2. package/README.md +9 -7
  3. package/dist/components/forms/Form.svelte +112 -2
  4. package/dist/components/forms/Form.svelte.d.ts +12 -1
  5. package/dist/components/forms/Form.svelte.d.ts.map +1 -1
  6. package/dist/components/settings/SettingsCatalog.svelte +283 -0
  7. package/dist/components/settings/SettingsCatalog.svelte.d.ts +52 -0
  8. package/dist/components/settings/SettingsCatalog.svelte.d.ts.map +1 -0
  9. package/dist/components/settings/__tests__/SettingsCatalog.test.js +69 -0
  10. package/dist/components/settings/__tests__/catalog.test.js +46 -0
  11. package/dist/components/settings/catalog.d.ts +11 -0
  12. package/dist/components/settings/catalog.d.ts.map +1 -0
  13. package/dist/components/settings/catalog.js +40 -0
  14. package/dist/components/settings/index.d.ts +4 -0
  15. package/dist/components/settings/index.d.ts.map +1 -0
  16. package/dist/components/settings/index.js +2 -0
  17. package/dist/components/settings/types.d.ts +25 -0
  18. package/dist/components/settings/types.d.ts.map +1 -0
  19. package/dist/components/settings/types.js +1 -0
  20. package/dist/components/workspace/README.md +11 -1
  21. package/dist/components/workspace/__tests__/AdminShell.test.js +82 -0
  22. package/dist/components/workspace/__tests__/WorkspaceAccountMenu.test.js +73 -0
  23. package/dist/components/workspace/admin-shell/AdminShell.svelte +66 -15
  24. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +5 -2
  25. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -1
  26. package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte +188 -0
  27. package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte.d.ts +19 -0
  28. package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte.d.ts.map +1 -0
  29. package/dist/components/workspace/admin-shell/types.d.ts +7 -0
  30. package/dist/components/workspace/admin-shell/types.d.ts.map +1 -1
  31. package/dist/components/workspace/index.d.ts +2 -1
  32. package/dist/components/workspace/index.d.ts.map +1 -1
  33. package/dist/components/workspace/index.js +1 -0
  34. package/dist/hooks/useTheme.d.ts +1 -1
  35. package/dist/hooks/useTheme.d.ts.map +1 -1
  36. package/dist/hooks/useTheme.js +1 -1
  37. package/dist/i18n/strings.settings.d.ts +12 -0
  38. package/dist/i18n/strings.settings.d.ts.map +1 -0
  39. package/dist/i18n/strings.settings.js +12 -0
  40. package/dist/i18n/strings.workspace.d.ts +3 -0
  41. package/dist/i18n/strings.workspace.d.ts.map +1 -1
  42. package/dist/i18n/strings.workspace.js +3 -0
  43. package/dist/state/form-context.d.ts +19 -0
  44. package/dist/state/form-context.d.ts.map +1 -1
  45. package/dist/web/__tests__/update-available-harness.svelte +4 -1
  46. package/dist/web/__tests__/update-available-harness.svelte.d.ts.map +1 -1
  47. package/package.json +23 -16
  48. package/LICENSE +0 -7
package/AGENTS.md CHANGED
@@ -81,6 +81,7 @@ the top-of-stack, domain-aware pieces:
81
81
  | AI | `Provider`, `AILoadingOverlay`, `CapabilityGate`, `DownloadProgress`, `STTTest`, `VoiceInput` |
82
82
  | Forms (`/forms`) | `TextInput`, `Select`, `MoneyInput`, `DateTimeInput`, `Toggle`, `FileUpload`, `AddressInput`, + more (AI-wired inputs use the hooks/browser-ai here) |
83
83
  | Module | `ModulePanel` |
84
+ | Settings (`/settings`) | `SettingsCatalog`, `paginateSettingsCatalog` |
84
85
  | Workspace (`/workspace`) | `WorkspaceShell`, `NavTree`, `Breadcrumbs`, `ToolsDock`, `RoleShell` |
85
86
 
86
87
  ### Gap primitives & S10 consolidation (L3 #1422)
@@ -120,6 +121,7 @@ library. Which barrel for what:
120
121
  | `Container`, `Grid`, `Header`, `Footer`, `PageHeader`, `EmptyState` | `@happyvertical/smrt-svelte/layout` |
121
122
  | Chat message bubble, reaction picker, typing indicator | `@happyvertical/smrt-svelte/chat` |
122
123
  | Admin shell, nav tree, breadcrumbs, tools dock | `@happyvertical/smrt-svelte/workspace` |
124
+ | Server-paged settings search, selection, and list/detail layout | `@happyvertical/smrt-svelte/settings` |
123
125
 
124
126
  The package root re-exports `./ui`, `./forms`, etc., so `from
125
127
  '@happyvertical/smrt-svelte'` also works; prefer the specific subpath in domain
@@ -236,6 +238,26 @@ await expectNoA11yViolations(container); // axe; color-contrast off (jsdom has n
236
238
  - `@happyvertical/logger` (SDK) is a `dependency` — the console logger used for voice/AI error reporting in the form components. Consume it **only** through `src/internal/logger.ts`, never `createLogger()` at module scope: `createLogger()` reads `HAVE_LOGGER_LEVEL` from `process.env`, so a top-level call throws `ReferenceError: process is not defined` in the browser and kills client-side hydration under `vite dev` (prod builds tree-shake/define it away, so this only bites in dev). The `internal/logger` wrapper constructs the logger lazily and falls back to a bare `ConsoleLogger` when `process.env` is absent, keeping this browser-reachable module (imported by `Provider` + the form primitives) safe.
237
239
  - Peer (all optional): `svelte` >=5.18.2, plus the browser-AI engines (`@huggingface/transformers`, `@mlc-ai/web-llm`, `@remotion/whisper-web`, `@xenova/transformers`) and `chrono-node`.
238
240
 
241
+ ## Scalable settings catalog (`./settings`)
242
+
243
+ `SettingsCatalog` is the shared search/browse/select/edit shell for large
244
+ code-first or database-backed settings registries. Its interface accepts a
245
+ server-produced `SettingsCatalogPage`: callers keep transport, authorization,
246
+ and domain-specific editor forms, while the module owns compact result rows,
247
+ GET search, query-preserving selection links, bounded pagination, result counts,
248
+ empty states, and responsive list/detail layout.
249
+
250
+ `paginateSettingsCatalog()` is the optional server helper for definitions that
251
+ already live in memory (prompt and language registries). It searches before
252
+ paging, clamps pages, caps rendered slices at 100 rows, and selects only one
253
+ detail item. Database-backed callers should query their own page and construct
254
+ the same `SettingsCatalogPage` interface directly rather than loading every row.
255
+
256
+ The summary-row type and selected-detail type may differ: list pages can remain
257
+ cheap while only the selected definition is fully resolved. This is the
258
+ scalability contract; do not resolve every settings editor before passing data
259
+ to the catalog.
260
+
239
261
  ## AdminShell workspace surface
240
262
 
241
263
  The `./workspace` subpath (`src/components/workspace/`) is the canonical
package/README.md CHANGED
@@ -37,17 +37,17 @@ pnpm add @happyvertical/smrt-svelte
37
37
  <Toggle label="Active" bind:checked={active} />
38
38
  ```
39
39
 
40
- ### UI Components
40
+ ### UI Foundation
41
41
 
42
42
  ```svelte
43
43
  <script>
44
- import { Button, Card, Badge, Pagination } from '@happyvertical/smrt-svelte/ui';
45
- import { DataTable } from '@happyvertical/smrt-svelte';
44
+ import { Button, Card, Badge, Pagination } from '@happyvertical/smrt-ui/ui';
45
+ import { DataTable } from '@happyvertical/smrt-ui/data';
46
46
  </script>
47
47
 
48
48
  <Card>
49
- <DataTable columns={cols} rows={data} />
50
- <Pagination total={100} page={1} perPage={20} />
49
+ <DataTable columns={cols} data={rows} pageSize={20} />
50
+ <Pagination currentPage={1} totalPages={5} />
51
51
  </Card>
52
52
  ```
53
53
 
@@ -72,7 +72,7 @@ pnpm add @happyvertical/smrt-svelte
72
72
 
73
73
  ```svelte
74
74
  <script>
75
- import { ThemeProvider } from '@happyvertical/smrt-svelte/themes';
75
+ import { ThemeProvider } from '@happyvertical/smrt-ui/themes';
76
76
  </script>
77
77
 
78
78
  <ThemeProvider preset="glass" colorScheme="system">
@@ -151,7 +151,9 @@ scaffold adopts AdminShell as its default chrome exactly this way; copy its
151
151
  | `@happyvertical/smrt-svelte/forms` | Form inputs (TextInput, Select, MoneyInput, etc.) |
152
152
  | `@happyvertical/smrt-svelte/layout` | Layout (Container, Grid, Header, Footer, Masthead, etc.) |
153
153
  | `@happyvertical/smrt-svelte/ui` | UI primitives (Button, Card, Badge, Pagination) |
154
- | `@happyvertical/smrt-svelte/themes` | ThemeProvider, presets (material/glass/studio), CSS generation |
154
+ | `@happyvertical/smrt-ui/themes` | Canonical ThemeProvider, Material/Glass/Studio/SMRT presets, CSS generation |
155
+ | `@happyvertical/smrt-ui/forms` | Provider-free foundational form controls and the agent interaction registry |
156
+ | `@happyvertical/smrt-ui/data` | DataTable, CollectionList/ContentList, CollectionToolbar |
155
157
  | `@happyvertical/smrt-svelte/registry` | ModuleUIRegistry for agent admin panels |
156
158
  | `@happyvertical/smrt-svelte/workspace` | AdminShell, ShellState, tenant nav, focus tools, settings, activities, and system/app panels |
157
159
  | `@happyvertical/smrt-svelte/browser-ai` | Browser AI client (STT/TTS/LLM adapters, capability detection) |
@@ -1,4 +1,11 @@
1
1
  <script lang="ts">
2
+ import {
3
+ type ControlInteractionEvent,
4
+ type ControlInteractionRegistry,
5
+ type ControlKind,
6
+ createControlInteractionRegistry,
7
+ setControlInteractionContext,
8
+ } from '@happyvertical/smrt-ui/forms';
2
9
  import { useI18n } from '@happyvertical/smrt-ui/i18n';
3
10
  import type { Snippet } from 'svelte';
4
11
  import { onDestroy } from 'svelte';
@@ -34,6 +41,13 @@ export interface Props {
34
41
  method?: 'GET' | 'POST';
35
42
  /** Form action URL for native form submission */
36
43
  action?: string;
44
+ /** Stable identity used by control/agent interaction adapters. */
45
+ formId?: string;
46
+ interactionRegistry?: ControlInteractionRegistry;
47
+ oninteraction?: (event: ControlInteractionEvent) => void;
48
+ id?: string;
49
+ name?: string;
50
+ class?: string;
37
51
  }
38
52
 
39
53
  const {
@@ -46,10 +60,38 @@ const {
46
60
  onsubmit,
47
61
  method,
48
62
  action,
63
+ formId,
64
+ interactionRegistry,
65
+ oninteraction,
66
+ id,
67
+ name,
68
+ class: className = '',
49
69
  }: Props = $props();
50
70
 
51
71
  const app = useAppState();
52
72
  const stt = useSTT();
73
+ const instanceId = $props.id();
74
+ const generatedFormId = `smrt-form-${instanceId}`;
75
+ const localInteractionRegistry = createControlInteractionRegistry();
76
+ const resolvedFormId = $derived(formId ?? id ?? name ?? generatedFormId);
77
+ const resolvedInteractionRegistry = $derived(
78
+ interactionRegistry ?? localInteractionRegistry,
79
+ );
80
+ const interactionDisposers = new Map<string, () => void>();
81
+
82
+ setControlInteractionContext({
83
+ get formId() {
84
+ return resolvedFormId;
85
+ },
86
+ get registry() {
87
+ return resolvedInteractionRegistry;
88
+ },
89
+ });
90
+
91
+ $effect(() => {
92
+ if (!oninteraction) return;
93
+ return resolvedInteractionRegistry.subscribe(oninteraction);
94
+ });
53
95
 
54
96
  // Internal state
55
97
  let fields = $state<Map<string, FieldDefinition>>(new Map());
@@ -66,16 +108,64 @@ let levelMonitorStream: MediaStream | null = null;
66
108
 
67
109
  const isSmrt = $derived(app.state.mode === 'smrt');
68
110
 
111
+ function interactionKind(field: FieldDefinition): ControlKind {
112
+ if (field.interactionKind) return field.interactionKind;
113
+ switch (field.type) {
114
+ case 'text':
115
+ case 'email':
116
+ case 'number':
117
+ case 'checkbox':
118
+ case 'select':
119
+ case 'textarea':
120
+ return field.type;
121
+ case 'datetime':
122
+ return 'datetime';
123
+ default:
124
+ return 'custom';
125
+ }
126
+ }
127
+
69
128
  // Create form context
70
129
  const formContext: SMRTFormContext = {
71
130
  get mode() {
72
131
  return app.state.mode === 'smrt' ? 'smrt' : 'default';
73
132
  },
74
133
  registerField(field: FieldDefinition) {
134
+ interactionDisposers.get(field.name)?.();
75
135
  fields.set(field.name, field);
76
136
  fields = new Map(fields); // Trigger reactivity
137
+ interactionDisposers.set(
138
+ field.name,
139
+ resolvedInteractionRegistry.register({
140
+ identity: {
141
+ formId: resolvedFormId,
142
+ controlId: field.controlId ?? field.name,
143
+ },
144
+ metadata: {
145
+ kind: interactionKind(field),
146
+ label: field.label,
147
+ description: field.description,
148
+ sensitivity: field.sensitivity ?? 'public',
149
+ readable: field.readable,
150
+ writable: field.writable,
151
+ constraints: field.constraints,
152
+ options: field.options,
153
+ unit: field.unit,
154
+ },
155
+ getValue: field.getValue,
156
+ setValue: field.setValue,
157
+ clear: field.clear ?? (() => field.setValue('')),
158
+ focus: field.focus,
159
+ reveal: field.reveal,
160
+ highlight: field.highlight,
161
+ validate: field.validate,
162
+ getState: field.getState,
163
+ }),
164
+ );
77
165
  },
78
166
  unregisterField(name: string) {
167
+ interactionDisposers.get(name)?.();
168
+ interactionDisposers.delete(name);
79
169
  fields.delete(name);
80
170
  fields = new Map(fields);
81
171
  },
@@ -89,6 +179,12 @@ const formContext: SMRTFormContext = {
89
179
  return isExtracting;
90
180
  },
91
181
  toggleListening: () => toggleFormListening(),
182
+ get interactionRegistry() {
183
+ return resolvedInteractionRegistry;
184
+ },
185
+ get formId() {
186
+ return resolvedFormId;
187
+ },
92
188
  };
93
189
 
94
190
  // Provide context to children
@@ -498,6 +594,8 @@ onDestroy(() => {
498
594
  clearTimeout(silenceTimer);
499
595
  }
500
596
  stopAudioLevelMonitoring();
597
+ for (const dispose of interactionDisposers.values()) dispose();
598
+ interactionDisposers.clear();
501
599
  });
502
600
 
503
601
  function handleModeToggle() {
@@ -519,16 +617,28 @@ function handleSubmit(e: Event) {
519
617
  // Otherwise, let native form submission happen (e.g., for SvelteKit use:enhance)
520
618
  }
521
619
 
522
- function getFormData(): Record<string, unknown> {
620
+ export function getFormData(): Record<string, unknown> {
523
621
  const data: Record<string, unknown> = {};
524
622
  for (const [name, field] of fields) {
525
623
  data[name] = field.getValue();
526
624
  }
527
625
  return data;
528
626
  }
627
+
628
+ export function getInteractionRegistry(): ControlInteractionRegistry {
629
+ return resolvedInteractionRegistry;
630
+ }
529
631
  </script>
530
632
 
531
- <form class="smrt-form" onsubmit={handleSubmit} {method} {action}>
633
+ <form
634
+ {id}
635
+ {name}
636
+ class="smrt-form {className}"
637
+ data-smrt-form={resolvedFormId}
638
+ onsubmit={handleSubmit}
639
+ {method}
640
+ {action}
641
+ >
532
642
  <!--
533
643
  Screen-reader status region (L1 #1420): announces async STT / field-
534
644
  extraction state politely, since the visible cues live on a button whose
@@ -1,3 +1,4 @@
1
+ import { type ControlInteractionEvent, type ControlInteractionRegistry } from '@happyvertical/smrt-ui/forms';
1
2
  import type { Snippet } from 'svelte';
2
3
  import type { LLMModelId, STTAdapterType } from './types.js';
3
4
  export interface Props {
@@ -19,8 +20,18 @@ export interface Props {
19
20
  method?: 'GET' | 'POST';
20
21
  /** Form action URL for native form submission */
21
22
  action?: string;
23
+ /** Stable identity used by control/agent interaction adapters. */
24
+ formId?: string;
25
+ interactionRegistry?: ControlInteractionRegistry;
26
+ oninteraction?: (event: ControlInteractionEvent) => void;
27
+ id?: string;
28
+ name?: string;
29
+ class?: string;
22
30
  }
23
- declare const Form: import("svelte").Component<Props, {}, "">;
31
+ declare const Form: import("svelte").Component<Props, {
32
+ getFormData: () => Record<string, unknown>;
33
+ getInteractionRegistry: () => ControlInteractionRegistry;
34
+ }, "">;
24
35
  type Form = ReturnType<typeof Form>;
25
36
  export default Form;
26
37
  //# sourceMappingURL=Form.svelte.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Form.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Form.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAWtC,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG7D,MAAM,WAAW,KAAK;IACpB,oBAAoB;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,8BAA8B;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oCAAoC;IACpC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iDAAiD;IACjD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB;IACvB,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACnD,4DAA4D;IAC5D,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACxB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA8jBD,QAAA,MAAM,IAAI,2CAAwC,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
1
+ {"version":3,"file":"Form.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Form.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAIhC,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAWtC,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG7D,MAAM,WAAW,KAAK;IACpB,oBAAoB;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,8BAA8B;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oCAAoC;IACpC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iDAAiD;IACjD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB;IACvB,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACnD,4DAA4D;IAC5D,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACxB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mBAAmB,CAAC,EAAE,0BAA0B,CAAC;IACjD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAC;IACzD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAupBD,QAAA,MAAM,IAAI;uBAhFe,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;kCAQZ,0BAA0B;MAwEZ,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
@@ -0,0 +1,283 @@
1
+ <script lang="ts" generics="T extends SettingsCatalogItem, D extends { id: string } = T">
2
+ import { Form, Input } from '@happyvertical/smrt-ui/forms';
3
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
4
+ import { Button } from '@happyvertical/smrt-ui/ui';
5
+ import type { Snippet } from 'svelte';
6
+ import { M } from '../../i18n/strings.settings.js';
7
+ import type { SettingsCatalogItem, SettingsCatalogPage } from './types.js';
8
+
9
+ interface Props<T extends SettingsCatalogItem, D extends { id: string }> {
10
+ page: SettingsCatalogPage<T, D>;
11
+ baseUrl: string;
12
+ detail: Snippet<[{ item: D }]>;
13
+ searchPlaceholder?: string;
14
+ searchLabel?: string;
15
+ resultsLabel?: string;
16
+ emptyLabel?: string;
17
+ selectionLabel?: string;
18
+ preservedParams?: Record<string, string>;
19
+ }
20
+
21
+ let {
22
+ page,
23
+ baseUrl,
24
+ detail,
25
+ searchPlaceholder,
26
+ searchLabel,
27
+ resultsLabel,
28
+ emptyLabel,
29
+ selectionLabel,
30
+ preservedParams = {},
31
+ }: Props<T, D> = $props();
32
+
33
+ const { t } = useI18n();
34
+ const totalPages = $derived(Math.max(1, Math.ceil(page.total / page.pageSize)));
35
+ const rangeStart = $derived(
36
+ page.total === 0 ? 0 : (page.page - 1) * page.pageSize + 1,
37
+ );
38
+ const rangeEnd = $derived(Math.min(page.total, page.page * page.pageSize));
39
+
40
+ function catalogHref(targetPage: number, selectedId?: string): string {
41
+ const params = new URLSearchParams(preservedParams);
42
+ if (page.query) params.set('q', page.query);
43
+ if (targetPage > 1) params.set('page', String(targetPage));
44
+ if (selectedId) params.set('selected', selectedId);
45
+ const query = params.toString();
46
+ return query ? `${baseUrl}?${query}` : baseUrl;
47
+ }
48
+ </script>
49
+
50
+ <section class="smrt-settings-catalog">
51
+ <aside class="smrt-settings-catalog__browser">
52
+ <Form method="GET" action={baseUrl} preventDefault={false} class="smrt-settings-catalog__search" role="search">
53
+ {#each Object.entries(preservedParams) as [name, value] (name)}
54
+ <Input type="hidden" {name} {value} />
55
+ {/each}
56
+ <label for="smrt-settings-catalog-query" class="smrt-settings-catalog__sr-only">
57
+ {searchLabel ?? t(M['ui.settings_catalog.search'])}
58
+ </label>
59
+ <Input
60
+ id="smrt-settings-catalog-query"
61
+ type="search"
62
+ name="q"
63
+ value={page.query}
64
+ placeholder={searchPlaceholder ?? t(M['ui.settings_catalog.search'])}
65
+ />
66
+ <Button type="submit" size="sm">
67
+ {t(M['ui.settings_catalog.search_action'])}
68
+ </Button>
69
+ </Form>
70
+
71
+ <div class="smrt-settings-catalog__summary">
72
+ <span>
73
+ {t(M['ui.settings_catalog.result_range'], {
74
+ start: rangeStart,
75
+ end: rangeEnd,
76
+ total: page.total,
77
+ })}
78
+ </span>
79
+ </div>
80
+
81
+ <nav
82
+ class="smrt-settings-catalog__results"
83
+ aria-label={resultsLabel ?? t(M['ui.settings_catalog.results'])}
84
+ >
85
+ {#each page.items as item (item.id)}
86
+ <a
87
+ href={catalogHref(page.page, item.id)}
88
+ class:smrt-settings-catalog__result--active={page.selected?.id === item.id}
89
+ aria-current={page.selected?.id === item.id ? 'true' : undefined}
90
+ >
91
+ {#if item.eyebrow}
92
+ <small>{item.eyebrow}</small>
93
+ {/if}
94
+ <strong>{item.label}</strong>
95
+ {#if item.description}
96
+ <span>{item.description}</span>
97
+ {/if}
98
+ {#if item.status}
99
+ <span class="smrt-settings-catalog__status">{item.status}</span>
100
+ {/if}
101
+ </a>
102
+ {:else}
103
+ <p class="smrt-settings-catalog__empty">
104
+ {emptyLabel ?? t(M['ui.settings_catalog.no_results'])}
105
+ </p>
106
+ {/each}
107
+ </nav>
108
+
109
+ {#if totalPages > 1}
110
+ <nav
111
+ class="smrt-settings-catalog__pager"
112
+ aria-label={t(M['ui.settings_catalog.pagination'])}
113
+ >
114
+ {#if page.page > 1}
115
+ <Button href={catalogHref(page.page - 1)} variant="ghost" size="sm">
116
+ {t(M['ui.settings_catalog.previous'])}
117
+ </Button>
118
+ {:else}
119
+ <span></span>
120
+ {/if}
121
+ <span>{page.page} / {totalPages}</span>
122
+ {#if page.page < totalPages}
123
+ <Button href={catalogHref(page.page + 1)} variant="ghost" size="sm">
124
+ {t(M['ui.settings_catalog.next'])}
125
+ </Button>
126
+ {:else}
127
+ <span></span>
128
+ {/if}
129
+ </nav>
130
+ {/if}
131
+ </aside>
132
+
133
+ <div class="smrt-settings-catalog__detail">
134
+ {#if page.selected}
135
+ {@render detail({ item: page.selected })}
136
+ {:else}
137
+ <p>{selectionLabel ?? t(M['ui.settings_catalog.select_item'])}</p>
138
+ {/if}
139
+ </div>
140
+ </section>
141
+
142
+ <style>
143
+ .smrt-settings-catalog {
144
+ display: grid;
145
+ grid-template-columns: minmax(17rem, 22rem) minmax(0, 1fr);
146
+ gap: var(--smrt-spacing-5);
147
+ align-items: start;
148
+ min-width: 0;
149
+ }
150
+
151
+ :global(.smrt-settings-catalog__search) {
152
+ display: grid;
153
+ grid-template-columns: minmax(0, 1fr) auto;
154
+ gap: var(--smrt-spacing-2);
155
+ align-items: center;
156
+ }
157
+
158
+ .smrt-settings-catalog__browser,
159
+ .smrt-settings-catalog__detail {
160
+ min-width: 0;
161
+ }
162
+
163
+ .smrt-settings-catalog__browser {
164
+ position: sticky;
165
+ top: var(--smrt-spacing-4);
166
+ display: grid;
167
+ gap: var(--smrt-spacing-3);
168
+ }
169
+
170
+ .smrt-settings-catalog__summary {
171
+ color: var(--smrt-color-on-surface-variant);
172
+ font-size: var(--smrt-typography-label-medium-size);
173
+ }
174
+
175
+ .smrt-settings-catalog__results {
176
+ display: grid;
177
+ max-block-size: min(42rem, calc(100vh - 19rem));
178
+ overflow-y: auto;
179
+ border-block: 1px solid var(--smrt-color-outline-variant);
180
+ }
181
+
182
+ .smrt-settings-catalog__results a {
183
+ position: relative;
184
+ display: grid;
185
+ gap: var(--smrt-spacing-1);
186
+ min-width: 0;
187
+ padding: var(--smrt-spacing-3);
188
+ border-bottom: 1px solid var(--smrt-color-outline-variant);
189
+ color: inherit;
190
+ text-decoration: none;
191
+ }
192
+
193
+ .smrt-settings-catalog__results a:hover,
194
+ .smrt-settings-catalog__results a.smrt-settings-catalog__result--active {
195
+ background: var(--smrt-color-surface-container-high);
196
+ }
197
+
198
+ .smrt-settings-catalog__results a.smrt-settings-catalog__result--active::before {
199
+ position: absolute;
200
+ inset-block: var(--smrt-spacing-2);
201
+ inset-inline-start: 0;
202
+ inline-size: 3px;
203
+ border-radius: var(--smrt-radius-full);
204
+ background: var(--smrt-color-primary);
205
+ content: '';
206
+ }
207
+
208
+ .smrt-settings-catalog__results small,
209
+ .smrt-settings-catalog__results span,
210
+ .smrt-settings-catalog__results .smrt-settings-catalog__status {
211
+ overflow: hidden;
212
+ color: var(--smrt-color-on-surface-variant);
213
+ font-size: var(--smrt-typography-label-medium-size);
214
+ text-overflow: ellipsis;
215
+ white-space: nowrap;
216
+ }
217
+
218
+ .smrt-settings-catalog__results strong {
219
+ overflow: hidden;
220
+ text-overflow: ellipsis;
221
+ white-space: nowrap;
222
+ }
223
+
224
+ .smrt-settings-catalog__results .smrt-settings-catalog__status {
225
+ color: var(--smrt-color-primary);
226
+ }
227
+
228
+ .smrt-settings-catalog__empty,
229
+ .smrt-settings-catalog__detail > p {
230
+ margin: 0;
231
+ color: var(--smrt-color-on-surface-variant);
232
+ }
233
+
234
+ .smrt-settings-catalog__empty {
235
+ padding: var(--smrt-spacing-4) var(--smrt-spacing-3);
236
+ }
237
+
238
+ .smrt-settings-catalog__pager {
239
+ display: grid;
240
+ grid-template-columns: 1fr auto 1fr;
241
+ align-items: center;
242
+ gap: var(--smrt-spacing-2);
243
+ color: var(--smrt-color-on-surface-variant);
244
+ font-size: var(--smrt-typography-label-medium-size);
245
+ }
246
+
247
+ .smrt-settings-catalog__pager :global(a:last-child) {
248
+ justify-self: end;
249
+ }
250
+
251
+ .smrt-settings-catalog__detail {
252
+ border: 1px solid var(--smrt-color-outline);
253
+ border-radius: var(--smrt-radius-medium);
254
+ background: var(--smrt-color-surface);
255
+ padding: var(--smrt-spacing-4);
256
+ }
257
+
258
+ .smrt-settings-catalog__sr-only {
259
+ position: absolute;
260
+ inline-size: 1px;
261
+ block-size: 1px;
262
+ padding: 0;
263
+ margin: -1px;
264
+ overflow: hidden;
265
+ clip: rect(0, 0, 0, 0);
266
+ white-space: nowrap;
267
+ border: 0;
268
+ }
269
+
270
+ @media (max-width: 760px) {
271
+ .smrt-settings-catalog {
272
+ grid-template-columns: minmax(0, 1fr);
273
+ }
274
+
275
+ .smrt-settings-catalog__browser {
276
+ position: static;
277
+ }
278
+
279
+ .smrt-settings-catalog__results {
280
+ max-block-size: 18rem;
281
+ }
282
+ }
283
+ </style>
@@ -0,0 +1,52 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { SettingsCatalogItem, SettingsCatalogPage } from './types.js';
3
+ interface Props<T extends SettingsCatalogItem, D extends {
4
+ id: string;
5
+ }> {
6
+ page: SettingsCatalogPage<T, D>;
7
+ baseUrl: string;
8
+ detail: Snippet<[{
9
+ item: D;
10
+ }]>;
11
+ searchPlaceholder?: string;
12
+ searchLabel?: string;
13
+ resultsLabel?: string;
14
+ emptyLabel?: string;
15
+ selectionLabel?: string;
16
+ preservedParams?: Record<string, string>;
17
+ }
18
+ declare function $$render<T extends SettingsCatalogItem, D extends {
19
+ id: string;
20
+ } = T>(): {
21
+ props: Props<T, D>;
22
+ exports: {};
23
+ bindings: "";
24
+ slots: {};
25
+ events: {};
26
+ };
27
+ declare class __sveltets_Render<T extends SettingsCatalogItem, D extends {
28
+ id: string;
29
+ } = T> {
30
+ props(): ReturnType<typeof $$render<T, D>>['props'];
31
+ events(): ReturnType<typeof $$render<T, D>>['events'];
32
+ slots(): ReturnType<typeof $$render<T, D>>['slots'];
33
+ bindings(): "";
34
+ exports(): {};
35
+ }
36
+ interface $$IsomorphicComponent {
37
+ new <T extends SettingsCatalogItem, D extends {
38
+ id: string;
39
+ } = T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T, D>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T, D>['props']>, ReturnType<__sveltets_Render<T, D>['events']>, ReturnType<__sveltets_Render<T, D>['slots']>> & {
40
+ $$bindings?: ReturnType<__sveltets_Render<T, D>['bindings']>;
41
+ } & ReturnType<__sveltets_Render<T, D>['exports']>;
42
+ <T extends SettingsCatalogItem, D extends {
43
+ id: string;
44
+ } = T>(internal: unknown, props: ReturnType<__sveltets_Render<T, D>['props']> & {}): ReturnType<__sveltets_Render<T, D>['exports']>;
45
+ z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
46
+ }
47
+ declare const SettingsCatalog: $$IsomorphicComponent;
48
+ type SettingsCatalog<T extends SettingsCatalogItem, D extends {
49
+ id: string;
50
+ } = T> = InstanceType<typeof SettingsCatalog<T, D>>;
51
+ export default SettingsCatalog;
52
+ //# sourceMappingURL=SettingsCatalog.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SettingsCatalog.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/settings/SettingsCatalog.svelte.ts"],"names":[],"mappings":"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"}