@happyvertical/smrt-ui 0.51.39 → 0.52.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 +14 -2
- package/README.md +257 -5
- package/dist/actions/__tests__/swipe-dismiss.test.js +69 -0
- package/dist/actions/swipe-dismiss.d.ts +42 -0
- package/dist/actions/swipe-dismiss.d.ts.map +1 -0
- package/dist/actions/swipe-dismiss.js +69 -0
- package/dist/components/calendar/Calendar.svelte +5 -0
- package/dist/components/calendar/Calendar.svelte.d.ts.map +1 -1
- package/dist/components/calendar/CalendarView.svelte +1250 -0
- package/dist/components/calendar/CalendarView.svelte.d.ts +66 -0
- package/dist/components/calendar/CalendarView.svelte.d.ts.map +1 -0
- package/dist/components/calendar/DayView.svelte +5 -0
- package/dist/components/calendar/DayView.svelte.d.ts.map +1 -1
- package/dist/components/calendar/__tests__/CalendarView.test.js +226 -0
- package/dist/components/calendar/__tests__/calendar-model.test.js +138 -0
- package/dist/components/calendar/calendar-model.d.ts +178 -0
- package/dist/components/calendar/calendar-model.d.ts.map +1 -0
- package/dist/components/calendar/calendar-model.js +362 -0
- package/dist/components/calendar/index.d.ts +12 -1
- package/dist/components/calendar/index.d.ts.map +1 -1
- package/dist/components/calendar/index.js +11 -1
- package/dist/components/data/CollectionList.svelte +23 -5
- package/dist/components/data/CollectionList.svelte.d.ts +8 -2
- package/dist/components/data/CollectionList.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTable.svelte +118 -40
- package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTableController.d.ts +4 -0
- package/dist/components/data/DataTableController.d.ts.map +1 -1
- package/dist/components/data/DataTableController.js +11 -4
- package/dist/components/data/ListSortSelect.svelte +121 -0
- package/dist/components/data/ListSortSelect.svelte.d.ts +22 -0
- package/dist/components/data/ListSortSelect.svelte.d.ts.map +1 -0
- package/dist/components/data/SortableHeader.svelte +169 -0
- package/dist/components/data/SortableHeader.svelte.d.ts +30 -0
- package/dist/components/data/SortableHeader.svelte.d.ts.map +1 -0
- package/dist/components/data/__tests__/CollectionList.test.js +37 -0
- package/dist/components/data/__tests__/DataTable.test.js +76 -0
- package/dist/components/data/__tests__/DataTableController.test.js +35 -0
- package/dist/components/data/__tests__/SortableHeader.test.js +101 -0
- package/dist/components/data/__tests__/link-step-surface.test.js +172 -0
- package/dist/components/data/__tests__/list-sort-state.svelte.test.js +71 -0
- package/dist/components/data/__tests__/list-sort.test.js +91 -0
- package/dist/components/data/__tests__/surface-navigation.test.js +121 -0
- package/dist/components/data/data-surface.d.ts +1 -0
- package/dist/components/data/data-surface.d.ts.map +1 -1
- package/dist/components/data/data-surface.js +1 -0
- package/dist/components/data/index.d.ts +6 -0
- package/dist/components/data/index.d.ts.map +1 -1
- package/dist/components/data/index.js +6 -0
- package/dist/components/data/link-surface.d.ts +79 -0
- package/dist/components/data/link-surface.d.ts.map +1 -0
- package/dist/components/data/link-surface.js +155 -0
- package/dist/components/data/list-sort-state.svelte.d.ts +39 -0
- package/dist/components/data/list-sort-state.svelte.d.ts.map +1 -0
- package/dist/components/data/list-sort-state.svelte.js +47 -0
- package/dist/components/data/list-sort.d.ts +94 -0
- package/dist/components/data/list-sort.d.ts.map +1 -0
- package/dist/components/data/list-sort.js +182 -0
- package/dist/components/data/step-surface.d.ts +73 -0
- package/dist/components/data/step-surface.d.ts.map +1 -0
- package/dist/components/data/step-surface.js +177 -0
- package/dist/components/data/surface-navigation.d.ts +49 -0
- package/dist/components/data/surface-navigation.d.ts.map +1 -0
- package/dist/components/data/surface-navigation.js +100 -0
- package/dist/components/data/types.d.ts +19 -0
- package/dist/components/data/types.d.ts.map +1 -1
- package/dist/components/feedback/PhoneSheet.svelte +224 -0
- package/dist/components/feedback/PhoneSheet.svelte.d.ts +35 -0
- package/dist/components/feedback/PhoneSheet.svelte.d.ts.map +1 -0
- package/dist/components/feedback/WorkingStrip.svelte +426 -0
- package/dist/components/feedback/WorkingStrip.svelte.d.ts +37 -0
- package/dist/components/feedback/WorkingStrip.svelte.d.ts.map +1 -0
- package/dist/components/feedback/__tests__/PhoneSheet.test.js +101 -0
- package/dist/components/feedback/__tests__/WorkingStrip.test.js +172 -0
- package/dist/components/feedback/index.d.ts +4 -0
- package/dist/components/feedback/index.d.ts.map +1 -1
- package/dist/components/feedback/index.js +5 -0
- package/dist/components/feedback/working-status.d.ts +34 -0
- package/dist/components/feedback/working-status.d.ts.map +1 -0
- package/dist/components/feedback/working-status.js +5 -0
- package/dist/components/forms/Checkbox.svelte +26 -2
- package/dist/components/forms/Combobox.svelte +68 -12
- package/dist/components/forms/Combobox.svelte.d.ts +6 -0
- package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
- package/dist/components/forms/DictationButton.svelte +128 -0
- package/dist/components/forms/DictationButton.svelte.d.ts +19 -0
- package/dist/components/forms/DictationButton.svelte.d.ts.map +1 -0
- package/dist/components/forms/DictationStatus.svelte +164 -0
- package/dist/components/forms/DictationStatus.svelte.d.ts +11 -0
- package/dist/components/forms/DictationStatus.svelte.d.ts.map +1 -0
- package/dist/components/forms/Fieldset.svelte +5 -1
- package/dist/components/forms/Fieldset.svelte.d.ts +2 -0
- package/dist/components/forms/Fieldset.svelte.d.ts.map +1 -1
- package/dist/components/forms/FormActionBar.svelte +84 -0
- package/dist/components/forms/FormActionBar.svelte.d.ts +23 -0
- package/dist/components/forms/FormActionBar.svelte.d.ts.map +1 -0
- package/dist/components/forms/Input.svelte +9 -0
- package/dist/components/forms/Radio.svelte +15 -4
- package/dist/components/forms/SearchInput.svelte +207 -0
- package/dist/components/forms/SearchInput.svelte.d.ts +22 -0
- package/dist/components/forms/SearchInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/Select.svelte +9 -0
- package/dist/components/forms/StagedControlReview.svelte +21 -12
- package/dist/components/forms/StagedControlReview.svelte.d.ts.map +1 -1
- package/dist/components/forms/Switch.svelte +11 -5
- package/dist/components/forms/TagsInput.svelte +8 -1
- package/dist/components/forms/Textarea.svelte +8 -0
- package/dist/components/forms/__tests__/Combobox.test.js +87 -0
- package/dist/components/forms/__tests__/ControlHitArea.test.js +73 -0
- package/dist/components/forms/__tests__/DictationButton.test.js +176 -0
- package/dist/components/forms/__tests__/Fieldset.test.js +37 -0
- package/dist/components/forms/__tests__/FormActionBar.test.js +22 -0
- package/dist/components/forms/__tests__/SearchInput.test.js +92 -0
- package/dist/components/forms/__tests__/StagedControlReview.test.js +37 -3
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte +19 -0
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts +11 -0
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/control-interaction.test.js +19 -1
- package/dist/components/forms/__tests__/control-proposals.test.js +63 -0
- package/dist/components/forms/__tests__/dictation-recording.test.js +582 -0
- package/dist/components/forms/__tests__/dictation.test.js +486 -0
- package/dist/components/forms/__tests__/long-press.test.js +175 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte +33 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts +12 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/audio-capture.d.ts +69 -0
- package/dist/components/forms/audio-capture.d.ts.map +1 -0
- package/dist/components/forms/audio-capture.js +193 -0
- package/dist/components/forms/control-interaction.d.ts +6 -0
- package/dist/components/forms/control-interaction.d.ts.map +1 -1
- package/dist/components/forms/control-interaction.js +12 -1
- package/dist/components/forms/control-proposals.d.ts +55 -0
- package/dist/components/forms/control-proposals.d.ts.map +1 -0
- package/dist/components/forms/control-proposals.js +188 -0
- package/dist/components/forms/dictation-transcribe.d.ts +36 -0
- package/dist/components/forms/dictation-transcribe.d.ts.map +1 -0
- package/dist/components/forms/dictation-transcribe.js +65 -0
- package/dist/components/forms/dictation.svelte.d.ts +200 -0
- package/dist/components/forms/dictation.svelte.d.ts.map +1 -0
- package/dist/components/forms/dictation.svelte.js +686 -0
- package/dist/components/forms/index.d.ts +14 -1
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +14 -1
- package/dist/components/forms/insert-text.d.ts +12 -0
- package/dist/components/forms/insert-text.d.ts.map +1 -0
- package/dist/components/forms/insert-text.js +38 -0
- package/dist/components/forms/long-press.d.ts +88 -0
- package/dist/components/forms/long-press.d.ts.map +1 -0
- package/dist/components/forms/long-press.js +213 -0
- package/dist/components/forms/ready-beep.d.ts +24 -0
- package/dist/components/forms/ready-beep.d.ts.map +1 -0
- package/dist/components/forms/ready-beep.js +82 -0
- package/dist/components/layout/PageHeader.svelte +151 -17
- package/dist/components/layout/PageHeader.svelte.d.ts +47 -7
- package/dist/components/layout/PageHeader.svelte.d.ts.map +1 -1
- package/dist/components/layout/__tests__/PageHeader.test.js +87 -1
- package/dist/components/layout/index.d.ts +1 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/components/layout/index.js +1 -0
- package/dist/components/layout/page-header-context.d.ts +25 -0
- package/dist/components/layout/page-header-context.d.ts.map +1 -0
- package/dist/components/layout/page-header-context.js +16 -0
- package/dist/components/nav/Tabs.svelte +241 -0
- package/dist/components/nav/Tabs.svelte.d.ts +15 -0
- package/dist/components/nav/Tabs.svelte.d.ts.map +1 -1
- package/dist/components/nav/__tests__/LinkTabs.test.js +117 -0
- package/dist/components/nav/index.d.ts +1 -0
- package/dist/components/nav/index.d.ts.map +1 -1
- package/dist/components/nav/index.js +1 -0
- package/dist/components/nav/tabs-overflow.d.ts +12 -0
- package/dist/components/nav/tabs-overflow.d.ts.map +1 -0
- package/dist/components/nav/tabs-overflow.js +17 -0
- package/dist/components/nav/types.d.ts +8 -0
- package/dist/components/nav/types.d.ts.map +1 -1
- package/dist/components/ui/Button.svelte +7 -0
- package/dist/components/ui/Card.svelte +14 -5
- package/dist/components/ui/Dropdown.svelte +55 -1
- package/dist/components/ui/Dropdown.svelte.d.ts +8 -0
- package/dist/components/ui/Dropdown.svelte.d.ts.map +1 -1
- package/dist/components/ui/IconToggle.svelte +139 -0
- package/dist/components/ui/IconToggle.svelte.d.ts +32 -0
- package/dist/components/ui/IconToggle.svelte.d.ts.map +1 -0
- package/dist/components/ui/IconToggleGroup.svelte +56 -0
- package/dist/components/ui/IconToggleGroup.svelte.d.ts +21 -0
- package/dist/components/ui/IconToggleGroup.svelte.d.ts.map +1 -0
- package/dist/components/ui/Pagination.svelte +1 -1
- package/dist/components/ui/Popover.svelte +17 -1
- package/dist/components/ui/Popover.svelte.d.ts +8 -0
- package/dist/components/ui/Popover.svelte.d.ts.map +1 -1
- package/dist/components/ui/__tests__/IconToggle.test.js +59 -0
- package/dist/components/ui/__tests__/IconToggleGroup.test.js +30 -0
- package/dist/components/ui/__tests__/Popover.test.js +44 -0
- package/dist/components/ui/__tests__/dropdown-popover-icon.test.js +125 -0
- package/dist/components/ui/__tests__/phone-touch-targets.test.js +39 -0
- package/dist/components/ui/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +2 -0
- package/dist/i18n/strings.ui.d.ts +41 -0
- package/dist/i18n/strings.ui.d.ts.map +1 -1
- package/dist/i18n/strings.ui.js +46 -0
- package/dist/svelte/__tests__/playground.test.js +1 -0
- package/dist/svelte/playground/CalendarPreview.svelte +140 -0
- package/dist/svelte/playground/CalendarPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/CalendarPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +10 -0
- package/dist/types-generic.d.ts +2 -0
- package/dist/types-generic.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/svelte';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import ListSortSelect from '../ListSortSelect.svelte';
|
|
4
|
+
import { createLocalListSort, createUrlListSort, } from '../list-sort-state.svelte.js';
|
|
5
|
+
import SortableHeader from '../SortableHeader.svelte';
|
|
6
|
+
const spec = {
|
|
7
|
+
columns: [{ id: 'published', firstDirection: 'desc' }, 'title'],
|
|
8
|
+
default: { columnId: 'published', direction: 'desc' },
|
|
9
|
+
};
|
|
10
|
+
describe('list sort state', () => {
|
|
11
|
+
it('local: starts at the default and toggles', () => {
|
|
12
|
+
const list = createLocalListSort(spec);
|
|
13
|
+
expect(list.sort).toEqual(spec.default);
|
|
14
|
+
list.toggle('title');
|
|
15
|
+
expect(list.sort).toEqual({ columnId: 'title', direction: 'asc' });
|
|
16
|
+
list.toggle('title');
|
|
17
|
+
expect(list.sort).toEqual({ columnId: 'title', direction: 'desc' });
|
|
18
|
+
expect(list.href('title')).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
it('url: reads the URL, links headers, and navigates on set', async () => {
|
|
21
|
+
let url = new URL('https://x.test/list?sort=title&dir=asc&page=3');
|
|
22
|
+
const navigate = vi.fn((href) => {
|
|
23
|
+
url = new URL(href, url);
|
|
24
|
+
});
|
|
25
|
+
const list = createUrlListSort(spec, { url: () => url, navigate });
|
|
26
|
+
expect(list.sort).toEqual({ columnId: 'title', direction: 'asc' });
|
|
27
|
+
expect(list.href('title')).toBe('/list?sort=title&dir=desc');
|
|
28
|
+
expect(list.href('published')).toBe('/list');
|
|
29
|
+
await list.set({ columnId: 'published', direction: 'asc' });
|
|
30
|
+
expect(navigate).toHaveBeenCalledWith('/list?sort=published&dir=asc');
|
|
31
|
+
});
|
|
32
|
+
it('ignores an undeclared column or unknown direction on set', async () => {
|
|
33
|
+
const local = createLocalListSort(spec);
|
|
34
|
+
local.set({ columnId: 'secret_score', direction: 'asc' });
|
|
35
|
+
expect(local.sort).toEqual(spec.default);
|
|
36
|
+
local.set({ columnId: 'title', direction: 'sideways' });
|
|
37
|
+
expect(local.sort).toEqual(spec.default);
|
|
38
|
+
local.set({ columnId: 'title', direction: 'asc' });
|
|
39
|
+
expect(local.sort).toEqual({ columnId: 'title', direction: 'asc' });
|
|
40
|
+
// An invalid initial sort starts at the default.
|
|
41
|
+
expect(createLocalListSort(spec, { columnId: 'nope', direction: 'asc' }).sort).toEqual(spec.default);
|
|
42
|
+
const navigate = vi.fn();
|
|
43
|
+
const url = new URL('https://x.test/list');
|
|
44
|
+
const remote = createUrlListSort(spec, { url: () => url, navigate });
|
|
45
|
+
await remote.set({ columnId: 'secret_score', direction: 'desc' });
|
|
46
|
+
await remote.set(null);
|
|
47
|
+
expect(navigate).not.toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
it('drives SortableHeader and ListSortSelect through `list`', async () => {
|
|
50
|
+
const list = createLocalListSort(spec);
|
|
51
|
+
render(SortableHeader, {
|
|
52
|
+
props: { list, columnId: 'title', label: 'Title', as: 'div' },
|
|
53
|
+
});
|
|
54
|
+
const header = screen.getByRole('columnheader');
|
|
55
|
+
expect(header.getAttribute('aria-sort')).toBe('none');
|
|
56
|
+
await fireEvent.click(screen.getByRole('button', { name: 'Sort by Title, ascending' }));
|
|
57
|
+
expect(list.sort).toEqual({ columnId: 'title', direction: 'asc' });
|
|
58
|
+
render(ListSortSelect, {
|
|
59
|
+
props: {
|
|
60
|
+
list,
|
|
61
|
+
columns: [
|
|
62
|
+
{ id: 'published', label: 'Published', kind: 'date' },
|
|
63
|
+
{ id: 'title', label: 'Title' },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const select = screen.getByLabelText('Sort by');
|
|
68
|
+
await fireEvent.change(select, { target: { value: 'published:desc' } });
|
|
69
|
+
expect(list.sort).toEqual(spec.default);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { compareListValues, isValidListSort, listSortAria, listSortHref, listSortOrderBy, listSortSearchParams, parseListSort, sortListRows, toggleListSort, } from '../list-sort.js';
|
|
3
|
+
const spec = {
|
|
4
|
+
columns: [
|
|
5
|
+
{ id: 'published', firstDirection: 'desc' },
|
|
6
|
+
'title',
|
|
7
|
+
{ id: 'views', firstDirection: 'desc' },
|
|
8
|
+
],
|
|
9
|
+
default: { columnId: 'published', direction: 'desc' },
|
|
10
|
+
};
|
|
11
|
+
describe('list sort', () => {
|
|
12
|
+
it('parses the URL, falling back to the default for unknown values', () => {
|
|
13
|
+
expect(parseListSort(new URLSearchParams(''), spec)).toEqual(spec.default);
|
|
14
|
+
expect(parseListSort(new URLSearchParams('sort=title&dir=desc'), spec)).toEqual({
|
|
15
|
+
columnId: 'title',
|
|
16
|
+
direction: 'desc',
|
|
17
|
+
});
|
|
18
|
+
expect(parseListSort(new URLSearchParams('sort=title'), spec)).toEqual({
|
|
19
|
+
columnId: 'title',
|
|
20
|
+
direction: 'asc',
|
|
21
|
+
});
|
|
22
|
+
expect(parseListSort(new URLSearchParams('sort=views'), spec)).toEqual({
|
|
23
|
+
columnId: 'views',
|
|
24
|
+
direction: 'desc',
|
|
25
|
+
});
|
|
26
|
+
expect(parseListSort(new URLSearchParams('sort=password&dir=asc'), spec)).toEqual(spec.default);
|
|
27
|
+
expect(parseListSort(new URLSearchParams('dir=asc'), spec)).toEqual({
|
|
28
|
+
columnId: 'published',
|
|
29
|
+
direction: 'asc',
|
|
30
|
+
});
|
|
31
|
+
expect(parseListSort(new URLSearchParams('sort=title&dir=sideways'), spec)).toEqual({
|
|
32
|
+
columnId: 'title',
|
|
33
|
+
direction: 'asc',
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
it('toggles asc/desc and starts a new column at its first direction', () => {
|
|
37
|
+
const start = spec.default;
|
|
38
|
+
expect(toggleListSort(start, 'published', spec)).toEqual({
|
|
39
|
+
columnId: 'published',
|
|
40
|
+
direction: 'asc',
|
|
41
|
+
});
|
|
42
|
+
expect(toggleListSort({ columnId: 'published', direction: 'asc' }, 'published', spec)).toEqual(start);
|
|
43
|
+
expect(toggleListSort(start, 'title', spec)).toEqual({
|
|
44
|
+
columnId: 'title',
|
|
45
|
+
direction: 'asc',
|
|
46
|
+
});
|
|
47
|
+
expect(toggleListSort(start, 'views', spec)).toEqual({
|
|
48
|
+
columnId: 'views',
|
|
49
|
+
direction: 'desc',
|
|
50
|
+
});
|
|
51
|
+
expect(toggleListSort(start, 'nope', spec)).toBe(start);
|
|
52
|
+
expect(isValidListSort(spec, { columnId: 'nope', direction: 'asc' })).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
it('writes the sort to the URL, omits the default, and resets the page', () => {
|
|
55
|
+
const params = listSortSearchParams('status=draft&page=3', { columnId: 'title', direction: 'asc' }, spec);
|
|
56
|
+
expect(params.toString()).toBe('status=draft&sort=title&dir=asc');
|
|
57
|
+
expect(listSortSearchParams('sort=title&dir=asc&page=2', spec.default, spec).toString()).toBe('');
|
|
58
|
+
expect(listSortSearchParams('sort=title&dir=asc&page=2', { columnId: 'title', direction: 'asc' }, spec).toString()).toBe('sort=title&dir=asc&page=2');
|
|
59
|
+
expect(listSortHref(new URL('https://x.test/a?page=2'), { columnId: 'views', direction: 'desc' }, spec)).toBe('/a?sort=views&dir=desc');
|
|
60
|
+
expect(listSortHref(new URL('https://x.test/a?page=2'), spec.default, spec)).toBe('/a?page=2');
|
|
61
|
+
});
|
|
62
|
+
it('reports aria-sort', () => {
|
|
63
|
+
expect(listSortAria(spec.default, 'published')).toBe('descending');
|
|
64
|
+
expect(listSortAria({ columnId: 'title', direction: 'asc' }, 'title')).toBe('ascending');
|
|
65
|
+
expect(listSortAria(spec.default, 'title')).toBe('none');
|
|
66
|
+
});
|
|
67
|
+
it('sorts rows stably with empty values last in both directions', () => {
|
|
68
|
+
const rows = [
|
|
69
|
+
{ id: 1, at: '2026-01-02' },
|
|
70
|
+
{ id: 2, at: null },
|
|
71
|
+
{ id: 3, at: '2026-03-01' },
|
|
72
|
+
{ id: 4, at: '2026-01-02' },
|
|
73
|
+
];
|
|
74
|
+
const value = (row) => row.at;
|
|
75
|
+
expect(sortListRows(rows, { columnId: 'at', direction: 'desc' }, value).map((r) => r.id)).toEqual([3, 1, 4, 2]);
|
|
76
|
+
expect(sortListRows(rows, { columnId: 'at', direction: 'asc' }, value).map((r) => r.id)).toEqual([1, 4, 3, 2]);
|
|
77
|
+
expect(compareListValues('item 2', 'item 10', 'asc')).toBeLessThan(0);
|
|
78
|
+
expect(compareListValues(new Date('2026-01-01'), new Date('2025-01-01'), 'desc')).toBeLessThan(0);
|
|
79
|
+
});
|
|
80
|
+
it('builds ORDER BY only from the allow-list', () => {
|
|
81
|
+
const map = {
|
|
82
|
+
published: 'COALESCE(c.publish_date, c.created_at)',
|
|
83
|
+
title: 'c.title',
|
|
84
|
+
};
|
|
85
|
+
expect(listSortOrderBy(spec.default, map)).toBe('COALESCE(c.publish_date, c.created_at) DESC NULLS LAST');
|
|
86
|
+
expect(listSortOrderBy({ columnId: 'title', direction: 'asc' }, map, {
|
|
87
|
+
nulls: false,
|
|
88
|
+
})).toBe('c.title ASC');
|
|
89
|
+
expect(() => listSortOrderBy({ columnId: 'title; drop', direction: 'asc' }, map)).toThrow();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createDataSurfaceRegistry } from '../data-surface.js';
|
|
3
|
+
import { registerLinkSurface } from '../link-surface.js';
|
|
4
|
+
import { hasPendingSurfaceNavigation, trackSurfaceNavigation, whenSurfaceNavigationSettled, } from '../surface-navigation.js';
|
|
5
|
+
const links = [{ id: 'events', label: 'Events', href: '/s/events' }];
|
|
6
|
+
function deferred() {
|
|
7
|
+
let resolve;
|
|
8
|
+
const promise = new Promise((r) => {
|
|
9
|
+
resolve = r;
|
|
10
|
+
});
|
|
11
|
+
return { promise, resolve };
|
|
12
|
+
}
|
|
13
|
+
function openCommand(revision = 1) {
|
|
14
|
+
return {
|
|
15
|
+
version: 1,
|
|
16
|
+
commandId: `open-${Math.random()}`,
|
|
17
|
+
identity: { surfaceId: 'site-sections', kind: 'list' },
|
|
18
|
+
expectedRevision: revision,
|
|
19
|
+
controlId: 'open',
|
|
20
|
+
payload: { target: 'Events' },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
describe('whenSurfaceNavigationSettled', () => {
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.useRealTimers();
|
|
26
|
+
});
|
|
27
|
+
it('waits for a link surface navigation and the new page to register', async () => {
|
|
28
|
+
const registry = createDataSurfaceRegistry();
|
|
29
|
+
const navigation = deferred();
|
|
30
|
+
registerLinkSurface({
|
|
31
|
+
registry,
|
|
32
|
+
surfaceId: 'site-sections',
|
|
33
|
+
label: 'Site sections',
|
|
34
|
+
description: 'Pages',
|
|
35
|
+
links,
|
|
36
|
+
navigate: () => navigation.promise,
|
|
37
|
+
});
|
|
38
|
+
const result = await registry.execute(openCommand());
|
|
39
|
+
// The command answers at once; the page change is still running.
|
|
40
|
+
expect(result.ok).toBe(true);
|
|
41
|
+
expect(hasPendingSurfaceNavigation(registry)).toBe(true);
|
|
42
|
+
let settled = false;
|
|
43
|
+
const waiting = whenSurfaceNavigationSettled(registry, {
|
|
44
|
+
quietMs: 20,
|
|
45
|
+
}).then(() => {
|
|
46
|
+
settled = true;
|
|
47
|
+
});
|
|
48
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
49
|
+
expect(settled).toBe(false);
|
|
50
|
+
// The new page mounts its surface as the navigation finishes.
|
|
51
|
+
navigation.resolve();
|
|
52
|
+
await Promise.resolve();
|
|
53
|
+
registry.register({
|
|
54
|
+
descriptor: {
|
|
55
|
+
version: 1,
|
|
56
|
+
identity: { surfaceId: 'events', kind: 'list' },
|
|
57
|
+
schemaVersion: 1,
|
|
58
|
+
label: 'Events',
|
|
59
|
+
rowKey: 'id',
|
|
60
|
+
columns: [
|
|
61
|
+
{ id: 'id', label: 'ID', capabilities: ['read'], role: 'row-key' },
|
|
62
|
+
],
|
|
63
|
+
query: { modes: ['rows'], projectableColumnIds: ['id'] },
|
|
64
|
+
controls: [],
|
|
65
|
+
actions: [],
|
|
66
|
+
limits: { maxQueryRows: 1, maxQueryBytes: 1000, maxSelectionSize: 1 },
|
|
67
|
+
},
|
|
68
|
+
getSnapshot: () => ({ revision: 1, state: {}, selection: null }),
|
|
69
|
+
});
|
|
70
|
+
await waiting;
|
|
71
|
+
expect(settled).toBe(true);
|
|
72
|
+
expect(hasPendingSurfaceNavigation(registry)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
it('resolves after a quiet period when nothing navigates', async () => {
|
|
75
|
+
const registry = createDataSurfaceRegistry();
|
|
76
|
+
const started = Date.now();
|
|
77
|
+
await whenSurfaceNavigationSettled(registry, { quietMs: 10 });
|
|
78
|
+
expect(Date.now() - started).toBeLessThan(1_000);
|
|
79
|
+
});
|
|
80
|
+
it('gives up after the timeout, and a failed navigation still settles', async () => {
|
|
81
|
+
const registry = createDataSurfaceRegistry();
|
|
82
|
+
void trackSurfaceNavigation(registry, new Promise(() => { }));
|
|
83
|
+
const started = Date.now();
|
|
84
|
+
await whenSurfaceNavigationSettled(registry, {
|
|
85
|
+
quietMs: 5,
|
|
86
|
+
timeoutMs: 40,
|
|
87
|
+
});
|
|
88
|
+
expect(Date.now() - started).toBeLessThan(1_000);
|
|
89
|
+
const other = createDataSurfaceRegistry();
|
|
90
|
+
const failing = trackSurfaceNavigation(other, Promise.reject(new Error('nope')));
|
|
91
|
+
await expect(failing).rejects.toThrow('nope');
|
|
92
|
+
await whenSurfaceNavigationSettled(other, { quietMs: 5 });
|
|
93
|
+
expect(hasPendingSurfaceNavigation(other)).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
it('keeps waiting while a watched source keeps changing', async () => {
|
|
96
|
+
const registry = createDataSurfaceRegistry();
|
|
97
|
+
const listeners = new Set();
|
|
98
|
+
const tools = {
|
|
99
|
+
subscribe(listener) {
|
|
100
|
+
listeners.add(listener);
|
|
101
|
+
return () => listeners.delete(listener);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
let settled = false;
|
|
105
|
+
const waiting = whenSurfaceNavigationSettled(registry, {
|
|
106
|
+
quietMs: 30,
|
|
107
|
+
alsoWatch: [tools],
|
|
108
|
+
}).then(() => {
|
|
109
|
+
settled = true;
|
|
110
|
+
});
|
|
111
|
+
for (let i = 0; i < 3; i += 1) {
|
|
112
|
+
await new Promise((r) => setTimeout(r, 15));
|
|
113
|
+
for (const listener of listeners)
|
|
114
|
+
listener({ type: 'registered' });
|
|
115
|
+
}
|
|
116
|
+
expect(settled).toBe(false);
|
|
117
|
+
await waiting;
|
|
118
|
+
expect(settled).toBe(true);
|
|
119
|
+
expect(listeners.size).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -21,4 +21,5 @@ export declare function normalizeDataSurfaceQueryRequest(value: DataSurfaceQuery
|
|
|
21
21
|
export declare function normalizeDataSurfaceActionRequest(value: DataSurfaceActionRequest): DataSurfaceActionRequest;
|
|
22
22
|
/** Create an isolated registry; applications choose how it is exposed. */
|
|
23
23
|
export declare function createDataSurfaceRegistry(): DataSurfaceRegistry;
|
|
24
|
+
export * from './surface-navigation.js';
|
|
24
25
|
//# sourceMappingURL=data-surface.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"data-surface.d.ts","sourceRoot":"","sources":["../../../src/components/data/data-surface.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,wBAAwB,EAOxB,qBAAqB,EAMrB,uBAAuB,EAEvB,mBAAmB,EAMnB,mBAAmB,EAGnB,yBAAyB,EAC1B,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EACV,2BAA2B,EAC3B,wBAAwB,EACxB,uBAAuB,EACvB,2BAA2B,EAC3B,4BAA4B,EAC5B,2BAA2B,EAC3B,2BAA2B,EAC3B,0BAA0B,EAC1B,qBAAqB,EACrB,2BAA2B,EAC3B,2BAA2B,EAC3B,+BAA+B,EAC/B,wBAAwB,EACxB,qBAAqB,EACrB,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,EACxB,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,4BAA4B,EAC5B,oBAAoB,EACpB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,EACxB,gBAAgB,EAChB,6BAA6B,EAC7B,yBAAyB,EACzB,sBAAsB,EACtB,mBAAmB,EACnB,wBAAwB,EACxB,kBAAkB,EAClB,2BAA2B,EAC3B,2BAA2B,EAC3B,yBAAyB,EACzB,yBAAyB,GAC1B,MAAM,2BAA2B,CAAC;AAEnC,iFAAiF;AACjF,eAAO,MAAM,8BAA8B,SAAU,CAAC;AAEtD,sEAAsE;AACtE,eAAO,MAAM,kCAAkC,MAAM,CAAC;AAEtD,mEAAmE;AACnE,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAypBnD,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,qBAAqB,GAC3B,qBAAqB,CAgSvB;AAED,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,mBAAmB,GACzB,mBAAmB,CAsBrB;AAED,wBAAgB,kCAAkC,CAChD,KAAK,EAAE,yBAAyB,GAC/B,yBAAyB,CAoC3B;AAED,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,uBAAuB,GAC7B,uBAAuB,CA+FzB;AAED,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,wBAAwB,GAC9B,wBAAwB,CAmD1B;AAmED,0EAA0E;AAC1E,wBAAgB,yBAAyB,IAAI,mBAAmB,CAka/D"}
|
|
1
|
+
{"version":3,"file":"data-surface.d.ts","sourceRoot":"","sources":["../../../src/components/data/data-surface.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,wBAAwB,EAOxB,qBAAqB,EAMrB,uBAAuB,EAEvB,mBAAmB,EAMnB,mBAAmB,EAGnB,yBAAyB,EAC1B,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EACV,2BAA2B,EAC3B,wBAAwB,EACxB,uBAAuB,EACvB,2BAA2B,EAC3B,4BAA4B,EAC5B,2BAA2B,EAC3B,2BAA2B,EAC3B,0BAA0B,EAC1B,qBAAqB,EACrB,2BAA2B,EAC3B,2BAA2B,EAC3B,+BAA+B,EAC/B,wBAAwB,EACxB,qBAAqB,EACrB,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,EACxB,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,4BAA4B,EAC5B,oBAAoB,EACpB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,EACxB,gBAAgB,EAChB,6BAA6B,EAC7B,yBAAyB,EACzB,sBAAsB,EACtB,mBAAmB,EACnB,wBAAwB,EACxB,kBAAkB,EAClB,2BAA2B,EAC3B,2BAA2B,EAC3B,yBAAyB,EACzB,yBAAyB,GAC1B,MAAM,2BAA2B,CAAC;AAEnC,iFAAiF;AACjF,eAAO,MAAM,8BAA8B,SAAU,CAAC;AAEtD,sEAAsE;AACtE,eAAO,MAAM,kCAAkC,MAAM,CAAC;AAEtD,mEAAmE;AACnE,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAypBnD,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,qBAAqB,GAC3B,qBAAqB,CAgSvB;AAED,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,mBAAmB,GACzB,mBAAmB,CAsBrB;AAED,wBAAgB,kCAAkC,CAChD,KAAK,EAAE,yBAAyB,GAC/B,yBAAyB,CAoC3B;AAED,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,uBAAuB,GAC7B,uBAAuB,CA+FzB;AAED,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,wBAAwB,GAC9B,wBAAwB,CAmD1B;AAmED,0EAA0E;AAC1E,wBAAgB,yBAAyB,IAAI,mBAAmB,CAka/D;AACD,cAAc,yBAAyB,CAAC"}
|
|
@@ -10,5 +10,11 @@ export * from './DataTablePerformance.js';
|
|
|
10
10
|
export * from './DataTableVirtualization.js';
|
|
11
11
|
export * from './data-surface.js';
|
|
12
12
|
export * from './data-table-surface.js';
|
|
13
|
+
export { default as ListSortSelect } from './ListSortSelect.svelte';
|
|
14
|
+
export * from './link-surface.js';
|
|
15
|
+
export * from './list-sort.js';
|
|
16
|
+
export * from './list-sort-state.svelte.js';
|
|
17
|
+
export { default as SortableHeader } from './SortableHeader.svelte';
|
|
18
|
+
export * from './step-surface.js';
|
|
13
19
|
export * from './types.js';
|
|
14
20
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/data/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,OAAO,IAAI,cAAc,EACzB,OAAO,IAAI,WAAW,GACvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/data/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,OAAO,IAAI,cAAc,EACzB,OAAO,IAAI,WAAW,GACvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC"}
|
|
@@ -10,4 +10,10 @@ export * from './DataTablePerformance.js';
|
|
|
10
10
|
export * from './DataTableVirtualization.js';
|
|
11
11
|
export * from './data-surface.js';
|
|
12
12
|
export * from './data-table-surface.js';
|
|
13
|
+
export { default as ListSortSelect } from './ListSortSelect.svelte';
|
|
14
|
+
export * from './link-surface.js';
|
|
15
|
+
export * from './list-sort.js';
|
|
16
|
+
export * from './list-sort-state.svelte.js';
|
|
17
|
+
export { default as SortableHeader } from './SortableHeader.svelte';
|
|
18
|
+
export * from './step-surface.js';
|
|
13
19
|
export * from './types.js';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A navigation list (a menu, a tab row, a section list) as a mounted data
|
|
3
|
+
* surface, so an agent can move a person between pages without a REST path.
|
|
4
|
+
*
|
|
5
|
+
* A view intent can only dispatch a registry command, so "go to page X" needs
|
|
6
|
+
* a mounted surface whose visible control performs the navigation. This
|
|
7
|
+
* registers one: the links are published in the snapshot state
|
|
8
|
+
* (`state.links`: id, label, description, group — never an href an agent
|
|
9
|
+
* could steer), and each declared control resolves its payload to one of
|
|
10
|
+
* those links (or to a caller-built href) and hands it to `navigate`.
|
|
11
|
+
*
|
|
12
|
+
* Navigation is a read-only view-state change: nothing is created or changed.
|
|
13
|
+
*/
|
|
14
|
+
import type { DataSurfaceDescriptor, DataSurfaceJsonValue, DataSurfaceKind, DataSurfaceRegistry, DataSurfaceSubject } from '@happyvertical/smrt-types';
|
|
15
|
+
export interface LinkSurfaceItem {
|
|
16
|
+
/** Stable id, e.g. the nav item or tab id. */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Plain label a person sees in the menu. */
|
|
19
|
+
label: string;
|
|
20
|
+
href: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
/** Group or section heading the link sits under, if any. */
|
|
23
|
+
group?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface LinkSurfaceControl {
|
|
26
|
+
id: string;
|
|
27
|
+
label: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a command payload to an href, or `null` to refuse. Defaults to
|
|
31
|
+
* {@link resolveLinkTarget} over the published links.
|
|
32
|
+
*/
|
|
33
|
+
resolve?: (payload: DataSurfaceJsonValue | undefined, links: readonly LinkSurfaceItem[]) => string | null;
|
|
34
|
+
}
|
|
35
|
+
export interface LinkSurfaceOptions {
|
|
36
|
+
registry: DataSurfaceRegistry;
|
|
37
|
+
surfaceId: string;
|
|
38
|
+
kind?: DataSurfaceKind;
|
|
39
|
+
subject?: DataSurfaceSubject;
|
|
40
|
+
label: string;
|
|
41
|
+
description: string;
|
|
42
|
+
links: readonly LinkSurfaceItem[];
|
|
43
|
+
/** Defaults to one `open` control over the published links. */
|
|
44
|
+
controls?: readonly LinkSurfaceControl[];
|
|
45
|
+
/**
|
|
46
|
+
* Perform the navigation. Return its promise (e.g. SvelteKit's `goto`):
|
|
47
|
+
* the command does not wait for it, since the new page may unmount this
|
|
48
|
+
* surface, but it is tracked on the registry, so
|
|
49
|
+
* `whenSurfaceNavigationSettled(registry)` resolves only once it finished
|
|
50
|
+
* and the new page's surfaces registered.
|
|
51
|
+
*/
|
|
52
|
+
navigate: (href: string) => unknown;
|
|
53
|
+
/** Extra JSON state published alongside the links (e.g. the active id). */
|
|
54
|
+
state?: Record<string, DataSurfaceJsonValue>;
|
|
55
|
+
}
|
|
56
|
+
export interface LinkSurfaceHandle {
|
|
57
|
+
update(next: {
|
|
58
|
+
links?: readonly LinkSurfaceItem[];
|
|
59
|
+
state?: Record<string, DataSurfaceJsonValue>;
|
|
60
|
+
}): void;
|
|
61
|
+
destroy(): void;
|
|
62
|
+
}
|
|
63
|
+
/** Cap on published links (and the surface's query limit). */
|
|
64
|
+
export declare const LINK_SURFACE_MAX_LINKS = 200;
|
|
65
|
+
/**
|
|
66
|
+
* The string an agent passed: `"Events"` or `{ target: "Events" }` (also
|
|
67
|
+
* `id` / `label` / `section` / `tab` / `page`).
|
|
68
|
+
*/
|
|
69
|
+
export declare function linkTargetFromPayload(payload: DataSurfaceJsonValue | undefined): string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a payload to one link's href: by id, then exact label, then a unique
|
|
72
|
+
* label prefix, all case-insensitive. Ambiguity resolves to `null`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveLinkTarget(payload: DataSurfaceJsonValue | undefined, links: readonly LinkSurfaceItem[]): string | null;
|
|
75
|
+
/** The descriptor {@link registerLinkSurface} mounts. */
|
|
76
|
+
export declare function linkSurfaceDescriptor(options: Pick<LinkSurfaceOptions, 'surfaceId' | 'kind' | 'subject' | 'label' | 'description' | 'controls'>): DataSurfaceDescriptor;
|
|
77
|
+
/** Mount a navigation list on a data-surface registry. */
|
|
78
|
+
export declare function registerLinkSurface(options: LinkSurfaceOptions): LinkSurfaceHandle;
|
|
79
|
+
//# sourceMappingURL=link-surface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-surface.d.ts","sourceRoot":"","sources":["../../../src/components/data/link-surface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EACV,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,mBAAmB,EACnB,kBAAkB,EAEnB,MAAM,2BAA2B,CAAC;AAGnC,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,CACR,OAAO,EAAE,oBAAoB,GAAG,SAAS,EACzC,KAAK,EAAE,SAAS,eAAe,EAAE,KAC9B,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAClC,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACzC;;;;;;OAMG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACpC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;CAC9C;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,IAAI,EAAE;QACX,KAAK,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;QACnC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;KAC9C,GAAG,IAAI,CAAC;IACT,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,8DAA8D;AAC9D,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAe1C;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,oBAAoB,GAAG,SAAS,GACxC,MAAM,GAAG,IAAI,CASf;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,GAAG,SAAS,EACzC,KAAK,EAAE,SAAS,eAAe,EAAE,GAChC,MAAM,GAAG,IAAI,CAaf;AAaD,yDAAyD;AACzD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,IAAI,CACX,kBAAkB,EAClB,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,aAAa,GAAG,UAAU,CACxE,GACA,qBAAqB,CAqCvB;AAED,0DAA0D;AAC1D,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,kBAAkB,GAC1B,iBAAiB,CAiDnB"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A navigation list (a menu, a tab row, a section list) as a mounted data
|
|
3
|
+
* surface, so an agent can move a person between pages without a REST path.
|
|
4
|
+
*
|
|
5
|
+
* A view intent can only dispatch a registry command, so "go to page X" needs
|
|
6
|
+
* a mounted surface whose visible control performs the navigation. This
|
|
7
|
+
* registers one: the links are published in the snapshot state
|
|
8
|
+
* (`state.links`: id, label, description, group — never an href an agent
|
|
9
|
+
* could steer), and each declared control resolves its payload to one of
|
|
10
|
+
* those links (or to a caller-built href) and hands it to `navigate`.
|
|
11
|
+
*
|
|
12
|
+
* Navigation is a read-only view-state change: nothing is created or changed.
|
|
13
|
+
*/
|
|
14
|
+
import { trackSurfaceNavigation } from './surface-navigation.js';
|
|
15
|
+
/** Cap on published links (and the surface's query limit). */
|
|
16
|
+
export const LINK_SURFACE_MAX_LINKS = 200;
|
|
17
|
+
const DEFAULT_CONTROLS = [
|
|
18
|
+
{
|
|
19
|
+
id: 'open',
|
|
20
|
+
label: 'Open',
|
|
21
|
+
description: 'Go to one of the listed pages. Payload: { target } — a link id or its label.',
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
function normalize(value) {
|
|
25
|
+
return value.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The string an agent passed: `"Events"` or `{ target: "Events" }` (also
|
|
29
|
+
* `id` / `label` / `section` / `tab` / `page`).
|
|
30
|
+
*/
|
|
31
|
+
export function linkTargetFromPayload(payload) {
|
|
32
|
+
if (typeof payload === 'string')
|
|
33
|
+
return payload.trim() ? payload : null;
|
|
34
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
35
|
+
for (const key of ['target', 'id', 'label', 'section', 'tab', 'page']) {
|
|
36
|
+
const value = payload[key];
|
|
37
|
+
if (typeof value === 'string' && value.trim())
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a payload to one link's href: by id, then exact label, then a unique
|
|
45
|
+
* label prefix, all case-insensitive. Ambiguity resolves to `null`.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveLinkTarget(payload, links) {
|
|
48
|
+
const target = linkTargetFromPayload(payload);
|
|
49
|
+
if (!target)
|
|
50
|
+
return null;
|
|
51
|
+
const wanted = normalize(target);
|
|
52
|
+
const byId = links.find((link) => normalize(link.id) === wanted);
|
|
53
|
+
if (byId)
|
|
54
|
+
return byId.href;
|
|
55
|
+
const byLabel = links.filter((link) => normalize(link.label) === wanted);
|
|
56
|
+
if (byLabel.length === 1)
|
|
57
|
+
return byLabel[0].href;
|
|
58
|
+
if (byLabel.length > 1)
|
|
59
|
+
return null;
|
|
60
|
+
const byPrefix = links.filter((link) => normalize(link.label).startsWith(wanted));
|
|
61
|
+
return byPrefix.length === 1 ? byPrefix[0].href : null;
|
|
62
|
+
}
|
|
63
|
+
function publishedLinks(links) {
|
|
64
|
+
return links.slice(0, LINK_SURFACE_MAX_LINKS).map((link) => ({
|
|
65
|
+
id: link.id,
|
|
66
|
+
label: link.label,
|
|
67
|
+
...(link.description ? { description: link.description } : {}),
|
|
68
|
+
...(link.group ? { group: link.group } : {}),
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
/** The descriptor {@link registerLinkSurface} mounts. */
|
|
72
|
+
export function linkSurfaceDescriptor(options) {
|
|
73
|
+
const columns = [
|
|
74
|
+
{ id: 'id', label: 'ID', capabilities: ['read'], role: 'row-key' },
|
|
75
|
+
{ id: 'label', label: 'Name', capabilities: ['read'] },
|
|
76
|
+
{ id: 'description', label: 'About', capabilities: ['read'] },
|
|
77
|
+
{ id: 'group', label: 'Group', capabilities: ['read'] },
|
|
78
|
+
];
|
|
79
|
+
return {
|
|
80
|
+
version: 1,
|
|
81
|
+
identity: {
|
|
82
|
+
kind: options.kind ?? 'list',
|
|
83
|
+
surfaceId: options.surfaceId,
|
|
84
|
+
...(options.subject ? { subject: options.subject } : {}),
|
|
85
|
+
},
|
|
86
|
+
schemaVersion: 1,
|
|
87
|
+
label: options.label,
|
|
88
|
+
description: options.description,
|
|
89
|
+
rowKey: 'id',
|
|
90
|
+
columns,
|
|
91
|
+
query: {
|
|
92
|
+
modes: ['rows'],
|
|
93
|
+
projectableColumnIds: columns.map((column) => column.id),
|
|
94
|
+
filterableColumnIds: [],
|
|
95
|
+
sortableColumnIds: [],
|
|
96
|
+
},
|
|
97
|
+
controls: (options.controls ?? DEFAULT_CONTROLS).map((control) => ({
|
|
98
|
+
id: control.id,
|
|
99
|
+
label: control.label,
|
|
100
|
+
...(control.description ? { description: control.description } : {}),
|
|
101
|
+
})),
|
|
102
|
+
actions: [],
|
|
103
|
+
limits: {
|
|
104
|
+
maxQueryRows: LINK_SURFACE_MAX_LINKS,
|
|
105
|
+
maxQueryBytes: 200_000,
|
|
106
|
+
maxSelectionSize: 1,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Mount a navigation list on a data-surface registry. */
|
|
111
|
+
export function registerLinkSurface(options) {
|
|
112
|
+
let links = [...options.links];
|
|
113
|
+
let extraState = { ...(options.state ?? {}) };
|
|
114
|
+
let revision = 1;
|
|
115
|
+
const controls = new Map((options.controls ?? DEFAULT_CONTROLS).map((control) => [
|
|
116
|
+
control.id,
|
|
117
|
+
control,
|
|
118
|
+
]));
|
|
119
|
+
const dispose = options.registry.register({
|
|
120
|
+
descriptor: linkSurfaceDescriptor(options),
|
|
121
|
+
getSnapshot: () => ({
|
|
122
|
+
revision,
|
|
123
|
+
state: { ...extraState, links: publishedLinks(links) },
|
|
124
|
+
selection: null,
|
|
125
|
+
}),
|
|
126
|
+
execute: (command) => {
|
|
127
|
+
const control = controls.get(command.controlId);
|
|
128
|
+
if (!control)
|
|
129
|
+
return { ok: false };
|
|
130
|
+
const href = (control.resolve ?? resolveLinkTarget)(command.payload, links);
|
|
131
|
+
if (!href)
|
|
132
|
+
return { ok: false };
|
|
133
|
+
void trackSurfaceNavigation(options.registry, options.navigate(href));
|
|
134
|
+
return undefined;
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
update(next) {
|
|
139
|
+
const nextLinks = next.links ? [...next.links] : links;
|
|
140
|
+
const nextState = next.state ? { ...next.state } : extraState;
|
|
141
|
+
const changed = JSON.stringify(publishedLinks(nextLinks)) !==
|
|
142
|
+
JSON.stringify(publishedLinks(links)) ||
|
|
143
|
+
JSON.stringify(nextState) !== JSON.stringify(extraState) ||
|
|
144
|
+
nextLinks.length !== links.length ||
|
|
145
|
+
nextLinks.some((link, index) => link.href !== links[index]?.href);
|
|
146
|
+
links = nextLinks;
|
|
147
|
+
extraState = nextState;
|
|
148
|
+
if (changed)
|
|
149
|
+
revision += 1;
|
|
150
|
+
},
|
|
151
|
+
destroy() {
|
|
152
|
+
dispose();
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive list-sort state for pages — the one object a list's headers
|
|
3
|
+
* (`SortableHeader list={...}`), phone picker (`ListSortSelect list={...}`),
|
|
4
|
+
* rows (`sortListRows(rows, list.sort, ...)`), and data surface (`sort` /
|
|
5
|
+
* `onSort`) share. Call during component initialisation.
|
|
6
|
+
*
|
|
7
|
+
* - `createUrlListSort(spec, { url, navigate })` — server-paginated lists:
|
|
8
|
+
* the sort lives in the URL (`?sort=&dir=`); headers are links, and `set`
|
|
9
|
+
* navigates. Framework-agnostic: pass the router's current URL and its
|
|
10
|
+
* navigate function (SvelteKit: `() => page.url` and `goto`).
|
|
11
|
+
* - `createLocalListSort(spec)` — lists that load every row: page state.
|
|
12
|
+
*/
|
|
13
|
+
import { type ListSort, type ListSortSpec } from './list-sort.js';
|
|
14
|
+
export interface ListSortState {
|
|
15
|
+
readonly sort: ListSort;
|
|
16
|
+
readonly spec: ListSortSpec;
|
|
17
|
+
/** Link to the list after activating `columnId`'s header (URL lists only). */
|
|
18
|
+
href(columnId: string): string | undefined;
|
|
19
|
+
/** Activate a header: toggle `columnId` (URL lists: the link navigates). */
|
|
20
|
+
toggle(columnId: string): void | Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Apply a sort (headers, the "Sort by" picker, agents). A sort naming an
|
|
23
|
+
* undeclared column or an unknown direction is ignored.
|
|
24
|
+
*/
|
|
25
|
+
set(sort: ListSort): void | Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export interface UrlListSortOptions {
|
|
28
|
+
/** The current URL (reactive, e.g. SvelteKit's `page.url`). */
|
|
29
|
+
url: () => URL | {
|
|
30
|
+
pathname: string;
|
|
31
|
+
search: string;
|
|
32
|
+
searchParams: URLSearchParams;
|
|
33
|
+
};
|
|
34
|
+
/** Navigate to a same-page href (keep focus and scroll, replace history). */
|
|
35
|
+
navigate: (href: string) => void | Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export declare function createUrlListSort(spec: ListSortSpec, options: UrlListSortOptions): ListSortState;
|
|
38
|
+
export declare function createLocalListSort(spec: ListSortSpec, initial?: ListSort): ListSortState;
|
|
39
|
+
//# sourceMappingURL=list-sort-state.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-sort-state.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/data/list-sort-state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,YAAY,EAIlB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,8EAA8E;IAC9E,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAC3C,4EAA4E;IAC5E,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C;;;OAGG;IACH,GAAG,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,GAAG,EAAE,MACD,GAAG,GACH;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,eAAe,CAAA;KAAE,CAAC;IACxE,6EAA6E;IAC7E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClD;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,kBAAkB,GAC1B,aAAa,CAef;AAED,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,YAAY,EAClB,OAAO,CAAC,EAAE,QAAQ,GACjB,aAAa,CAkBf"}
|