@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,47 @@
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 { isValidListSort, listSortHref, parseListSort, toggleListSort, } from './list-sort.js';
14
+ export function createUrlListSort(spec, options) {
15
+ const sort = $derived(parseListSort(options.url().searchParams, spec));
16
+ return {
17
+ get sort() {
18
+ return sort;
19
+ },
20
+ spec,
21
+ href: (columnId) => listSortHref(options.url(), toggleListSort(sort, columnId, spec), spec),
22
+ toggle: () => { },
23
+ set: (next) => {
24
+ if (!isValidListSort(spec, next))
25
+ return;
26
+ return options.navigate(listSortHref(options.url(), next, spec));
27
+ },
28
+ };
29
+ }
30
+ export function createLocalListSort(spec, initial) {
31
+ let sort = $state(isValidListSort(spec, initial) ? { ...initial } : { ...spec.default });
32
+ return {
33
+ get sort() {
34
+ return sort;
35
+ },
36
+ spec,
37
+ href: () => undefined,
38
+ toggle: (columnId) => {
39
+ sort = toggleListSort(sort, columnId, spec);
40
+ },
41
+ set: (next) => {
42
+ if (!isValidListSort(spec, next))
43
+ return;
44
+ sort = { columnId: next.columnId, direction: next.direction };
45
+ },
46
+ };
47
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * List sorting — one contract for every list, client or server paginated.
3
+ *
4
+ * - A list declares its sortable columns and a default sort (usually its main
5
+ * date column, newest first).
6
+ * - Clicking a header toggles that column between ascending and descending;
7
+ * a column not yet sorted starts at its `firstDirection` (dates and numbers
8
+ * usually `desc`, text `asc`). Sorting never "clears" back to unsorted: the
9
+ * list always has one well-defined order.
10
+ * - Server-paginated lists keep the sort in the URL (`?sort=<column>&dir=<asc|desc>`)
11
+ * so the database orders the WHOLE result set, not just the current page.
12
+ * The default sort is omitted from the URL, and changing the sort drops the
13
+ * page parameter (back to page 1).
14
+ * - Values are validated against the declared columns, so a URL or an agent
15
+ * command can only pick a declared column and a known direction.
16
+ *
17
+ * Pure functions, no Svelte or DOM: usable in `+page.server.ts` loads,
18
+ * components, and data-surface handlers alike.
19
+ */
20
+ export type ListSortDirection = 'asc' | 'desc';
21
+ export interface ListSort {
22
+ columnId: string;
23
+ direction: ListSortDirection;
24
+ }
25
+ export interface ListSortColumnSpec {
26
+ id: string;
27
+ /** Direction on the first click of this column (default `asc`; use `desc` for dates and counts). */
28
+ firstDirection?: ListSortDirection;
29
+ }
30
+ /** A column offered by `ListSortSelect` (the phone / card-list sort picker). */
31
+ export interface ListSortSelectColumn {
32
+ id: string;
33
+ label: string;
34
+ /** Picks the plain words: dates "newest first", numbers "highest first", text "A–Z". */
35
+ kind?: 'date' | 'number' | 'text';
36
+ }
37
+ export interface ListSortSpec {
38
+ /** Sortable columns. Ids are what the URL and agents use. */
39
+ columns: readonly (ListSortColumnSpec | string)[];
40
+ /** The list's order with no `sort` parameter. Must name a declared column. */
41
+ default: ListSort;
42
+ /** Query parameter names (default `sort` and `dir`). */
43
+ sortParam?: string;
44
+ dirParam?: string;
45
+ /** Page parameter to drop when the sort changes (default `page`; `null` keeps it). */
46
+ pageParam?: string | null;
47
+ }
48
+ /** The ids of the sortable columns. */
49
+ export declare function listSortColumnIds(spec: ListSortSpec): string[];
50
+ /** Whether `sort` names a declared column and a known direction. */
51
+ export declare function isValidListSort(spec: ListSortSpec, sort: Partial<ListSort> | null | undefined): sort is ListSort;
52
+ /**
53
+ * Read the sort from URL parameters. An unknown column falls back to the
54
+ * default; a missing or unknown direction falls back to the column's first
55
+ * direction (or the default's direction for the default column).
56
+ */
57
+ export declare function parseListSort(params: URLSearchParams | {
58
+ get(name: string): string | null;
59
+ }, spec: ListSortSpec): ListSort;
60
+ /** The sort after activating a column's header (toggle; never cleared). */
61
+ export declare function toggleListSort(current: ListSort, columnId: string, spec: ListSortSpec): ListSort;
62
+ /**
63
+ * URL parameters for `sort`: sets (or, for the default, removes) the sort
64
+ * parameters and drops the page parameter when the sort changed.
65
+ */
66
+ export declare function listSortSearchParams(params: URLSearchParams | string, sort: ListSort, spec: ListSortSpec): URLSearchParams;
67
+ /** `?query` string (with the leading `?`, or `''`) for a sort link. */
68
+ export declare function listSortHref(url: URL | {
69
+ pathname: string;
70
+ search: string;
71
+ }, sort: ListSort, spec: ListSortSpec): string;
72
+ /** `aria-sort` for a header cell. */
73
+ export declare function listSortAria(current: ListSort | null | undefined, columnId: string): 'ascending' | 'descending' | 'none';
74
+ /** A plain-language label for the header's action, e.g. "Sort by Date, newest first". */
75
+ export declare function listSortActionLabel(current: ListSort | null | undefined, columnId: string, label: string, spec?: ListSortSpec): string;
76
+ export type ListSortValue = string | number | boolean | Date | null | undefined;
77
+ /**
78
+ * Compare two values for `direction`. Empty values (null, undefined, '',
79
+ * invalid dates) always sort last. Strings compare with `localeCompare`
80
+ * (numeric-aware, case-insensitive).
81
+ */
82
+ export declare function compareListValues(left: ListSortValue, right: ListSortValue, direction: ListSortDirection): number;
83
+ /** A sorted copy of `rows` (stable; empties last). */
84
+ export declare function sortListRows<T>(rows: readonly T[], sort: ListSort | null | undefined, getValue: (row: T, columnId: string) => ListSortValue): T[];
85
+ /**
86
+ * Resolve a sort to an allow-listed ORDER BY term for a server query. `map`
87
+ * names the SQL expression (or ORM field) for each sortable column; the
88
+ * result is only ever built from `map` values, never from the input.
89
+ * Returns e.g. `COALESCE(c.publish_date, c.created_at) DESC NULLS LAST`.
90
+ */
91
+ export declare function listSortOrderBy(sort: ListSort, map: Readonly<Record<string, string>>, options?: {
92
+ nulls?: 'last' | 'first' | false;
93
+ }): string;
94
+ //# sourceMappingURL=list-sort.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-sort.d.ts","sourceRoot":"","sources":["../../../src/components/data/list-sort.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * List sorting — one contract for every list, client or server paginated.
3
+ *
4
+ * - A list declares its sortable columns and a default sort (usually its main
5
+ * date column, newest first).
6
+ * - Clicking a header toggles that column between ascending and descending;
7
+ * a column not yet sorted starts at its `firstDirection` (dates and numbers
8
+ * usually `desc`, text `asc`). Sorting never "clears" back to unsorted: the
9
+ * list always has one well-defined order.
10
+ * - Server-paginated lists keep the sort in the URL (`?sort=<column>&dir=<asc|desc>`)
11
+ * so the database orders the WHOLE result set, not just the current page.
12
+ * The default sort is omitted from the URL, and changing the sort drops the
13
+ * page parameter (back to page 1).
14
+ * - Values are validated against the declared columns, so a URL or an agent
15
+ * command can only pick a declared column and a known direction.
16
+ *
17
+ * Pure functions, no Svelte or DOM: usable in `+page.server.ts` loads,
18
+ * components, and data-surface handlers alike.
19
+ */
20
+ function columnSpecs(spec) {
21
+ return spec.columns.map((column) => typeof column === 'string' ? { id: column } : column);
22
+ }
23
+ function columnSpec(spec, columnId) {
24
+ return columnSpecs(spec).find((column) => column.id === columnId);
25
+ }
26
+ function isDirection(value) {
27
+ return value === 'asc' || value === 'desc';
28
+ }
29
+ /** The ids of the sortable columns. */
30
+ export function listSortColumnIds(spec) {
31
+ return columnSpecs(spec).map((column) => column.id);
32
+ }
33
+ /** Whether `sort` names a declared column and a known direction. */
34
+ export function isValidListSort(spec, sort) {
35
+ return (!!sort &&
36
+ typeof sort.columnId === 'string' &&
37
+ columnSpec(spec, sort.columnId) !== undefined &&
38
+ isDirection(sort.direction));
39
+ }
40
+ /**
41
+ * Read the sort from URL parameters. An unknown column falls back to the
42
+ * default; a missing or unknown direction falls back to the column's first
43
+ * direction (or the default's direction for the default column).
44
+ */
45
+ export function parseListSort(params, spec) {
46
+ const columnId = params.get(spec.sortParam ?? 'sort');
47
+ const direction = params.get(spec.dirParam ?? 'dir');
48
+ const column = columnId ? columnSpec(spec, columnId) : undefined;
49
+ if (!column) {
50
+ return isDirection(direction) && !columnId
51
+ ? { columnId: spec.default.columnId, direction }
52
+ : { ...spec.default };
53
+ }
54
+ if (isDirection(direction))
55
+ return { columnId: column.id, direction };
56
+ return {
57
+ columnId: column.id,
58
+ direction: column.id === spec.default.columnId
59
+ ? spec.default.direction
60
+ : (column.firstDirection ?? 'asc'),
61
+ };
62
+ }
63
+ /** The sort after activating a column's header (toggle; never cleared). */
64
+ export function toggleListSort(current, columnId, spec) {
65
+ const column = columnSpec(spec, columnId);
66
+ if (!column)
67
+ return current;
68
+ if (current.columnId === columnId) {
69
+ return {
70
+ columnId,
71
+ direction: current.direction === 'asc' ? 'desc' : 'asc',
72
+ };
73
+ }
74
+ return { columnId, direction: column.firstDirection ?? 'asc' };
75
+ }
76
+ /**
77
+ * URL parameters for `sort`: sets (or, for the default, removes) the sort
78
+ * parameters and drops the page parameter when the sort changed.
79
+ */
80
+ export function listSortSearchParams(params, sort, spec) {
81
+ const next = new URLSearchParams(params);
82
+ const sortParam = spec.sortParam ?? 'sort';
83
+ const dirParam = spec.dirParam ?? 'dir';
84
+ const previous = parseListSort(next, spec);
85
+ const isDefault = sort.columnId === spec.default.columnId &&
86
+ sort.direction === spec.default.direction;
87
+ if (isDefault) {
88
+ next.delete(sortParam);
89
+ next.delete(dirParam);
90
+ }
91
+ else {
92
+ next.set(sortParam, sort.columnId);
93
+ next.set(dirParam, sort.direction);
94
+ }
95
+ const pageParam = spec.pageParam === undefined ? 'page' : spec.pageParam;
96
+ if (pageParam &&
97
+ (previous.columnId !== sort.columnId ||
98
+ previous.direction !== sort.direction)) {
99
+ next.delete(pageParam);
100
+ }
101
+ return next;
102
+ }
103
+ /** `?query` string (with the leading `?`, or `''`) for a sort link. */
104
+ export function listSortHref(url, sort, spec) {
105
+ const query = listSortSearchParams(url.search, sort, spec).toString();
106
+ return `${url.pathname}${query ? `?${query}` : ''}`;
107
+ }
108
+ /** `aria-sort` for a header cell. */
109
+ export function listSortAria(current, columnId) {
110
+ if (!current || current.columnId !== columnId)
111
+ return 'none';
112
+ return current.direction === 'asc' ? 'ascending' : 'descending';
113
+ }
114
+ /** A plain-language label for the header's action, e.g. "Sort by Date, newest first". */
115
+ export function listSortActionLabel(current, columnId, label, spec) {
116
+ const next = spec
117
+ ? toggleListSort(current ?? spec.default, columnId, spec).direction
118
+ : current?.columnId === columnId && current.direction === 'asc'
119
+ ? 'desc'
120
+ : 'asc';
121
+ return `Sort by ${label}, ${next === 'asc' ? 'ascending' : 'descending'}`;
122
+ }
123
+ function comparable(value) {
124
+ if (value === null || value === undefined || value === '')
125
+ return null;
126
+ if (value instanceof Date) {
127
+ const time = value.getTime();
128
+ return Number.isNaN(time) ? null : time;
129
+ }
130
+ if (typeof value === 'boolean')
131
+ return value ? 1 : 0;
132
+ if (typeof value === 'number')
133
+ return Number.isFinite(value) ? value : null;
134
+ return value;
135
+ }
136
+ /**
137
+ * Compare two values for `direction`. Empty values (null, undefined, '',
138
+ * invalid dates) always sort last. Strings compare with `localeCompare`
139
+ * (numeric-aware, case-insensitive).
140
+ */
141
+ export function compareListValues(left, right, direction) {
142
+ const a = comparable(left);
143
+ const b = comparable(right);
144
+ if (a === null && b === null)
145
+ return 0;
146
+ if (a === null)
147
+ return 1;
148
+ if (b === null)
149
+ return -1;
150
+ const sign = direction === 'asc' ? 1 : -1;
151
+ if (typeof a === 'number' && typeof b === 'number')
152
+ return (a - b) * sign;
153
+ return (String(a).localeCompare(String(b), undefined, {
154
+ numeric: true,
155
+ sensitivity: 'base',
156
+ }) * sign);
157
+ }
158
+ /** A sorted copy of `rows` (stable; empties last). */
159
+ export function sortListRows(rows, sort, getValue) {
160
+ if (!sort)
161
+ return [...rows];
162
+ return rows
163
+ .map((row, index) => ({ row, index }))
164
+ .sort((left, right) => compareListValues(getValue(left.row, sort.columnId), getValue(right.row, sort.columnId), sort.direction) || left.index - right.index)
165
+ .map((entry) => entry.row);
166
+ }
167
+ /**
168
+ * Resolve a sort to an allow-listed ORDER BY term for a server query. `map`
169
+ * names the SQL expression (or ORM field) for each sortable column; the
170
+ * result is only ever built from `map` values, never from the input.
171
+ * Returns e.g. `COALESCE(c.publish_date, c.created_at) DESC NULLS LAST`.
172
+ */
173
+ export function listSortOrderBy(sort, map, options = {}) {
174
+ const expression = Object.hasOwn(map, sort.columnId)
175
+ ? map[sort.columnId]
176
+ : undefined;
177
+ if (!expression) {
178
+ throw new Error(`Unknown sort column: ${sort.columnId}`);
179
+ }
180
+ const nulls = options.nulls === undefined ? 'last' : options.nulls;
181
+ return `${expression} ${sort.direction === 'asc' ? 'ASC' : 'DESC'}${nulls ? ` NULLS ${nulls === 'last' ? 'LAST' : 'FIRST'}` : ''}`;
182
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * A multi-step flow (a wizard, a setup checklist) as a mounted data surface,
3
+ * so an agent can see where a person is and move the flow forward.
4
+ *
5
+ * The published state lists the steps (`state.steps`: id, label, status) and
6
+ * the current one. Three controls move between them:
7
+ *
8
+ * - `next` — advance from the current step. A step whose forward button only
9
+ * checks input and shows the next step runs the page's `next` handler, the
10
+ * same function the button calls. A step whose forward button SAVES or
11
+ * CREATES something (`nextWrites`) is never pressed by an agent: `next`
12
+ * reveals and highlights that button instead, and publishes
13
+ * `awaitingPerson: true` until the page moves on. People apply writes.
14
+ * - `back` — return to the previous step (never a write).
15
+ * - `go-to` — jump to a step the page marks `available` (payload
16
+ * `{ target }`: a step id or label).
17
+ *
18
+ * A handler that returns `false` (validation failed) refuses the command, so
19
+ * the agent sees the step did not change and can read the form's errors.
20
+ */
21
+ import type { DataSurfaceDescriptor, DataSurfaceJsonValue, DataSurfaceRegistry, DataSurfaceSubject } from '@happyvertical/smrt-types';
22
+ export interface StepSurfaceStep {
23
+ id: string;
24
+ /** Plain label a person sees in the progress row. */
25
+ label: string;
26
+ description?: string;
27
+ /** The step's work is done. */
28
+ complete?: boolean;
29
+ /** `go-to` may jump here (default: complete steps and the current one). */
30
+ available?: boolean;
31
+ }
32
+ type StepHandler = () => boolean | undefined | Promise<boolean | undefined>;
33
+ export interface StepSurfaceOptions {
34
+ registry: DataSurfaceRegistry;
35
+ surfaceId: string;
36
+ subject?: DataSurfaceSubject;
37
+ label: string;
38
+ description: string;
39
+ steps: readonly StepSurfaceStep[];
40
+ /** Id of the step on screen. */
41
+ current: string;
42
+ /**
43
+ * The forward action of the current step, when it does not write — the
44
+ * same function its Continue button calls. Return `false` to refuse.
45
+ */
46
+ next?: StepHandler;
47
+ /**
48
+ * The current step's forward button saves or creates something. An agent's
49
+ * `next` then only reveals the button (see {@link StepSurfaceOptions.showNext}).
50
+ */
51
+ nextWrites?: boolean;
52
+ /** Reveal and highlight the current step's forward button for the person. */
53
+ showNext?: () => void;
54
+ /** Return to the previous step. Return `false` to refuse. */
55
+ back?: StepHandler;
56
+ /** Jump to a step. Return `false` to refuse. */
57
+ goTo?: (stepId: string) => boolean | undefined | Promise<boolean | undefined>;
58
+ /** Extra JSON state published alongside the steps. */
59
+ state?: Record<string, DataSurfaceJsonValue>;
60
+ }
61
+ export type StepSurfaceUpdate = Partial<Pick<StepSurfaceOptions, 'steps' | 'current' | 'next' | 'nextWrites' | 'showNext' | 'back' | 'goTo' | 'state'>>;
62
+ export interface StepSurfaceHandle {
63
+ update(next: StepSurfaceUpdate): void;
64
+ destroy(): void;
65
+ }
66
+ /** Resolve a `go-to` payload to a step id (id, exact label, unique prefix). */
67
+ export declare function resolveStepTarget(payload: DataSurfaceJsonValue | undefined, steps: readonly StepSurfaceStep[]): string | null;
68
+ /** The descriptor {@link registerStepSurface} mounts. */
69
+ export declare function stepSurfaceDescriptor(options: Pick<StepSurfaceOptions, 'surfaceId' | 'subject' | 'label' | 'description'>): DataSurfaceDescriptor;
70
+ /** Mount a multi-step flow on a data-surface registry. */
71
+ export declare function registerStepSurface(options: StepSurfaceOptions): StepSurfaceHandle;
72
+ export {};
73
+ //# sourceMappingURL=step-surface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-surface.d.ts","sourceRoot":"","sources":["../../../src/components/data/step-surface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EACV,qBAAqB,EACrB,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,EAEnB,MAAM,2BAA2B,CAAC;AAGnC,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,KAAK,WAAW,GAAG,MAAM,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;AAE5E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAClC,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,gDAAgD;IAChD,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAC9E,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;CAC9C;AAED,MAAM,MAAM,iBAAiB,GAAG,OAAO,CACrC,IAAI,CACF,kBAAkB,EAChB,OAAO,GACP,SAAS,GACT,MAAM,GACN,YAAY,GACZ,UAAU,GACV,MAAM,GACN,MAAM,GACN,OAAO,CACV,CACF,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACtC,OAAO,IAAI,IAAI,CAAC;CACjB;AAUD,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,GAAG,SAAS,EACzC,KAAK,EAAE,SAAS,eAAe,EAAE,GAChC,MAAM,GAAG,IAAI,CAaf;AAED,yDAAyD;AACzD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,IAAI,CACX,kBAAkB,EAClB,WAAW,GAAG,SAAS,GAAG,OAAO,GAAG,aAAa,CAClD,GACA,qBAAqB,CA2CvB;AAED,0DAA0D;AAC1D,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,kBAAkB,GAC1B,iBAAiB,CA6FnB"}
@@ -0,0 +1,177 @@
1
+ /**
2
+ * A multi-step flow (a wizard, a setup checklist) as a mounted data surface,
3
+ * so an agent can see where a person is and move the flow forward.
4
+ *
5
+ * The published state lists the steps (`state.steps`: id, label, status) and
6
+ * the current one. Three controls move between them:
7
+ *
8
+ * - `next` — advance from the current step. A step whose forward button only
9
+ * checks input and shows the next step runs the page's `next` handler, the
10
+ * same function the button calls. A step whose forward button SAVES or
11
+ * CREATES something (`nextWrites`) is never pressed by an agent: `next`
12
+ * reveals and highlights that button instead, and publishes
13
+ * `awaitingPerson: true` until the page moves on. People apply writes.
14
+ * - `back` — return to the previous step (never a write).
15
+ * - `go-to` — jump to a step the page marks `available` (payload
16
+ * `{ target }`: a step id or label).
17
+ *
18
+ * A handler that returns `false` (validation failed) refuses the command, so
19
+ * the agent sees the step did not change and can read the form's errors.
20
+ */
21
+ import { linkTargetFromPayload } from './link-surface.js';
22
+ function normalize(value) {
23
+ return value.trim().toLowerCase().replace(/\s+/g, ' ');
24
+ }
25
+ function stepAvailable(step, current) {
26
+ return step.available ?? (step.complete === true || step.id === current);
27
+ }
28
+ /** Resolve a `go-to` payload to a step id (id, exact label, unique prefix). */
29
+ export function resolveStepTarget(payload, steps) {
30
+ const target = linkTargetFromPayload(payload);
31
+ if (!target)
32
+ return null;
33
+ const wanted = normalize(target);
34
+ const byId = steps.find((step) => normalize(step.id) === wanted);
35
+ if (byId)
36
+ return byId.id;
37
+ const byLabel = steps.filter((step) => normalize(step.label) === wanted);
38
+ if (byLabel.length === 1)
39
+ return byLabel[0].id;
40
+ if (byLabel.length > 1)
41
+ return null;
42
+ const byPrefix = steps.filter((step) => normalize(step.label).startsWith(wanted));
43
+ return byPrefix.length === 1 ? byPrefix[0].id : null;
44
+ }
45
+ /** The descriptor {@link registerStepSurface} mounts. */
46
+ export function stepSurfaceDescriptor(options) {
47
+ const columns = [
48
+ { id: 'id', label: 'ID', capabilities: ['read'], role: 'row-key' },
49
+ { id: 'label', label: 'Step', capabilities: ['read'] },
50
+ { id: 'status', label: 'Status', capabilities: ['read'], role: 'status' },
51
+ { id: 'description', label: 'About', capabilities: ['read'] },
52
+ ];
53
+ return {
54
+ version: 1,
55
+ identity: {
56
+ kind: 'custom',
57
+ surfaceId: options.surfaceId,
58
+ ...(options.subject ? { subject: options.subject } : {}),
59
+ },
60
+ schemaVersion: 1,
61
+ label: options.label,
62
+ description: options.description,
63
+ rowKey: 'id',
64
+ columns,
65
+ query: {
66
+ modes: ['rows'],
67
+ projectableColumnIds: columns.map((column) => column.id),
68
+ filterableColumnIds: [],
69
+ sortableColumnIds: [],
70
+ },
71
+ controls: [
72
+ {
73
+ id: 'next',
74
+ label: 'Next step',
75
+ description: 'Continue from the current step. When the step saves or creates something (state.nextWrites), this only shows the person the button to press.',
76
+ },
77
+ { id: 'back', label: 'Previous step' },
78
+ {
79
+ id: 'go-to',
80
+ label: 'Go to step',
81
+ description: 'Jump to an available step. Payload: { target } — a step id or label.',
82
+ },
83
+ ],
84
+ actions: [],
85
+ limits: { maxQueryRows: 50, maxQueryBytes: 100_000, maxSelectionSize: 1 },
86
+ };
87
+ }
88
+ /** Mount a multi-step flow on a data-surface registry. */
89
+ export function registerStepSurface(options) {
90
+ let current = { ...options };
91
+ let awaitingPerson = false;
92
+ let revision = 1;
93
+ const publishedState = () => {
94
+ const index = current.steps.findIndex((step) => step.id === current.current);
95
+ return {
96
+ ...(current.state ?? {}),
97
+ current: current.current,
98
+ currentLabel: current.steps[index]?.label ?? null,
99
+ stepNumber: index + 1,
100
+ stepCount: current.steps.length,
101
+ nextWrites: current.nextWrites === true,
102
+ awaitingPerson,
103
+ steps: current.steps.map((step) => ({
104
+ id: step.id,
105
+ label: step.label,
106
+ ...(step.description ? { description: step.description } : {}),
107
+ status: step.id === current.current
108
+ ? 'current'
109
+ : step.complete
110
+ ? 'complete'
111
+ : stepAvailable(step, current.current)
112
+ ? 'available'
113
+ : 'upcoming',
114
+ })),
115
+ };
116
+ };
117
+ let signature = JSON.stringify(publishedState());
118
+ const publish = () => {
119
+ const next = JSON.stringify(publishedState());
120
+ if (next !== signature) {
121
+ signature = next;
122
+ revision += 1;
123
+ }
124
+ };
125
+ const run = async (handler) => {
126
+ if (!handler)
127
+ return { ok: false };
128
+ const result = await handler();
129
+ return result === false ? { ok: false } : undefined;
130
+ };
131
+ const dispose = options.registry.register({
132
+ descriptor: stepSurfaceDescriptor(options),
133
+ getSnapshot: () => ({ revision, state: publishedState(), selection: null }),
134
+ execute: async (command) => {
135
+ switch (command.controlId) {
136
+ case 'next': {
137
+ if (current.nextWrites) {
138
+ if (!current.showNext)
139
+ return { ok: false };
140
+ current.showNext();
141
+ awaitingPerson = true;
142
+ publish();
143
+ return undefined;
144
+ }
145
+ return run(current.next);
146
+ }
147
+ case 'back':
148
+ return run(current.back);
149
+ case 'go-to': {
150
+ const stepId = resolveStepTarget(command.payload, current.steps);
151
+ const step = current.steps.find((item) => item.id === stepId);
152
+ if (!step || !current.goTo)
153
+ return { ok: false };
154
+ if (!stepAvailable(step, current.current))
155
+ return { ok: false };
156
+ const goTo = current.goTo;
157
+ return run(() => goTo(step.id));
158
+ }
159
+ default:
160
+ return { ok: false };
161
+ }
162
+ },
163
+ });
164
+ return {
165
+ update(next) {
166
+ const moved = next.current !== undefined && next.current !== current.current;
167
+ current = { ...current, ...next };
168
+ // The person pressed the button (or the flow moved another way).
169
+ if (moved || next.nextWrites === false)
170
+ awaitingPerson = false;
171
+ publish();
172
+ },
173
+ destroy() {
174
+ dispose();
175
+ },
176
+ };
177
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * "Has the page settled after that command?"
3
+ *
4
+ * A visible command on a navigation surface (a link list, a tab row) starts a
5
+ * page change and returns at once: awaiting it inside the command would make
6
+ * the registry report `not_found` whenever the new page unmounts the surface
7
+ * that ran it. The navigation's promise is tracked here instead, keyed by the
8
+ * registry the command ran on, so whoever drives the page next — an in-page
9
+ * assistant about to show the model the page's tools, a test — can wait for
10
+ * it to finish and for the new page's surfaces to register:
11
+ *
12
+ * ```ts
13
+ * await registry.execute(openEventsCommand);
14
+ * await whenSurfaceNavigationSettled(registry); // new page mounted and quiet
15
+ * ```
16
+ *
17
+ * `registerLinkSurface` tracks its own `navigate` result. A bespoke surface
18
+ * whose command navigates calls {@link trackSurfaceNavigation} itself.
19
+ */
20
+ import type { DataSurfaceRegistry } from '@happyvertical/smrt-types';
21
+ /**
22
+ * Record a navigation a command on `registry` started. Returns the same
23
+ * promise. A rejection is swallowed for waiting purposes (a failed navigation
24
+ * is still a finished one); the caller keeps its own handling.
25
+ */
26
+ export declare function trackSurfaceNavigation<T>(registry: DataSurfaceRegistry, navigation: T | Promise<T>): T | Promise<T>;
27
+ /** Whether a tracked navigation on `registry` is still running. */
28
+ export declare function hasPendingSurfaceNavigation(registry: DataSurfaceRegistry): boolean;
29
+ export interface SurfaceNavigationSettleOptions {
30
+ /**
31
+ * How long the registry must go without a surface registering or
32
+ * unregistering before the page counts as settled. Default 120 ms.
33
+ */
34
+ quietMs?: number;
35
+ /** Give up waiting after this long. Default 5000 ms. */
36
+ timeoutMs?: number;
37
+ /** Extra sources whose changes also reset the quiet timer (e.g. page tools). */
38
+ alsoWatch?: ReadonlyArray<{
39
+ subscribe?: (listener: (event: unknown) => void) => () => void;
40
+ } | null | undefined>;
41
+ signal?: AbortSignal;
42
+ }
43
+ /**
44
+ * Resolve once every navigation tracked on `registry` has finished AND the
45
+ * registry (plus `alsoWatch` sources) has been quiet for `quietMs`, or after
46
+ * `timeoutMs`, whichever comes first. Never rejects.
47
+ */
48
+ export declare function whenSurfaceNavigationSettled(registry: DataSurfaceRegistry, options?: SurfaceNavigationSettleOptions): Promise<void>;
49
+ //# sourceMappingURL=surface-navigation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"surface-navigation.d.ts","sourceRoot":"","sources":["../../../src/components/data/surface-navigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAIrE;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EACtC,QAAQ,EAAE,mBAAmB,EAC7B,UAAU,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GACzB,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAiBhB;AAED,mEAAmE;AACnE,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,mBAAmB,GAC5B,OAAO,CAET;AAED,MAAM,WAAW,8BAA8B;IAC7C;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,SAAS,CAAC,EAAE,aAAa,CACrB;QAAE,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,KAAK,MAAM,IAAI,CAAA;KAAE,GAClE,IAAI,GACJ,SAAS,CACZ,CAAC;IACF,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,GAAE,8BAAmC,GAC3C,OAAO,CAAC,IAAI,CAAC,CAiDf"}