@happyvertical/smrt-ui 0.51.38 → 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.
Files changed (212) hide show
  1. package/AGENTS.md +14 -2
  2. package/README.md +257 -5
  3. package/dist/actions/__tests__/swipe-dismiss.test.js +69 -0
  4. package/dist/actions/swipe-dismiss.d.ts +42 -0
  5. package/dist/actions/swipe-dismiss.d.ts.map +1 -0
  6. package/dist/actions/swipe-dismiss.js +69 -0
  7. package/dist/components/calendar/Calendar.svelte +5 -0
  8. package/dist/components/calendar/Calendar.svelte.d.ts.map +1 -1
  9. package/dist/components/calendar/CalendarView.svelte +1250 -0
  10. package/dist/components/calendar/CalendarView.svelte.d.ts +66 -0
  11. package/dist/components/calendar/CalendarView.svelte.d.ts.map +1 -0
  12. package/dist/components/calendar/DayView.svelte +5 -0
  13. package/dist/components/calendar/DayView.svelte.d.ts.map +1 -1
  14. package/dist/components/calendar/__tests__/CalendarView.test.js +226 -0
  15. package/dist/components/calendar/__tests__/calendar-model.test.js +138 -0
  16. package/dist/components/calendar/calendar-model.d.ts +178 -0
  17. package/dist/components/calendar/calendar-model.d.ts.map +1 -0
  18. package/dist/components/calendar/calendar-model.js +362 -0
  19. package/dist/components/calendar/index.d.ts +12 -1
  20. package/dist/components/calendar/index.d.ts.map +1 -1
  21. package/dist/components/calendar/index.js +11 -1
  22. package/dist/components/data/CollectionList.svelte +23 -5
  23. package/dist/components/data/CollectionList.svelte.d.ts +8 -2
  24. package/dist/components/data/CollectionList.svelte.d.ts.map +1 -1
  25. package/dist/components/data/DataTable.svelte +118 -40
  26. package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
  27. package/dist/components/data/DataTableController.d.ts +4 -0
  28. package/dist/components/data/DataTableController.d.ts.map +1 -1
  29. package/dist/components/data/DataTableController.js +11 -4
  30. package/dist/components/data/ListSortSelect.svelte +121 -0
  31. package/dist/components/data/ListSortSelect.svelte.d.ts +22 -0
  32. package/dist/components/data/ListSortSelect.svelte.d.ts.map +1 -0
  33. package/dist/components/data/SortableHeader.svelte +169 -0
  34. package/dist/components/data/SortableHeader.svelte.d.ts +30 -0
  35. package/dist/components/data/SortableHeader.svelte.d.ts.map +1 -0
  36. package/dist/components/data/__tests__/CollectionList.test.js +37 -0
  37. package/dist/components/data/__tests__/DataTable.test.js +76 -0
  38. package/dist/components/data/__tests__/DataTableController.test.js +35 -0
  39. package/dist/components/data/__tests__/SortableHeader.test.js +101 -0
  40. package/dist/components/data/__tests__/link-step-surface.test.js +172 -0
  41. package/dist/components/data/__tests__/list-sort-state.svelte.test.js +71 -0
  42. package/dist/components/data/__tests__/list-sort.test.js +91 -0
  43. package/dist/components/data/__tests__/surface-navigation.test.js +121 -0
  44. package/dist/components/data/data-surface.d.ts +1 -0
  45. package/dist/components/data/data-surface.d.ts.map +1 -1
  46. package/dist/components/data/data-surface.js +1 -0
  47. package/dist/components/data/index.d.ts +6 -0
  48. package/dist/components/data/index.d.ts.map +1 -1
  49. package/dist/components/data/index.js +6 -0
  50. package/dist/components/data/link-surface.d.ts +79 -0
  51. package/dist/components/data/link-surface.d.ts.map +1 -0
  52. package/dist/components/data/link-surface.js +155 -0
  53. package/dist/components/data/list-sort-state.svelte.d.ts +39 -0
  54. package/dist/components/data/list-sort-state.svelte.d.ts.map +1 -0
  55. package/dist/components/data/list-sort-state.svelte.js +47 -0
  56. package/dist/components/data/list-sort.d.ts +94 -0
  57. package/dist/components/data/list-sort.d.ts.map +1 -0
  58. package/dist/components/data/list-sort.js +182 -0
  59. package/dist/components/data/step-surface.d.ts +73 -0
  60. package/dist/components/data/step-surface.d.ts.map +1 -0
  61. package/dist/components/data/step-surface.js +177 -0
  62. package/dist/components/data/surface-navigation.d.ts +49 -0
  63. package/dist/components/data/surface-navigation.d.ts.map +1 -0
  64. package/dist/components/data/surface-navigation.js +100 -0
  65. package/dist/components/data/types.d.ts +19 -0
  66. package/dist/components/data/types.d.ts.map +1 -1
  67. package/dist/components/feedback/PhoneSheet.svelte +224 -0
  68. package/dist/components/feedback/PhoneSheet.svelte.d.ts +35 -0
  69. package/dist/components/feedback/PhoneSheet.svelte.d.ts.map +1 -0
  70. package/dist/components/feedback/WorkingStrip.svelte +426 -0
  71. package/dist/components/feedback/WorkingStrip.svelte.d.ts +37 -0
  72. package/dist/components/feedback/WorkingStrip.svelte.d.ts.map +1 -0
  73. package/dist/components/feedback/__tests__/PhoneSheet.test.js +101 -0
  74. package/dist/components/feedback/__tests__/WorkingStrip.test.js +172 -0
  75. package/dist/components/feedback/index.d.ts +4 -0
  76. package/dist/components/feedback/index.d.ts.map +1 -1
  77. package/dist/components/feedback/index.js +5 -0
  78. package/dist/components/feedback/working-status.d.ts +34 -0
  79. package/dist/components/feedback/working-status.d.ts.map +1 -0
  80. package/dist/components/feedback/working-status.js +5 -0
  81. package/dist/components/forms/Checkbox.svelte +26 -2
  82. package/dist/components/forms/Combobox.svelte +68 -12
  83. package/dist/components/forms/Combobox.svelte.d.ts +6 -0
  84. package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
  85. package/dist/components/forms/DictationButton.svelte +128 -0
  86. package/dist/components/forms/DictationButton.svelte.d.ts +19 -0
  87. package/dist/components/forms/DictationButton.svelte.d.ts.map +1 -0
  88. package/dist/components/forms/DictationStatus.svelte +164 -0
  89. package/dist/components/forms/DictationStatus.svelte.d.ts +11 -0
  90. package/dist/components/forms/DictationStatus.svelte.d.ts.map +1 -0
  91. package/dist/components/forms/Fieldset.svelte +5 -1
  92. package/dist/components/forms/Fieldset.svelte.d.ts +2 -0
  93. package/dist/components/forms/Fieldset.svelte.d.ts.map +1 -1
  94. package/dist/components/forms/FormActionBar.svelte +84 -0
  95. package/dist/components/forms/FormActionBar.svelte.d.ts +23 -0
  96. package/dist/components/forms/FormActionBar.svelte.d.ts.map +1 -0
  97. package/dist/components/forms/Input.svelte +9 -0
  98. package/dist/components/forms/Radio.svelte +15 -4
  99. package/dist/components/forms/SearchInput.svelte +207 -0
  100. package/dist/components/forms/SearchInput.svelte.d.ts +22 -0
  101. package/dist/components/forms/SearchInput.svelte.d.ts.map +1 -0
  102. package/dist/components/forms/Select.svelte +9 -0
  103. package/dist/components/forms/StagedControlReview.svelte +21 -12
  104. package/dist/components/forms/StagedControlReview.svelte.d.ts.map +1 -1
  105. package/dist/components/forms/Switch.svelte +11 -5
  106. package/dist/components/forms/TagsInput.svelte +8 -1
  107. package/dist/components/forms/Textarea.svelte +8 -0
  108. package/dist/components/forms/__tests__/Combobox.test.js +87 -0
  109. package/dist/components/forms/__tests__/ControlHitArea.test.js +73 -0
  110. package/dist/components/forms/__tests__/DictationButton.test.js +176 -0
  111. package/dist/components/forms/__tests__/Fieldset.test.js +37 -0
  112. package/dist/components/forms/__tests__/FormActionBar.test.js +22 -0
  113. package/dist/components/forms/__tests__/SearchInput.test.js +92 -0
  114. package/dist/components/forms/__tests__/StagedControlReview.test.js +37 -3
  115. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte +19 -0
  116. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts +11 -0
  117. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts.map +1 -0
  118. package/dist/components/forms/__tests__/composite-registration.fixture.svelte +29 -0
  119. package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts +8 -0
  120. package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts.map +1 -0
  121. package/dist/components/forms/__tests__/control-interaction.test.js +19 -1
  122. package/dist/components/forms/__tests__/control-proposals.test.js +63 -0
  123. package/dist/components/forms/__tests__/dictation-recording.test.js +582 -0
  124. package/dist/components/forms/__tests__/dictation.test.js +486 -0
  125. package/dist/components/forms/__tests__/long-press.test.js +175 -0
  126. package/dist/components/forms/__tests__/place-picker.fixture.svelte +33 -0
  127. package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts +12 -0
  128. package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts.map +1 -0
  129. package/dist/components/forms/audio-capture.d.ts +69 -0
  130. package/dist/components/forms/audio-capture.d.ts.map +1 -0
  131. package/dist/components/forms/audio-capture.js +193 -0
  132. package/dist/components/forms/control-interaction.d.ts +6 -0
  133. package/dist/components/forms/control-interaction.d.ts.map +1 -1
  134. package/dist/components/forms/control-interaction.js +12 -1
  135. package/dist/components/forms/control-proposals.d.ts +55 -0
  136. package/dist/components/forms/control-proposals.d.ts.map +1 -0
  137. package/dist/components/forms/control-proposals.js +188 -0
  138. package/dist/components/forms/dictation-transcribe.d.ts +36 -0
  139. package/dist/components/forms/dictation-transcribe.d.ts.map +1 -0
  140. package/dist/components/forms/dictation-transcribe.js +65 -0
  141. package/dist/components/forms/dictation.svelte.d.ts +200 -0
  142. package/dist/components/forms/dictation.svelte.d.ts.map +1 -0
  143. package/dist/components/forms/dictation.svelte.js +686 -0
  144. package/dist/components/forms/index.d.ts +14 -1
  145. package/dist/components/forms/index.d.ts.map +1 -1
  146. package/dist/components/forms/index.js +14 -1
  147. package/dist/components/forms/insert-text.d.ts +12 -0
  148. package/dist/components/forms/insert-text.d.ts.map +1 -0
  149. package/dist/components/forms/insert-text.js +38 -0
  150. package/dist/components/forms/long-press.d.ts +88 -0
  151. package/dist/components/forms/long-press.d.ts.map +1 -0
  152. package/dist/components/forms/long-press.js +213 -0
  153. package/dist/components/forms/ready-beep.d.ts +24 -0
  154. package/dist/components/forms/ready-beep.d.ts.map +1 -0
  155. package/dist/components/forms/ready-beep.js +82 -0
  156. package/dist/components/layout/PageHeader.svelte +151 -17
  157. package/dist/components/layout/PageHeader.svelte.d.ts +47 -7
  158. package/dist/components/layout/PageHeader.svelte.d.ts.map +1 -1
  159. package/dist/components/layout/__tests__/PageHeader.test.js +87 -1
  160. package/dist/components/layout/index.d.ts +1 -0
  161. package/dist/components/layout/index.d.ts.map +1 -1
  162. package/dist/components/layout/index.js +1 -0
  163. package/dist/components/layout/page-header-context.d.ts +25 -0
  164. package/dist/components/layout/page-header-context.d.ts.map +1 -0
  165. package/dist/components/layout/page-header-context.js +16 -0
  166. package/dist/components/nav/Tabs.svelte +241 -0
  167. package/dist/components/nav/Tabs.svelte.d.ts +15 -0
  168. package/dist/components/nav/Tabs.svelte.d.ts.map +1 -1
  169. package/dist/components/nav/__tests__/LinkTabs.test.js +117 -0
  170. package/dist/components/nav/index.d.ts +1 -0
  171. package/dist/components/nav/index.d.ts.map +1 -1
  172. package/dist/components/nav/index.js +1 -0
  173. package/dist/components/nav/tabs-overflow.d.ts +12 -0
  174. package/dist/components/nav/tabs-overflow.d.ts.map +1 -0
  175. package/dist/components/nav/tabs-overflow.js +17 -0
  176. package/dist/components/nav/types.d.ts +8 -0
  177. package/dist/components/nav/types.d.ts.map +1 -1
  178. package/dist/components/ui/Button.svelte +7 -0
  179. package/dist/components/ui/Card.svelte +14 -5
  180. package/dist/components/ui/Dropdown.svelte +55 -1
  181. package/dist/components/ui/Dropdown.svelte.d.ts +8 -0
  182. package/dist/components/ui/Dropdown.svelte.d.ts.map +1 -1
  183. package/dist/components/ui/IconToggle.svelte +139 -0
  184. package/dist/components/ui/IconToggle.svelte.d.ts +32 -0
  185. package/dist/components/ui/IconToggle.svelte.d.ts.map +1 -0
  186. package/dist/components/ui/IconToggleGroup.svelte +56 -0
  187. package/dist/components/ui/IconToggleGroup.svelte.d.ts +21 -0
  188. package/dist/components/ui/IconToggleGroup.svelte.d.ts.map +1 -0
  189. package/dist/components/ui/Pagination.svelte +1 -1
  190. package/dist/components/ui/Popover.svelte +17 -1
  191. package/dist/components/ui/Popover.svelte.d.ts +8 -0
  192. package/dist/components/ui/Popover.svelte.d.ts.map +1 -1
  193. package/dist/components/ui/__tests__/IconToggle.test.js +59 -0
  194. package/dist/components/ui/__tests__/IconToggleGroup.test.js +30 -0
  195. package/dist/components/ui/__tests__/Popover.test.js +44 -0
  196. package/dist/components/ui/__tests__/dropdown-popover-icon.test.js +125 -0
  197. package/dist/components/ui/__tests__/phone-touch-targets.test.js +39 -0
  198. package/dist/components/ui/index.d.ts +2 -0
  199. package/dist/components/ui/index.d.ts.map +1 -1
  200. package/dist/components/ui/index.js +2 -0
  201. package/dist/i18n/strings.ui.d.ts +41 -0
  202. package/dist/i18n/strings.ui.d.ts.map +1 -1
  203. package/dist/i18n/strings.ui.js +46 -0
  204. package/dist/svelte/__tests__/playground.test.js +1 -0
  205. package/dist/svelte/playground/CalendarPreview.svelte +140 -0
  206. package/dist/svelte/playground/CalendarPreview.svelte.d.ts +4 -0
  207. package/dist/svelte/playground/CalendarPreview.svelte.d.ts.map +1 -0
  208. package/dist/svelte/playground.d.ts.map +1 -1
  209. package/dist/svelte/playground.js +10 -0
  210. package/dist/types-generic.d.ts +2 -0
  211. package/dist/types-generic.d.ts.map +1 -1
  212. 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"}
@@ -1259,3 +1259,4 @@ export function createDataSurfaceRegistry() {
1259
1259
  },
1260
1260
  };
1261
1261
  }
1262
+ export * from './surface-navigation.js';
@@ -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"}