@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.
- package/AGENTS.md +22 -0
- package/README.md +9 -7
- package/dist/components/forms/Form.svelte +112 -2
- package/dist/components/forms/Form.svelte.d.ts +12 -1
- package/dist/components/forms/Form.svelte.d.ts.map +1 -1
- package/dist/components/settings/SettingsCatalog.svelte +283 -0
- package/dist/components/settings/SettingsCatalog.svelte.d.ts +52 -0
- package/dist/components/settings/SettingsCatalog.svelte.d.ts.map +1 -0
- package/dist/components/settings/__tests__/SettingsCatalog.test.js +69 -0
- package/dist/components/settings/__tests__/catalog.test.js +46 -0
- package/dist/components/settings/catalog.d.ts +11 -0
- package/dist/components/settings/catalog.d.ts.map +1 -0
- package/dist/components/settings/catalog.js +40 -0
- package/dist/components/settings/index.d.ts +4 -0
- package/dist/components/settings/index.d.ts.map +1 -0
- package/dist/components/settings/index.js +2 -0
- package/dist/components/settings/types.d.ts +25 -0
- package/dist/components/settings/types.d.ts.map +1 -0
- package/dist/components/settings/types.js +1 -0
- package/dist/components/workspace/README.md +11 -1
- package/dist/components/workspace/__tests__/AdminShell.test.js +82 -0
- package/dist/components/workspace/__tests__/WorkspaceAccountMenu.test.js +73 -0
- package/dist/components/workspace/admin-shell/AdminShell.svelte +66 -15
- package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +5 -2
- package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte +188 -0
- package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte.d.ts +19 -0
- package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/types.d.ts +7 -0
- package/dist/components/workspace/admin-shell/types.d.ts.map +1 -1
- package/dist/components/workspace/index.d.ts +2 -1
- package/dist/components/workspace/index.d.ts.map +1 -1
- package/dist/components/workspace/index.js +1 -0
- package/dist/hooks/useTheme.d.ts +1 -1
- package/dist/hooks/useTheme.d.ts.map +1 -1
- package/dist/hooks/useTheme.js +1 -1
- package/dist/i18n/strings.settings.d.ts +12 -0
- package/dist/i18n/strings.settings.d.ts.map +1 -0
- package/dist/i18n/strings.settings.js +12 -0
- package/dist/i18n/strings.workspace.d.ts +3 -0
- package/dist/i18n/strings.workspace.d.ts.map +1 -1
- package/dist/i18n/strings.workspace.js +3 -0
- package/dist/state/form-context.d.ts +19 -0
- package/dist/state/form-context.d.ts.map +1 -1
- package/dist/web/__tests__/update-available-harness.svelte +4 -1
- package/dist/web/__tests__/update-available-harness.svelte.d.ts.map +1 -1
- package/package.json +23 -16
- 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
|
|
40
|
+
### UI Foundation
|
|
41
41
|
|
|
42
42
|
```svelte
|
|
43
43
|
<script>
|
|
44
|
-
import { Button, Card, Badge, Pagination } from '@happyvertical/smrt-
|
|
45
|
-
import { DataTable } from '@happyvertical/smrt-
|
|
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={
|
|
50
|
-
<Pagination
|
|
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-
|
|
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-
|
|
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
|
|
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":"
|
|
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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"}
|