@happyvertical/smrt-svelte 0.51.4 → 0.51.5
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 +1 -0
- package/README.md +39 -1
- package/dist/web/__tests__/list-data-surface.fixture.svelte +59 -0
- package/dist/web/__tests__/list-data-surface.fixture.svelte.d.ts +14 -0
- package/dist/web/__tests__/list-data-surface.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/list-data-surface.svelte.test.js +106 -0
- package/dist/web/__tests__/list-data-surface.test.js +651 -0
- package/dist/web/index.d.ts +1 -0
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +1 -0
- package/dist/web/list-data-surface.svelte.d.ts +133 -0
- package/dist/web/list-data-surface.svelte.d.ts.map +1 -0
- package/dist/web/list-data-surface.svelte.js +308 -0
- package/package.json +13 -13
package/AGENTS.md
CHANGED
|
@@ -16,6 +16,7 @@ subpath you are editing. This file keeps what holds in every module.
|
|
|
16
16
|
| `src/components/settings/` (`./settings`) | `SettingsCatalog`, `paginateSettingsCatalog`, and the summary-vs-detail scalability contract | [agents/settings.md](agents/settings.md) |
|
|
17
17
|
| `src/components/workspace/` (`./workspace` + `./web`) | the AdminShell family and its principles, the legacy ToolsDock surface, the `./web` activity-feed and `updateAvailable` adapters, and server-side dock gates | [agents/workspace.md](agents/workspace.md) |
|
|
18
18
|
| `src/web/remote-query.svelte.ts` | Svelte 5 binding for query-shaped remote pages: rows, page, totals, loading/refreshing/stale/error, retry, last-updated, and query-scoped live subscriptions (#2445) | — |
|
|
19
|
+
| `src/web/list-data-surface.svelte.ts` (`mountListDataSurface`) | One-call registration of an existing, non-`DataTable` list (already mirroring a headless `DataTableController`) as a mounted `DataSurfaceDescriptor` — a same-behavior port of `registerContentListDataSurface`'s registration/translation logic (`@happyvertical/smrt-content/svelte`), generalized off ContentList's view-mode concept, so any custom list markup can adopt it directly instead of hand-mirroring the registry contract. Not yet unified behind a shared `@happyvertical/smrt-ui/data` implementation (#2917) — this copy is currently ahead; see its own module doc comment for the current, authoritative list of divergences #2917 must adopt rather than assume parity (#2906) | — |
|
|
19
20
|
| `src/web/webmcp-provider.ts` | Provider config for generated data/model WebMCP tools: definitions, effect policy, namespace, budget, legacy/canonical filters, and fetcher seams (#2520) | — |
|
|
20
21
|
| `src/web/webmcp-ui.ts` | Fixed, low-cardinality WebMCP adapter over the Provider's mounted form-control and data-surface registries (#2521) | — |
|
|
21
22
|
| `src/web/webmcp.svelte.ts` (`useWebMcpTool`) | Component-owned bespoke WebMCP tool. Routes through `@happyvertical/smrt-web`'s `registerWebMcpBespokeTool`, so it shares the fail-closed effect classification and `effects` exposure policy of generated tools — undeclared `annotations` classify destructive and are excluded by default. `namespace` and `maxTools` never apply to a bespoke tool (#2586) | — |
|
package/README.md
CHANGED
|
@@ -144,6 +144,44 @@ should be discoverable:
|
|
|
144
144
|
</Provider>
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
A page with its own hand-rolled list markup — not `DataTable` — that already
|
|
148
|
+
mirrors a headless `DataTableController`'s search/filters/sort/page/selection
|
|
149
|
+
can register that same registry in one call with `mountListDataSurface`
|
|
150
|
+
(`@happyvertical/smrt-svelte/web`) instead of hand-writing the registration:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { createDataTableController } from '@happyvertical/smrt-ui/data';
|
|
154
|
+
import { mountListDataSurface } from '@happyvertical/smrt-svelte/web';
|
|
155
|
+
|
|
156
|
+
const controller = createDataTableController();
|
|
157
|
+
const handle = mountListDataSurface({
|
|
158
|
+
registry: surfaces,
|
|
159
|
+
descriptor: myListDescriptor,
|
|
160
|
+
controller,
|
|
161
|
+
context: { totalRows, queryFingerprint },
|
|
162
|
+
refresh: () => reload(),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Only `controller` is observed automatically. App-owned `context` is a
|
|
166
|
+
// one-time snapshot at mount — publish a fresh one whenever totalRows,
|
|
167
|
+
// queryFingerprint, or freshness changes (an $effect keyed on those values
|
|
168
|
+
// is the usual place):
|
|
169
|
+
handle.update({ totalRows, queryFingerprint });
|
|
170
|
+
|
|
171
|
+
// on unmount:
|
|
172
|
+
handle.destroy();
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
It mirrors `controller` into the registry, translates visible table commands
|
|
176
|
+
back into `controller.dispatch()` calls, and routes the fixed
|
|
177
|
+
`refresh`/`retry`/`focus`/`reveal`/`highlight` controls to callbacks; any
|
|
178
|
+
other `controlId` goes through an `onControl` escape hatch (denied by
|
|
179
|
+
default) — except a canonical table-control id (`set-filters`, `reset`,
|
|
180
|
+
`set-page`, … the full `DATA_TABLE_SURFACE_CONTROL_IDS` list from
|
|
181
|
+
`@happyvertical/smrt-ui/data`), which is always intercepted first and never
|
|
182
|
+
reaches `onControl`, even under a custom label. See
|
|
183
|
+
`docs/data-surface-conformance.md` for the full contract.
|
|
184
|
+
|
|
147
185
|
The default prefix is `smrt_ui_`. Configure `ui.prefix` when multiple Providers
|
|
148
186
|
must coexist in one document; the same prefix cannot be registered twice. The
|
|
149
187
|
six derived names are reserved through the document-global tool-name lock, so
|
|
@@ -306,7 +344,7 @@ importable, even if it appears in `dist/`.
|
|
|
306
344
|
| `@happyvertical/smrt-svelte/workspace/live` | `systemFeed` — the AdminShell system scope (jobs/schedules/dispatch) polled from an app status endpoint; deliberately carries no `smrt-web` dependency |
|
|
307
345
|
| `@happyvertical/smrt-svelte/browser-ai` | Browser AI client (STT/TTS/LLM adapters, capability detection) |
|
|
308
346
|
| `@happyvertical/smrt-svelte/browser-ai/svelte` | Svelte AI components (VoiceInput, CapabilityGate, etc.) |
|
|
309
|
-
| `@happyvertical/smrt-svelte/web` | `smrt-web` live-query bindings (`liveCollection`, `activityFeed`, `useUpdateAvailable`) |
|
|
347
|
+
| `@happyvertical/smrt-svelte/web` | `smrt-web` live-query bindings (`liveCollection`, `activityFeed`, `useUpdateAvailable`) plus `mountListDataSurface` (custom-list data-surface registration) |
|
|
310
348
|
| `@happyvertical/smrt-svelte/i18n/server` | Server-side i18n resolver (Node only) |
|
|
311
349
|
|
|
312
350
|
Domain-agnostic UI lives in `@happyvertical/smrt-ui`. There is no `ui`,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
DataSurfaceDescriptor,
|
|
4
|
+
DataSurfaceRegistry,
|
|
5
|
+
DataTableController,
|
|
6
|
+
} from '@happyvertical/smrt-ui/data';
|
|
7
|
+
import {
|
|
8
|
+
type ListDataSurfaceContext,
|
|
9
|
+
mountListDataSurface,
|
|
10
|
+
} from '../list-data-surface.svelte.js';
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
registry,
|
|
14
|
+
descriptor,
|
|
15
|
+
controller,
|
|
16
|
+
context,
|
|
17
|
+
onRefresh,
|
|
18
|
+
onStar,
|
|
19
|
+
}: {
|
|
20
|
+
registry: DataSurfaceRegistry;
|
|
21
|
+
descriptor: DataSurfaceDescriptor;
|
|
22
|
+
controller: DataTableController;
|
|
23
|
+
context: ListDataSurfaceContext;
|
|
24
|
+
onRefresh?: () => boolean | Promise<boolean>;
|
|
25
|
+
onStar?: (payload: unknown) => boolean;
|
|
26
|
+
} = $props();
|
|
27
|
+
|
|
28
|
+
let starredCount = $state(0);
|
|
29
|
+
let filterText = $state('');
|
|
30
|
+
|
|
31
|
+
$effect(() => {
|
|
32
|
+
const handle = mountListDataSurface({
|
|
33
|
+
registry,
|
|
34
|
+
descriptor,
|
|
35
|
+
controller,
|
|
36
|
+
context,
|
|
37
|
+
refresh: onRefresh,
|
|
38
|
+
onControl: (controlId, payload) => {
|
|
39
|
+
if (controlId === 'star') {
|
|
40
|
+
starredCount += 1;
|
|
41
|
+
return onStar ? onStar(payload) : true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
const unsubscribe = controller.subscribe(() => {
|
|
47
|
+
filterText = String(controller.getState().filters[0]?.value ?? '');
|
|
48
|
+
});
|
|
49
|
+
return () => {
|
|
50
|
+
unsubscribe();
|
|
51
|
+
handle.destroy();
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<ul data-testid="custom-list">
|
|
57
|
+
<li data-testid="filter-text">{filterText}</li>
|
|
58
|
+
</ul>
|
|
59
|
+
<p data-testid="starred-count">{starredCount}</p>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DataSurfaceDescriptor, DataSurfaceRegistry, DataTableController } from '@happyvertical/smrt-ui/data';
|
|
2
|
+
import { type ListDataSurfaceContext } from '../list-data-surface.svelte.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
registry: DataSurfaceRegistry;
|
|
5
|
+
descriptor: DataSurfaceDescriptor;
|
|
6
|
+
controller: DataTableController;
|
|
7
|
+
context: ListDataSurfaceContext;
|
|
8
|
+
onRefresh?: () => boolean | Promise<boolean>;
|
|
9
|
+
onStar?: (payload: unknown) => boolean;
|
|
10
|
+
};
|
|
11
|
+
declare const ListDataSurface: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type ListDataSurface = ReturnType<typeof ListDataSurface>;
|
|
13
|
+
export default ListDataSurface;
|
|
14
|
+
//# sourceMappingURL=list-data-surface.fixture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-data-surface.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../src/web/__tests__/list-data-surface.fixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,KAAK,sBAAsB,EAE5B,MAAM,gCAAgC,CAAC;AAEvC,KAAK,gBAAgB,GAAI;IACxB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,UAAU,EAAE,qBAAqB,CAAC;IAClC,UAAU,EAAE,mBAAmB,CAAC;IAChC,OAAO,EAAE,sBAAsB,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;CACxC,CAAC;AAiDF,QAAA,MAAM,eAAe,sDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component-lifecycle proof for `mountListDataSurface` (#2906), in the same
|
|
3
|
+
* conformance style as `data-surface-conformance.integration.svelte.test.ts`:
|
|
4
|
+
* a real Svelte component mounts a custom (non-DataTable) list, the registry
|
|
5
|
+
* drives it with commands, and unmounting the component tears the
|
|
6
|
+
* registration down. The registration/translation logic itself is unit
|
|
7
|
+
* tested directly against the registry in `list-data-surface.test.ts`.
|
|
8
|
+
*/
|
|
9
|
+
import { createDataSurfaceRegistry, createDataTableController, } from '@happyvertical/smrt-ui/data';
|
|
10
|
+
import { render, screen } from '@testing-library/svelte';
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import Fixture from './list-data-surface.fixture.svelte';
|
|
13
|
+
const identity = {
|
|
14
|
+
surfaceId: 'fixture-list',
|
|
15
|
+
kind: 'list',
|
|
16
|
+
};
|
|
17
|
+
function descriptor() {
|
|
18
|
+
return {
|
|
19
|
+
version: 1,
|
|
20
|
+
identity,
|
|
21
|
+
schemaVersion: 1,
|
|
22
|
+
label: 'Fixture list',
|
|
23
|
+
rowKey: 'id',
|
|
24
|
+
columns: [
|
|
25
|
+
{ id: 'id', label: 'ID', capabilities: ['read', 'project'] },
|
|
26
|
+
{
|
|
27
|
+
id: 'title',
|
|
28
|
+
label: 'Title',
|
|
29
|
+
capabilities: ['read', 'search', 'filter', 'sort', 'project'],
|
|
30
|
+
operators: { filter: ['contains'] },
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
query: {
|
|
34
|
+
modes: ['rows', 'count'],
|
|
35
|
+
projectableColumnIds: ['id', 'title'],
|
|
36
|
+
filterableColumnIds: ['title'],
|
|
37
|
+
sortableColumnIds: ['title'],
|
|
38
|
+
},
|
|
39
|
+
controls: [
|
|
40
|
+
{ id: 'set-filters', label: 'Filter' },
|
|
41
|
+
{ id: 'refresh', label: 'Refresh' },
|
|
42
|
+
{ id: 'star', label: 'Star selected' },
|
|
43
|
+
],
|
|
44
|
+
actions: [],
|
|
45
|
+
limits: { maxQueryRows: 50, maxQueryBytes: 10_000, maxSelectionSize: 5 },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
describe('mountListDataSurface (component integration)', () => {
|
|
49
|
+
it('drives a mounted custom list from the registry and tears down on unmount', async () => {
|
|
50
|
+
const registry = createDataSurfaceRegistry();
|
|
51
|
+
const controller = createDataTableController();
|
|
52
|
+
const onRefresh = vi.fn().mockResolvedValue(true);
|
|
53
|
+
const { unmount } = render(Fixture, {
|
|
54
|
+
props: {
|
|
55
|
+
registry,
|
|
56
|
+
descriptor: descriptor(),
|
|
57
|
+
controller,
|
|
58
|
+
context: { totalRows: 3, queryFingerprint: 'fixture-1' },
|
|
59
|
+
onRefresh,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
expect(screen.getByTestId('filter-text').textContent).toBe('');
|
|
63
|
+
const before = registry.inspect(identity);
|
|
64
|
+
expect(before?.state.totalRows).toBe(3);
|
|
65
|
+
const filtered = await registry.execute({
|
|
66
|
+
version: 1,
|
|
67
|
+
commandId: 'filter-1',
|
|
68
|
+
identity,
|
|
69
|
+
expectedRevision: before?.revision ?? 0,
|
|
70
|
+
controlId: 'set-filters',
|
|
71
|
+
payload: {
|
|
72
|
+
filters: [{ columnId: 'title', operator: 'contains', value: 'draft' }],
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
expect(filtered.ok).toBe(true);
|
|
76
|
+
expect(screen.getByTestId('filter-text').textContent).toBe('draft');
|
|
77
|
+
const refreshed = await registry.execute({
|
|
78
|
+
version: 1,
|
|
79
|
+
commandId: 'refresh-1',
|
|
80
|
+
identity,
|
|
81
|
+
expectedRevision: registry.inspect(identity)?.revision ?? 0,
|
|
82
|
+
controlId: 'refresh',
|
|
83
|
+
});
|
|
84
|
+
expect(refreshed.ok).toBe(true);
|
|
85
|
+
expect(onRefresh).toHaveBeenCalledOnce();
|
|
86
|
+
const starred = await registry.execute({
|
|
87
|
+
version: 1,
|
|
88
|
+
commandId: 'star-1',
|
|
89
|
+
identity,
|
|
90
|
+
expectedRevision: registry.inspect(identity)?.revision ?? 0,
|
|
91
|
+
controlId: 'star',
|
|
92
|
+
});
|
|
93
|
+
expect(starred.ok).toBe(true);
|
|
94
|
+
expect(screen.getByTestId('starred-count').textContent).toBe('1');
|
|
95
|
+
unmount();
|
|
96
|
+
// Unmounting tore down the mounted surface — the identity is gone.
|
|
97
|
+
expect(registry.inspect(identity)).toBeUndefined();
|
|
98
|
+
await expect(registry.execute({
|
|
99
|
+
version: 1,
|
|
100
|
+
commandId: 'after-unmount',
|
|
101
|
+
identity,
|
|
102
|
+
expectedRevision: 0,
|
|
103
|
+
controlId: 'refresh',
|
|
104
|
+
})).resolves.toMatchObject({ ok: false, reason: 'not_found' });
|
|
105
|
+
});
|
|
106
|
+
});
|