@parallelworks/ui 0.76.0 → 0.78.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 (270) hide show
  1. package/README.md +30 -5
  2. package/dist/access/AccessDrawer.d.ts +28 -7
  3. package/dist/access/AccessDrawer.js +216 -35
  4. package/dist/access/AccessList.d.ts +19 -1
  5. package/dist/access/AccessList.js +220 -9
  6. package/dist/access/GrantView.d.ts +3 -1
  7. package/dist/access/GrantView.js +17 -4
  8. package/dist/access/PrincipalPicker.js +5 -5
  9. package/dist/access/focus.js +1 -1
  10. package/dist/access/index.d.ts +2 -2
  11. package/dist/access/index.js +2 -2
  12. package/dist/access/strings.d.ts +18 -0
  13. package/dist/access/strings.js +20 -2
  14. package/dist/access/types.d.ts +26 -0
  15. package/dist/ai/adapter/types.d.ts +1 -0
  16. package/dist/ai/components/AllocationSelector.js +1 -1
  17. package/dist/ai/components/AttachmentManager.js +5 -5
  18. package/dist/ai/components/AttachmentUpload.js +2 -2
  19. package/dist/ai/components/BranchNavigator.js +1 -1
  20. package/dist/ai/components/ChatActivity.d.ts +2 -0
  21. package/dist/ai/components/ChatActivity.js +31 -0
  22. package/dist/ai/components/ChatEmptyState.js +3 -3
  23. package/dist/ai/components/ChatInput.js +3 -5
  24. package/dist/ai/components/ChatLayout.js +1 -1
  25. package/dist/ai/components/ChatMessage.js +1 -1
  26. package/dist/ai/components/ChatMessageList.js +2 -2
  27. package/dist/ai/components/ChatSidebar.js +3 -3
  28. package/dist/ai/components/ChatThread.js +4 -4
  29. package/dist/ai/components/ComposerAddMenu.js +9 -20
  30. package/dist/ai/components/ModelSelector.js +3 -3
  31. package/dist/ai/components/PastedText.js +5 -5
  32. package/dist/ai/components/SessionView.js +5 -2
  33. package/dist/ai/components/ShareDialog.js +3 -3
  34. package/dist/ai/components/SlashMenu.d.ts +31 -4
  35. package/dist/ai/components/SlashMenu.js +36 -63
  36. package/dist/ai/components/agent/ActivityLine.js +1 -1
  37. package/dist/ai/components/agent/AgentMessageParts.d.ts +1 -1
  38. package/dist/ai/components/agent/AgentMessageParts.js +44 -18
  39. package/dist/ai/components/conversationActions.js +3 -3
  40. package/dist/ai/core/ChatProvider.js +11 -2
  41. package/dist/ai/core/config.d.ts +10 -1
  42. package/dist/ai/core/config.js +1 -1
  43. package/dist/ai/index.d.ts +1 -1
  44. package/dist/ai/strings.d.ts +2 -0
  45. package/dist/ai/strings.js +2 -1
  46. package/dist/ai/types.d.ts +3 -0
  47. package/dist/ai/ui/ConversationSidebar.js +3 -3
  48. package/dist/ai/ui/sidebar.js +3 -3
  49. package/dist/assets/yaml.worker-DOBV2lND.js +884 -0
  50. package/dist/components/ActionMenu.d.ts +38 -9
  51. package/dist/components/ActionMenu.js +48 -22
  52. package/dist/components/Avatar.d.ts +66 -6
  53. package/dist/components/Avatar.js +130 -18
  54. package/dist/components/Breadcrumbs.d.ts +4 -0
  55. package/dist/components/Breadcrumbs.js +26 -10
  56. package/dist/components/Button.d.ts +3 -2
  57. package/dist/components/Button.js +11 -1
  58. package/dist/components/Callout.d.ts +3 -2
  59. package/dist/components/Callout.js +3 -2
  60. package/dist/components/Card.d.ts +33 -9
  61. package/dist/components/Card.js +24 -8
  62. package/dist/components/Checkbox.d.ts +3 -1
  63. package/dist/components/Checkbox.js +4 -2
  64. package/dist/components/ColorInput.js +1 -1
  65. package/dist/components/Combobox.d.ts +23 -0
  66. package/dist/components/Combobox.js +252 -86
  67. package/dist/components/ConfirmModal.d.ts +26 -2
  68. package/dist/components/ConfirmModal.js +81 -15
  69. package/dist/components/ContextMenu.d.ts +29 -2
  70. package/dist/components/ContextMenu.js +60 -8
  71. package/dist/components/CopyButton.d.ts +21 -8
  72. package/dist/components/CopyButton.js +29 -9
  73. package/dist/components/CreateModal.d.ts +16 -4
  74. package/dist/components/CreateModal.js +27 -16
  75. package/dist/components/DatePicker.d.ts +31 -1
  76. package/dist/components/DatePicker.js +178 -47
  77. package/dist/components/DescriptionList.d.ts +13 -3
  78. package/dist/components/DescriptionList.js +43 -11
  79. package/dist/components/Disclosure.d.ts +10 -2
  80. package/dist/components/Disclosure.js +18 -7
  81. package/dist/components/Drawer.js +5 -5
  82. package/dist/components/EmptyState.d.ts +12 -4
  83. package/dist/components/EmptyState.js +18 -10
  84. package/dist/components/ErrorPage.d.ts +40 -0
  85. package/dist/components/ErrorPage.js +101 -0
  86. package/dist/components/ExternalLink.d.ts +4 -1
  87. package/dist/components/ExternalLink.js +4 -5
  88. package/dist/components/Field.d.ts +21 -6
  89. package/dist/components/Field.js +32 -16
  90. package/dist/components/FileButton.d.ts +19 -0
  91. package/dist/components/FileButton.js +32 -0
  92. package/dist/components/HoverCard.d.ts +26 -6
  93. package/dist/components/HoverCard.js +11 -5
  94. package/dist/components/IconButton.js +4 -2
  95. package/dist/components/InfoTip.js +3 -3
  96. package/dist/components/Input.d.ts +33 -3
  97. package/dist/components/Input.js +149 -14
  98. package/dist/components/Kbd.d.ts +40 -0
  99. package/dist/components/Kbd.js +168 -0
  100. package/dist/components/Listbox.d.ts +103 -0
  101. package/dist/components/Listbox.js +190 -0
  102. package/dist/components/Loader.d.ts +8 -2
  103. package/dist/components/Loader.js +16 -1
  104. package/dist/components/Markdown.js +1 -1
  105. package/dist/components/MarkdownImpl.js +1 -1
  106. package/dist/components/Menu.d.ts +20 -2
  107. package/dist/components/Menu.js +48 -19
  108. package/dist/components/MenuEntries.d.ts +38 -2
  109. package/dist/components/MenuEntries.js +266 -39
  110. package/dist/components/Meter.d.ts +50 -11
  111. package/dist/components/Meter.js +71 -21
  112. package/dist/components/Modal.d.ts +9 -3
  113. package/dist/components/Modal.js +68 -29
  114. package/dist/components/OneTimeCodeInput.js +1 -1
  115. package/dist/components/PageHeader.js +111 -7
  116. package/dist/components/PageHeaderAction.d.ts +8 -3
  117. package/dist/components/PageHeaderAction.js +122 -34
  118. package/dist/components/Pagination.d.ts +13 -1
  119. package/dist/components/Pagination.js +90 -19
  120. package/dist/components/Popover.d.ts +13 -2
  121. package/dist/components/Popover.js +20 -21
  122. package/dist/components/Provider.d.ts +140 -3
  123. package/dist/components/Provider.js +142 -12
  124. package/dist/components/RefreshButton.d.ts +13 -6
  125. package/dist/components/RefreshButton.js +17 -9
  126. package/dist/components/RunStatusIcon.d.ts +16 -7
  127. package/dist/components/RunStatusIcon.js +21 -8
  128. package/dist/components/Section.d.ts +11 -4
  129. package/dist/components/Section.js +33 -22
  130. package/dist/components/SegmentedControl.d.ts +16 -3
  131. package/dist/components/SegmentedControl.js +7 -3
  132. package/dist/components/Select.d.ts +19 -3
  133. package/dist/components/Select.js +114 -68
  134. package/dist/components/SelectionCopy.d.ts +10 -0
  135. package/dist/components/SelectionCopy.js +105 -0
  136. package/dist/components/SettingModeOption.js +1 -1
  137. package/dist/components/SettingsCard.js +1 -1
  138. package/dist/components/Skeleton.js +1 -1
  139. package/dist/components/StatusBadge.d.ts +55 -10
  140. package/dist/components/StatusBadge.js +91 -15
  141. package/dist/components/StickyBar.js +1 -1
  142. package/dist/components/Table.d.ts +22 -4
  143. package/dist/components/Table.js +46 -24
  144. package/dist/components/Tabs.d.ts +9 -4
  145. package/dist/components/Tabs.js +9 -4
  146. package/dist/components/Tag.d.ts +49 -0
  147. package/dist/components/Tag.js +199 -0
  148. package/dist/components/ThemeEditor.js +1 -1
  149. package/dist/components/Toast.js +1 -1
  150. package/dist/components/ToggleGroup.d.ts +4 -1
  151. package/dist/components/ToggleGroup.js +30 -13
  152. package/dist/components/Tooltip.d.ts +9 -3
  153. package/dist/components/Tooltip.js +55 -39
  154. package/dist/components/buttonClasses.d.ts +5 -2
  155. package/dist/components/buttonClasses.js +11 -10
  156. package/dist/components/chart.d.ts +69 -0
  157. package/dist/components/chart.js +145 -0
  158. package/dist/components/comboboxItems.d.ts +7 -1
  159. package/dist/components/comboboxItems.js +6 -2
  160. package/dist/components/dismiss.d.ts +5 -1
  161. package/dist/components/dismiss.js +6 -2
  162. package/dist/components/floating.d.ts +4 -4
  163. package/dist/components/floating.js +5 -5
  164. package/dist/components/headerActionsFit.d.ts +37 -0
  165. package/dist/components/headerActionsFit.js +81 -0
  166. package/dist/components/pageHeaderStore.d.ts +3 -0
  167. package/dist/components/pageHeaderStore.js +3 -0
  168. package/dist/components/pathBreaks.js +2 -2
  169. package/dist/components/popupContainer.d.ts +17 -0
  170. package/dist/components/popupContainer.js +69 -0
  171. package/dist/components/relativeTime.d.ts +21 -10
  172. package/dist/components/relativeTime.js +50 -22
  173. package/dist/components/segmented.d.ts +18 -4
  174. package/dist/components/segmented.js +48 -6
  175. package/dist/components/typedDate.d.ts +5 -0
  176. package/dist/components/typedDate.js +50 -0
  177. package/dist/components/useClosingValue.d.ts +4 -0
  178. package/dist/components/useClosingValue.js +12 -0
  179. package/dist/components/useCopy.d.ts +6 -0
  180. package/dist/components/useCopy.js +3 -3
  181. package/dist/components/useNarrow.d.ts +2 -0
  182. package/dist/components/useNarrow.js +16 -0
  183. package/dist/editor/Editor.js +1 -1
  184. package/dist/editor/FormCodePanel.js +1 -1
  185. package/dist/editor/Monaco.js +1 -1
  186. package/dist/editor/workers.js +1 -1
  187. package/dist/file-explorer/FileExplorer.js +16 -15
  188. package/dist/file-explorer/FileExplorerContext.js +2 -2
  189. package/dist/file-explorer/FilePreview/Viewer.js +3 -3
  190. package/dist/file-explorer/FilePreview/index.js +2 -2
  191. package/dist/file-explorer/FileRowsWindow.js +3 -3
  192. package/dist/file-explorer/FileTable.js +1 -1
  193. package/dist/file-explorer/LoadMoreRow.js +1 -1
  194. package/dist/file-explorer/TreeView.js +5 -5
  195. package/dist/file-explorer/helpers.js +2 -2
  196. package/dist/file-explorer/toasts.js +2 -2
  197. package/dist/form/Fields/CheckboxGroupField.js +1 -1
  198. package/dist/form/Fields/DropdownField.js +1 -1
  199. package/dist/form/Fields/DurationField.js +2 -2
  200. package/dist/form/Fields/MultiDropdownField.js +1 -1
  201. package/dist/form/Fields/NumberField.js +1 -1
  202. package/dist/form/Fields/ObjectField.js +1 -1
  203. package/dist/form/Fields/RangeField.js +1 -1
  204. package/dist/form/Fields/StringField.js +1 -1
  205. package/dist/form/Form.js +3 -3
  206. package/dist/form/FormLayout.js +1 -1
  207. package/dist/form/Wizard/WizardContainer.js +1 -1
  208. package/dist/form/Wizard/WizardNavigation.js +1 -1
  209. package/dist/form/Wizard/WizardStepContent.js +1 -1
  210. package/dist/form/fieldRegistry.js +1 -1
  211. package/dist/form/formEditing.js +1 -1
  212. package/dist/format.d.ts +17 -7
  213. package/dist/format.js +39 -16
  214. package/dist/graph/AnnotationBanner.js +2 -2
  215. package/dist/graph/Connectors.js +5 -5
  216. package/dist/graph/DependencyGraph.js +10 -9
  217. package/dist/graph/GraphEditor.js +4 -4
  218. package/dist/graph/GraphEditorDialogs.js +13 -12
  219. package/dist/graph/GraphNode.js +1 -1
  220. package/dist/graph/InputDialog.js +5 -5
  221. package/dist/graph/InputsEditor.js +7 -7
  222. package/dist/graph/MatrixGroup.js +2 -2
  223. package/dist/graph/ProblemsButton.js +3 -3
  224. package/dist/graph/Reveal.js +1 -1
  225. package/dist/graph/RunWarningsList.js +1 -1
  226. package/dist/graph/ShortcutsButton.js +25 -8
  227. package/dist/graph/StepSummaries.js +1 -1
  228. package/dist/graph/TreeView.js +2 -2
  229. package/dist/graph/WorkflowSettingsDialog.js +3 -3
  230. package/dist/graph/editorFields.js +3 -3
  231. package/dist/graph/editorPrimitives.js +1 -1
  232. package/dist/graph/editorRows.js +5 -5
  233. package/dist/graph/fieldProblems.js +2 -2
  234. package/dist/graph/inputRefs.js +7 -6
  235. package/dist/graph/stepWith.js +2 -2
  236. package/dist/index.d.ts +30 -21
  237. package/dist/index.js +20 -12
  238. package/dist/list/ActionToolbar.d.ts +4 -0
  239. package/dist/list/ActionToolbar.js +35 -12
  240. package/dist/list/ListFilterBar.d.ts +42 -0
  241. package/dist/list/ListFilterBar.js +789 -0
  242. package/dist/list/ListPage.d.ts +42 -4
  243. package/dist/list/ListPage.js +56 -22
  244. package/dist/list/ListTable.d.ts +27 -3
  245. package/dist/list/ListTable.js +48 -23
  246. package/dist/list/ListToolbar.d.ts +31 -0
  247. package/dist/list/ListToolbar.js +43 -0
  248. package/dist/list/ListViewControls.d.ts +8 -4
  249. package/dist/list/ListViewControls.js +22 -14
  250. package/dist/list/UserHoverCard.d.ts +8 -2
  251. package/dist/list/UserHoverCard.js +26 -13
  252. package/dist/list/filterModel.d.ts +122 -0
  253. package/dist/list/filterModel.js +308 -0
  254. package/dist/list/index.d.ts +6 -2
  255. package/dist/list/index.js +6 -2
  256. package/dist/list/listSearch.d.ts +1 -1
  257. package/dist/list/listSearch.js +8 -4
  258. package/dist/list/listView.d.ts +24 -2
  259. package/dist/list/listView.js +47 -45
  260. package/dist/list/rowMenu.d.ts +10 -5
  261. package/dist/list/rowMenu.js +17 -7
  262. package/dist/list/useListFilters.d.ts +68 -0
  263. package/dist/list/useListFilters.js +252 -0
  264. package/dist/logviewer/LogViewer.js +4 -4
  265. package/dist/styles/theme.css +94 -0
  266. package/dist/styles.css +1 -1
  267. package/package.json +11 -11
  268. package/dist/assets/yaml.worker-k9AdOqzf.js +0 -1441
  269. package/dist/components/fullscreen.d.ts +0 -4
  270. package/dist/components/fullscreen.js +0 -18
@@ -1,5 +1,7 @@
1
1
  import { cx } from "../cx.js";
2
+ import { CheckIcon } from "../icons.js";
2
3
  import { useLink, useStrings } from "../components/Provider.js";
4
+ import { useCopy } from "../components/useCopy.js";
3
5
  import { Avatar } from "../components/Avatar.js";
4
6
  import { LoadingRegion, Skeleton } from "../components/Skeleton.js";
5
7
  import { CopyButton } from "../components/CopyButton.js";
@@ -68,21 +70,32 @@ function UserHoverCard({ username, name, avatarSrc, badge, href, linkComponent,
68
70
  var EMAIL = /^[^\s@]+@[^\s@]+$/;
69
71
  var rowClasses = "flex gap-2 text-(--theme-muted-text-color)";
70
72
  /** One detail line of a hover card: a leading icon and its content. With
71
- * `onClick` the line is a button. */
72
- function HoverCardRow({ icon, children, title, align = "center", truncate = false, onClick }) {
73
- const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
74
- "aria-hidden": "true",
75
- className: cx("flex shrink-0 items-center [&>svg]:h-3.5 [&>svg]:w-3.5", align === "start" && "mt-0.5"),
76
- children: icon
77
- }), /* @__PURE__ */ jsx("span", {
78
- className: cx("min-w-0", truncate && "truncate"),
79
- children
80
- })] });
73
+ * `onClick` the line is a button; with `copy` it is a button that copies a value,
74
+ * such as the email address it shows, its icon turning into a check for a moment. */
75
+ function HoverCardRow({ icon, children, title, align = "center", truncate = false, onClick, copy }) {
76
+ const { common: t } = useStrings();
77
+ const { copy: write, copied } = useCopy();
78
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
79
+ /* @__PURE__ */ jsx("span", {
80
+ "aria-hidden": "true",
81
+ className: cx("flex shrink-0 items-center [&>svg]:h-3.5 [&>svg]:w-3.5", align === "start" && "mt-0.5"),
82
+ children: copied ? /* @__PURE__ */ jsx(CheckIcon, { className: "text-(--theme-success)" }) : icon
83
+ }),
84
+ /* @__PURE__ */ jsx("span", {
85
+ className: cx("min-w-0", truncate && "truncate"),
86
+ children
87
+ }),
88
+ copy && /* @__PURE__ */ jsxs("span", {
89
+ className: "sr-only",
90
+ children: [". ", t.copyThing(copy.label)]
91
+ })
92
+ ] });
81
93
  const alignClass = align === "start" ? "items-start" : "items-center";
82
- if (onClick) return /* @__PURE__ */ jsx("button", {
94
+ const press = copy ? () => void write(copy.text, { label: copy.label }) : onClick;
95
+ if (press) return /* @__PURE__ */ jsx("button", {
83
96
  type: "button",
84
- title,
85
- onClick,
97
+ title: title ?? (copy ? t.copyThing(copy.label) : void 0),
98
+ onClick: press,
86
99
  className: cx(rowClasses, alignClass, "-mx-1 max-w-full rounded px-1 py-0.5 text-left transition-colors hover:bg-(--theme-muted-panel-bg) hover:text-(--theme-app)"),
87
100
  children: content
88
101
  });
@@ -0,0 +1,122 @@
1
+ import type { ReactNode } from 'react';
2
+ /** What a filter's value is, which decides its picker and its conditions. */
3
+ export type FilterKind = 'enum' | 'people' | 'date' | 'text' | 'boolean';
4
+ /** How a filter compares its values with a row's. */
5
+ export type FilterOperator =
6
+ /** enum and people: "is A" or "is any of A, B"; date: a preset or a day ("is Last 7 days"). */
7
+ 'is'
8
+ /** enum and people: "is not A" or "is none of A, B". */
9
+ | 'isNot'
10
+ /** enum on a field holding many values, such as labels: "includes all of A, B". */
11
+ | 'includesAll'
12
+ /** date: on or after the day. */
13
+ | 'after'
14
+ /** date: on or before the day. */
15
+ | 'before'
16
+ /** date: two days, both included. */
17
+ | 'between' | 'contains' | 'notContains'
18
+ /** Any kind: the row has a value at all ("has a due date"), with no values of its own. */
19
+ | 'isSet' | 'isNotSet';
20
+ export interface FilterOption {
21
+ value: string;
22
+ label: string;
23
+ /** A status dot, a label's color, an avatar. */
24
+ icon?: ReactNode;
25
+ /** A second line in the picker, such as a cycle's dates. */
26
+ description?: string | undefined;
27
+ /** How many rows hold it, shown at the end of its row in the picker. */
28
+ count?: number | undefined;
29
+ /** A heading the option sits under in the picker, such as People or Agents. */
30
+ group?: string | undefined;
31
+ }
32
+ /** A value a row holds for a field, for the matching a field does on its own. A date is a
33
+ * `Date`, an ISO string or epoch milliseconds. */
34
+ export type FilterRowValue = string | number | boolean | Date | readonly string[] | null | undefined;
35
+ export interface FilterField<T = never> {
36
+ /** The URL and storage key: stable, never renamed. */
37
+ key: string;
38
+ /** The chip's field part, such as "Status". */
39
+ label: string;
40
+ icon?: ReactNode;
41
+ kind: FilterKind;
42
+ /** The conditions offered, in order; the first is the one a new filter starts with. Defaults:
43
+ * enum and people `is`, `isNot`; date `is`, `after`, `before`, `between`; text `contains`,
44
+ * `notContains`; boolean `is`. Add `includesAll` or `isSet` here. */
45
+ operators?: readonly FilterOperator[] | undefined;
46
+ /** enum and people: the choices. */
47
+ options?: readonly FilterOption[] | undefined;
48
+ /** enum and people: searches on the server as the reader types. Its results show as given. */
49
+ loadOptions?: ((query: string, signal: AbortSignal) => Promise<readonly FilterOption[]>) | undefined;
50
+ /** Names a value no option holds any more, such as a deleted label in a saved link. */
51
+ valueLabel?: ((value: string) => string | undefined) | undefined;
52
+ /** enum and people: whether more than one value can be chosen (default true). */
53
+ multiple?: boolean | undefined;
54
+ /** Whether the picker has a search field. Default: more than eight options, or `loadOptions`. */
55
+ searchable?: boolean | undefined;
56
+ /** Always shows its chip, with no remove button, reading "Any" while unset; Clear leaves it. */
57
+ required?: boolean | undefined;
58
+ /** Always shows its chip, reading "Any" while unset; its remove button only empties it. */
59
+ pinned?: boolean | undefined;
60
+ /** One value across every page sharing the list's `sharedFiltersKey`, such as a namespace. */
61
+ shared?: boolean | undefined;
62
+ /** The row's value, so the list can match the filter itself for every condition. */
63
+ value?: ((row: T) => FilterRowValue) | undefined;
64
+ /** Whether a row passes, for matching `value` can't express. Omit both for a list filtered
65
+ * on the server. */
66
+ matches?: ((row: T, filter: ActiveFilter) => boolean) | undefined;
67
+ }
68
+ /** One filter in effect: plain data, so it can go in a URL, storage or a saved view. */
69
+ export interface ActiveFilter {
70
+ field: string;
71
+ op: FilterOperator;
72
+ /** Dates are `yyyy-MM-dd` days or relative presets (`today`, `-7d`, `month`), which stay
73
+ * relative wherever they are saved. A boolean is `true` or `false`. */
74
+ values: readonly string[];
75
+ }
76
+ /** The relative date presets the date picker offers, as a filter stores them. */
77
+ export declare const DATE_PRESETS: readonly ['today', '-7d', '-30d', '-90d', 'month'];
78
+ export type DatePreset = (typeof DATE_PRESETS)[number];
79
+ type AnyField = FilterField<any>;
80
+ export declare function fieldOperators(field: AnyField): readonly FilterOperator[];
81
+ export declare function isMultiple(field: AnyField): boolean;
82
+ /** Whether a filter narrows anything yet: a value chosen, or a condition that needs none. */
83
+ export declare function isComplete(filter: ActiveFilter): boolean;
84
+ /** Whether two sets of filters say the same, ignoring order, as Save or Update buttons ask. */
85
+ export declare function sameFilters(a: readonly ActiveFilter[], b: readonly ActiveFilter[]): boolean;
86
+ /** What storage holds per field: a plain list for `is`, the shape every earlier version wrote
87
+ * and still reads, or the condition beside its values. Key order keeps the order added. */
88
+ export type StoredFilters = Record<string, readonly string[] | {
89
+ op: FilterOperator;
90
+ values: readonly string[];
91
+ }>;
92
+ export declare function fromStored(stored: unknown): ActiveFilter[];
93
+ export declare function toStored(filters: readonly ActiveFilter[]): StoredFilters;
94
+ /** The URL params a set of filters reads as: `status=started,done&label!=bug&due>=2026-10-01`.
95
+ * Values are not URL-encoded here; the router or `URLSearchParams` does that. */
96
+ export declare function encodeFilters(filters: readonly ActiveFilter[]): Record<string, string>;
97
+ type SearchInput = URLSearchParams | Record<string, unknown>;
98
+ /** The filters a URL's params name, for the given fields; `null` when it names none, so the
99
+ * remembered filters apply. A plain `key=value` is `is`, so links made before conditions
100
+ * existed keep working. Params of no field are left for the host. */
101
+ export declare function decodeFilters(search: SearchInput, fields: readonly AnyField[]): ActiveFilter[] | null;
102
+ /** Every param a field could use, so a host can drop stale ones before writing new. */
103
+ export declare function filterParamKeys(fields: readonly AnyField[]): string[];
104
+ /** A URL's params with the fields' params replaced by these filters, other params kept. */
105
+ export declare function withFilterParams<S extends Record<string, unknown>>(search: S, filters: readonly ActiveFilter[], fields: readonly AnyField[]): Record<string, unknown>;
106
+ /** A local day as `yyyy-MM-dd`. */
107
+ export declare const toDay: (date: Date) => string;
108
+ /** The local day a `yyyy-MM-dd` value names. */
109
+ export declare function parseDay(value: string | undefined): Date | undefined;
110
+ export declare function isDatePreset(value: string | undefined): value is DatePreset;
111
+ /** The span a date filter covers right now: `from` included, `to` excluded (the start of the
112
+ * day after the last), either open. Relative presets resolve against `now`. */
113
+ export declare function dateFilterRange(filter: ActiveFilter, now?: Date): {
114
+ from?: Date;
115
+ to?: Date;
116
+ } | undefined;
117
+ /** Whether a row passes one filter, through the field's `matches` or its `value`. A field
118
+ * with neither passes every row: the host filters it on the server. */
119
+ export declare function matchFilter<T>(field: FilterField<T>, filter: ActiveFilter, row: T, now?: Date): boolean;
120
+ /** Rows passing every complete filter. */
121
+ export declare function applyFilterList<T>(rows: readonly T[], filters: readonly ActiveFilter[], fields: readonly FilterField<T>[], now?: Date): T[];
122
+ export {};
@@ -0,0 +1,308 @@
1
+ //#region src/list/filterModel.ts
2
+ /** The relative date presets the date picker offers, as a filter stores them. */
3
+ var DATE_PRESETS = [
4
+ "today",
5
+ "-7d",
6
+ "-30d",
7
+ "-90d",
8
+ "month"
9
+ ];
10
+ var DEFAULT_OPERATORS = {
11
+ enum: ["is", "isNot"],
12
+ people: ["is", "isNot"],
13
+ date: [
14
+ "is",
15
+ "after",
16
+ "before",
17
+ "between"
18
+ ],
19
+ text: ["contains", "notContains"],
20
+ boolean: ["is"]
21
+ };
22
+ function fieldOperators(field) {
23
+ return field.operators?.length ? field.operators : DEFAULT_OPERATORS[field.kind];
24
+ }
25
+ function isMultiple(field) {
26
+ return (field.kind === "enum" || field.kind === "people") && field.multiple !== false;
27
+ }
28
+ /** Whether a filter narrows anything yet: a value chosen, or a condition that needs none. */
29
+ function isComplete(filter) {
30
+ if (filter.op === "isSet" || filter.op === "isNotSet") return true;
31
+ if (filter.op === "between") return filter.values.length === 2 && filter.values.every(Boolean);
32
+ return filter.values.some(Boolean);
33
+ }
34
+ /** Whether two sets of filters say the same, ignoring order, as Save or Update buttons ask. */
35
+ function sameFilters(a, b) {
36
+ const norm = (list) => list.filter(isComplete).map((f) => `${f.field}\u0000${f.op}\u0000${[...f.values].sort().join("")}`).sort().join("");
37
+ return norm(a) === norm(b);
38
+ }
39
+ function fromStored(stored) {
40
+ if (typeof stored !== "object" || stored === null || Array.isArray(stored)) return [];
41
+ const out = [];
42
+ for (const [field, entry] of Object.entries(stored)) if (Array.isArray(entry)) {
43
+ const values = entry.filter((v) => typeof v === "string");
44
+ if (values.length) out.push({
45
+ field,
46
+ op: "is",
47
+ values
48
+ });
49
+ } else if (typeof entry === "object" && entry !== null && typeof entry.op === "string") {
50
+ const values = Array.isArray(entry.values) ? entry.values.filter((v) => typeof v === "string") : [];
51
+ out.push({
52
+ field,
53
+ op: entry.op,
54
+ values
55
+ });
56
+ }
57
+ return out;
58
+ }
59
+ function toStored(filters) {
60
+ const out = {};
61
+ for (const f of filters) out[f.field] = f.op === "is" ? [...f.values] : {
62
+ op: f.op,
63
+ values: [...f.values]
64
+ };
65
+ return out;
66
+ }
67
+ var SUFFIX = {
68
+ is: "",
69
+ isNot: "!",
70
+ includesAll: "*",
71
+ after: ">",
72
+ before: "<",
73
+ between: "",
74
+ contains: "~",
75
+ notContains: "!~",
76
+ isSet: "",
77
+ isNotSet: "!"
78
+ };
79
+ var SET = "_set";
80
+ var escapeValue = (v) => v.replaceAll("%", "%25").replaceAll(",", "%2C");
81
+ var unescapeValue = (v) => v.replaceAll("%2C", ",").replaceAll("%25", "%");
82
+ var joinValues = (values) => values.map(escapeValue).join(",");
83
+ var splitValues = (raw) => raw === "" ? [] : raw.split(",").map(unescapeValue).filter(Boolean);
84
+ /** The URL params a set of filters reads as: `status=started,done&label!=bug&due>=2026-10-01`.
85
+ * Values are not URL-encoded here; the router or `URLSearchParams` does that. */
86
+ function encodeFilters(filters) {
87
+ const out = {};
88
+ for (const f of filters) {
89
+ if (!isComplete(f)) continue;
90
+ if (f.op === "isSet" || f.op === "isNotSet") out[`${f.field}${SUFFIX[f.op]}`] = SET;
91
+ else if (f.op === "between") {
92
+ out[`${f.field}>`] = escapeValue(f.values[0] ?? "");
93
+ out[`${f.field}<`] = escapeValue(f.values[1] ?? "");
94
+ } else out[`${f.field}${SUFFIX[f.op]}`] = joinValues(f.values);
95
+ }
96
+ return out;
97
+ }
98
+ function entriesOf(search) {
99
+ if (search instanceof URLSearchParams) return [...search.entries()];
100
+ const out = [];
101
+ for (const [key, value] of Object.entries(search)) if (typeof value === "string") out.push([key, value]);
102
+ else if (typeof value === "number" || typeof value === "boolean") out.push([key, String(value)]);
103
+ else if (Array.isArray(value)) out.push([key, value.map((v) => escapeValue(String(v))).join(",")]);
104
+ return out;
105
+ }
106
+ /** The filters a URL's params name, for the given fields; `null` when it names none, so the
107
+ * remembered filters apply. A plain `key=value` is `is`, so links made before conditions
108
+ * existed keep working. Params of no field are left for the host. */
109
+ function decodeFilters(search, fields) {
110
+ const byKey = new Map(fields.map((f) => [f.key, f]));
111
+ const out = [];
112
+ const dates = /* @__PURE__ */ new Map();
113
+ let found = false;
114
+ for (const [rawKey, raw] of entriesOf(search)) {
115
+ const suffix = [
116
+ "!~",
117
+ "!",
118
+ "*",
119
+ ">",
120
+ "<",
121
+ "~"
122
+ ].find((s) => rawKey.endsWith(s) && byKey.has(rawKey.slice(0, -s.length)));
123
+ const key = suffix ? rawKey.slice(0, -suffix.length) : rawKey;
124
+ const field = byKey.get(key);
125
+ if (!field) continue;
126
+ found = true;
127
+ if (raw === SET && (suffix === void 0 || suffix === "!")) {
128
+ out.push({
129
+ field: key,
130
+ op: suffix ? "isNotSet" : "isSet",
131
+ values: []
132
+ });
133
+ continue;
134
+ }
135
+ if (suffix === ">" || suffix === "<") {
136
+ const entry = dates.get(key) ?? {};
137
+ entry[suffix === ">" ? "after" : "before"] = unescapeValue(raw);
138
+ if (!dates.has(key)) {
139
+ dates.set(key, entry);
140
+ out.push({
141
+ field: key,
142
+ op: "after",
143
+ values: []
144
+ });
145
+ }
146
+ continue;
147
+ }
148
+ const op = suffix === "!" ? "isNot" : suffix === "*" ? "includesAll" : suffix === "~" ? "contains" : suffix === "!~" ? "notContains" : field.kind === "text" ? "contains" : "is";
149
+ out.push({
150
+ field: key,
151
+ op,
152
+ values: splitValues(raw)
153
+ });
154
+ }
155
+ const resolved = out.map((f) => {
156
+ const range = dates.get(f.field);
157
+ if (!range || f.op !== "after" || f.values.length) return f;
158
+ if (range.after && range.before) return {
159
+ field: f.field,
160
+ op: "between",
161
+ values: [range.after, range.before]
162
+ };
163
+ return range.after ? {
164
+ field: f.field,
165
+ op: "after",
166
+ values: [range.after]
167
+ } : {
168
+ field: f.field,
169
+ op: "before",
170
+ values: [range.before ?? ""]
171
+ };
172
+ });
173
+ return found ? resolved : null;
174
+ }
175
+ /** Every param a field could use, so a host can drop stale ones before writing new. */
176
+ function filterParamKeys(fields) {
177
+ return fields.flatMap((f) => [
178
+ "",
179
+ "!",
180
+ "*",
181
+ ">",
182
+ "<",
183
+ "~",
184
+ "!~"
185
+ ].map((s) => `${f.key}${s}`));
186
+ }
187
+ /** A URL's params with the fields' params replaced by these filters, other params kept. */
188
+ function withFilterParams(search, filters, fields) {
189
+ const next = { ...search };
190
+ for (const key of filterParamKeys(fields)) delete next[key];
191
+ return {
192
+ ...next,
193
+ ...encodeFilters(filters)
194
+ };
195
+ }
196
+ var pad = (n) => String(n).padStart(2, "0");
197
+ /** A local day as `yyyy-MM-dd`. */
198
+ var toDay = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
199
+ /** The local day a `yyyy-MM-dd` value names. */
200
+ function parseDay(value) {
201
+ const match = value?.match(/^(\d{4})-(\d{2})-(\d{2})$/);
202
+ if (!match) return;
203
+ const [, y, m, d] = match.map(Number);
204
+ const date = new Date(y, m - 1, d);
205
+ return Number.isNaN(date.getTime()) ? void 0 : date;
206
+ }
207
+ var startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
208
+ var addDays = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
209
+ function isDatePreset(value) {
210
+ return DATE_PRESETS.includes(value ?? "") || /^-\d+d$/.test(value ?? "");
211
+ }
212
+ /** The days a preset or a day covers, as `[first, last]` local days, both included. */
213
+ function presetDays(value, now) {
214
+ const today = startOfDay(now);
215
+ if (value === "today") return [today, today];
216
+ if (value === "month") return [new Date(today.getFullYear(), today.getMonth(), 1), today];
217
+ const days = value.match(/^-(\d+)d$/);
218
+ if (days) return [addDays(today, 1 - Number(days[1])), today];
219
+ const day = parseDay(value);
220
+ return day ? [day, day] : void 0;
221
+ }
222
+ /** The span a date filter covers right now: `from` included, `to` excluded (the start of the
223
+ * day after the last), either open. Relative presets resolve against `now`. */
224
+ function dateFilterRange(filter, now = /* @__PURE__ */ new Date()) {
225
+ const [first, second] = filter.values;
226
+ const days = (v) => v ? presetDays(v, now) : void 0;
227
+ switch (filter.op) {
228
+ case "is": {
229
+ const span = days(first);
230
+ return span && {
231
+ from: span[0],
232
+ to: addDays(span[1], 1)
233
+ };
234
+ }
235
+ case "after": {
236
+ const span = days(first);
237
+ return span && { from: span[0] };
238
+ }
239
+ case "before": {
240
+ const span = days(first);
241
+ return span && { to: addDays(span[1], 1) };
242
+ }
243
+ case "between": {
244
+ const a = days(first);
245
+ const b = days(second);
246
+ return a && b ? {
247
+ from: a[0],
248
+ to: addDays(b[1], 1)
249
+ } : void 0;
250
+ }
251
+ default: return;
252
+ }
253
+ }
254
+ var isEmpty = (v) => v === null || v === void 0 || v === "" || Array.isArray(v) && v.length === 0;
255
+ var asList = (v) => isEmpty(v) ? [] : Array.isArray(v) ? [...v] : [String(v)];
256
+ function asTime(v) {
257
+ if (v instanceof Date) return v.getTime();
258
+ if (typeof v === "number") return v;
259
+ if (typeof v === "string" && v) {
260
+ const day = parseDay(v);
261
+ const t = day ? day.getTime() : Date.parse(v);
262
+ return Number.isNaN(t) ? void 0 : t;
263
+ }
264
+ }
265
+ /** Whether a row passes one filter, through the field's `matches` or its `value`. A field
266
+ * with neither passes every row: the host filters it on the server. */
267
+ function matchFilter(field, filter, row, now) {
268
+ if (field.matches) return field.matches(row, filter);
269
+ if (!field.value) return true;
270
+ const v = field.value(row);
271
+ switch (filter.op) {
272
+ case "isSet": return !isEmpty(v);
273
+ case "isNotSet": return isEmpty(v);
274
+ case "contains":
275
+ case "notContains": {
276
+ const needle = (filter.values[0] ?? "").toLocaleLowerCase();
277
+ const has = asList(v).some((s) => s.toLocaleLowerCase().includes(needle));
278
+ return filter.op === "contains" ? has : !has;
279
+ }
280
+ case "includesAll": {
281
+ const held = asList(v);
282
+ return filter.values.every((x) => held.includes(x));
283
+ }
284
+ case "isNot": return !asList(v).some((x) => filter.values.includes(x));
285
+ }
286
+ if (field.kind === "date") {
287
+ const range = dateFilterRange(filter, now);
288
+ const t = asTime(v);
289
+ if (!range || t === void 0) return range === void 0;
290
+ return (range.from === void 0 || t >= range.from.getTime()) && (range.to === void 0 || t < range.to.getTime());
291
+ }
292
+ if (field.kind === "boolean") return String(Boolean(v)) === (filter.values[0] ?? "true");
293
+ return asList(v).some((x) => filter.values.includes(x));
294
+ }
295
+ /** Rows passing every complete filter. */
296
+ function applyFilterList(rows, filters, fields, now = /* @__PURE__ */ new Date()) {
297
+ const checks = filters.flatMap((filter) => {
298
+ const field = fields.find((f) => f.key === filter.field);
299
+ return field && isComplete(filter) ? [{
300
+ field,
301
+ filter
302
+ }] : [];
303
+ });
304
+ if (checks.length === 0) return [...rows];
305
+ return rows.filter((row) => checks.every(({ field, filter }) => matchFilter(field, filter, row, now)));
306
+ }
307
+ //#endregion
308
+ export { DATE_PRESETS, applyFilterList, dateFilterRange, decodeFilters, encodeFilters, fieldOperators, filterParamKeys, fromStored, isComplete, isDatePreset, isMultiple, matchFilter, parseDay, sameFilters, toDay, toStored, withFilterParams };
@@ -1,8 +1,12 @@
1
1
  export { ActionToolbar, type ToolbarAction, ToolbarButton, type ToolbarItem, } from './ActionToolbar.js';
2
- export { ListColumns, ListPage, type ListPageProps, ListRow, ListRowActionsProvider, useListNavigate, } from './ListPage.js';
3
- export { actionsColumnWidth, GroupRow, ListActionsHeader, ListError, ListSelectAllHeader, ListSkeleton, listTableFixedProps, listTableProps, NAME_MIN_WIDTH_PX, NameCell, RowSelectCheckbox, } from './ListTable.js';
2
+ export { type ActiveFilter, applyFilterList, DATE_PRESETS, type DatePreset, dateFilterRange, decodeFilters, encodeFilters, type FilterField, type FilterKind, type FilterOperator, type FilterOption, type FilterRowValue, filterParamKeys, matchFilter, sameFilters, withFilterParams, } from './filterModel.js';
3
+ export { AddFilterMenu, type AddFilterMenuProps, FilterChip, ListFilterBar, type ListFilterBarProps, } from './ListFilterBar.js';
4
+ export { ListColumns, ListPage, type ListPageEmpty, type ListPageProps, ListRow, ListRowActionsProvider, useListNavigate, } from './ListPage.js';
5
+ export { actionsColumnWidth, GroupRow, ListActionsHeader, ListCellText, ListError, ListSelectAllHeader, ListSkeleton, listTableFixedProps, listTableProps, NAME_MIN_WIDTH_PX, NameCell, RowSelectCheckbox, } from './ListTable.js';
6
+ export { ListToolbar, type ListToolbarProps } from './ListToolbar.js';
4
7
  export { ListDisplayMenu, ListFilterMenu } from './ListViewControls.js';
5
8
  export { ListCount, type ListSearch, ListSearchControl, useListSearch, } from './listSearch.js';
6
9
  export { type ActionDef, type ColumnDef, DEFAULT_ORDER, FAVORITE_ORDER, type FilterFacet, type ListView, type ListViewChrome, type ListViewConfig, type OrderByOption, useListView, } from './listView.js';
7
10
  export { assembleRowMenu, type CopyFields, type CopySubmenu, MoreButton, type RowAction, rowContextMenuProps, useCopySubmenu, useRowMenuActive, } from './rowMenu.js';
8
11
  export { HoverCardRow, UserHoverCard, type UserHoverCardProps, } from './UserHoverCard.js';
12
+ export { type FilterStore, type ListFilters, type ListFiltersConfig, useListFilters, } from './useListFilters.js';
@@ -1,9 +1,13 @@
1
1
  import { ActionToolbar, ToolbarButton } from "./ActionToolbar.js";
2
- import { GroupRow, ListActionsHeader, ListError, ListSelectAllHeader, ListSkeleton, NAME_MIN_WIDTH_PX, NameCell, RowSelectCheckbox, actionsColumnWidth, listTableFixedProps, listTableProps } from "./ListTable.js";
2
+ import { DATE_PRESETS, applyFilterList, dateFilterRange, decodeFilters, encodeFilters, filterParamKeys, matchFilter, sameFilters, withFilterParams } from "./filterModel.js";
3
+ import { AddFilterMenu, FilterChip, ListFilterBar } from "./ListFilterBar.js";
4
+ import { GroupRow, ListActionsHeader, ListCellText, ListError, ListSelectAllHeader, ListSkeleton, NAME_MIN_WIDTH_PX, NameCell, RowSelectCheckbox, actionsColumnWidth, listTableFixedProps, listTableProps } from "./ListTable.js";
3
5
  import { MoreButton, assembleRowMenu, rowContextMenuProps, useCopySubmenu, useRowMenuActive } from "./rowMenu.js";
4
6
  import { ListColumns, ListPage, ListRow, ListRowActionsProvider, useListNavigate } from "./ListPage.js";
5
7
  import { ListDisplayMenu, ListFilterMenu } from "./ListViewControls.js";
6
8
  import { ListCount, ListSearchControl, useListSearch } from "./listSearch.js";
9
+ import { ListToolbar } from "./ListToolbar.js";
10
+ import { useListFilters } from "./useListFilters.js";
7
11
  import { DEFAULT_ORDER, FAVORITE_ORDER, useListView } from "./listView.js";
8
12
  import { HoverCardRow, UserHoverCard } from "./UserHoverCard.js";
9
- export { ActionToolbar, DEFAULT_ORDER, FAVORITE_ORDER, GroupRow, HoverCardRow, ListActionsHeader, ListColumns, ListCount, ListDisplayMenu, ListError, ListFilterMenu, ListPage, ListRow, ListRowActionsProvider, ListSearchControl, ListSelectAllHeader, ListSkeleton, MoreButton, NAME_MIN_WIDTH_PX, NameCell, RowSelectCheckbox, ToolbarButton, UserHoverCard, actionsColumnWidth, assembleRowMenu, listTableFixedProps, listTableProps, rowContextMenuProps, useCopySubmenu, useListNavigate, useListSearch, useListView, useRowMenuActive };
13
+ export { ActionToolbar, AddFilterMenu, DATE_PRESETS, DEFAULT_ORDER, FAVORITE_ORDER, FilterChip, GroupRow, HoverCardRow, ListActionsHeader, ListCellText, ListColumns, ListCount, ListDisplayMenu, ListError, ListFilterBar, ListFilterMenu, ListPage, ListRow, ListRowActionsProvider, ListSearchControl, ListSelectAllHeader, ListSkeleton, ListToolbar, MoreButton, NAME_MIN_WIDTH_PX, NameCell, RowSelectCheckbox, ToolbarButton, UserHoverCard, actionsColumnWidth, applyFilterList, assembleRowMenu, dateFilterRange, decodeFilters, encodeFilters, filterParamKeys, listTableFixedProps, listTableProps, matchFilter, rowContextMenuProps, sameFilters, useCopySubmenu, useListFilters, useListNavigate, useListSearch, useListView, useRowMenuActive, withFilterParams };
@@ -16,7 +16,7 @@ export type ListSearch = ReturnType<typeof useListSearch>;
16
16
  export declare function ListSearchControl({ search, placeholder, label: labelProp, }: {
17
17
  search: ListSearch;
18
18
  placeholder: string;
19
- label?: string;
19
+ label?: string | undefined;
20
20
  }): import("react").JSX.Element;
21
21
  export declare function ListCount({ total, matches, filterActive, noun, nounPlural, }: {
22
22
  total: number;
@@ -1,13 +1,14 @@
1
1
  import { buttonClasses } from "../components/buttonClasses.js";
2
2
  import { CloseIcon, RemoveIcon, SearchIcon } from "../icons.js";
3
+ import { ariaKeyShortcuts } from "../components/Kbd.js";
3
4
  import { Tooltip } from "../components/Tooltip.js";
4
- import { usePersistedState, useStrings } from "../components/Provider.js";
5
+ import { useLocale, usePersistedState, useStrings } from "../components/Provider.js";
5
6
  import { isTextEntryContext } from "../components/shortcutKey.js";
6
- import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { formatInteger } from "../format.js";
7
8
  import { useCallback, useEffect, useRef, useState } from "react";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
8
10
  //#region src/list/listSearch.tsx
9
11
  var NO_TERMS = [];
10
- var COUNT = new Intl.NumberFormat();
11
12
  /** Pass `enabled: false` to suspend the global ⌘F/`/` hijack (e.g. when search isn't shown and browser Find should stay active). */
12
13
  function useListSearch(enabled = true, storageKey) {
13
14
  const [filter, setFilter] = useState("");
@@ -122,6 +123,7 @@ function ListSearchControl({ search, placeholder, label: labelProp }) {
122
123
  type: "button",
123
124
  onClick: search.openSearch,
124
125
  "aria-label": label,
126
+ "aria-keyshortcuts": `/ ${ariaKeyShortcuts("Mod+F")}`,
125
127
  className: buttonClasses({
126
128
  variant: "outline",
127
129
  size: "sm",
@@ -133,12 +135,14 @@ function ListSearchControl({ search, placeholder, label: labelProp }) {
133
135
  }
134
136
  function ListCount({ total, matches, filterActive, noun, nounPlural }) {
135
137
  const { list: t } = useStrings();
138
+ const locale = useLocale();
139
+ const count = (n) => formatInteger(n, locale);
136
140
  return /* @__PURE__ */ jsx("div", {
137
141
  className: "flex items-center h-8",
138
142
  children: /* @__PURE__ */ jsx("span", {
139
143
  role: "status",
140
144
  className: "text-[13px] text-(--theme-muted-text-color)",
141
- children: filterActive ? t.matchCount(COUNT.format(matches), COUNT.format(total), nounPlural) : t.count(COUNT.format(total), total === 1 ? noun : nounPlural)
145
+ children: filterActive ? t.matchCount(count(matches), count(total), nounPlural) : t.count(count(total), total === 1 ? noun : nounPlural)
142
146
  })
143
147
  });
144
148
  }
@@ -1,4 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
+ import type { ActiveFilter, FilterField } from './filterModel.js';
3
+ import { type FilterStore, type ListFilters } from './useListFilters.js';
2
4
  /** A toggleable column. Rendering stays in the page's row component, gated by
3
5
  * {@link ListView.isVisible}; this is just the header metadata + visibility. */
4
6
  export interface ColumnDef {
@@ -23,6 +25,9 @@ export interface ColumnDef {
23
25
  * then sorts the list: it shows, and sets, the same order as Display
24
26
  * options' Ordering. */
25
27
  orderBy?: string;
28
+ /** `end` sets the header and the cells against the column's far edge, for
29
+ * figures, sizes and times that compare best lined up on their last digit. */
30
+ align?: 'start' | 'end';
26
31
  }
27
32
  /** Column responsive importance; `low` drops before `medium`. See {@link ColumnDef.priority}. */
28
33
  type ColumnPriority = 'low' | 'medium';
@@ -37,7 +42,9 @@ export interface OrderByOption<T> {
37
42
  compare: (a: T, b: T) => number;
38
43
  }
39
44
  type OrderDir = 'asc' | 'desc';
40
- /** matches decides whether a row passes given the user's selected values for this facet. */
45
+ /** A multi-choice filter: shorthand for an `enum` {@link FilterField} offering "is" and "is not".
46
+ * `matches` decides whether a row passes given the values chosen ("is not" passes the rows it
47
+ * turns away). */
41
48
  export interface FilterFacet<T> {
42
49
  key: string;
43
50
  label: string;
@@ -66,6 +73,16 @@ export interface ListViewConfig<T> {
66
73
  orderBys?: OrderByOption<T>[];
67
74
  defaultOrderBy?: string;
68
75
  facets?: FilterFacet<T>[];
76
+ /** Filters beyond facets: people, dates, text, yes-or-no, with conditions. They follow the
77
+ * facets in the Filter menu. */
78
+ filterFields?: FilterField<T>[] | undefined;
79
+ /** What the page shows before the reader changes anything; Reset returns to them. */
80
+ defaultFilters?: ActiveFilter[] | undefined;
81
+ /** Filters held outside the list too, such as the URL: its filters win while it has any, and
82
+ * changes go to both. See `decodeFilters` and `encodeFilters`. */
83
+ filterStore?: FilterStore | undefined;
84
+ /** False leaves remembering the filters to `filterStore` alone. */
85
+ rememberFilters?: boolean | undefined;
69
86
  actions?: ActionDef[];
70
87
  /** Explicit pinned-button count when not using actions+isPinned; sizes the actions column. */
71
88
  actionCount?: number;
@@ -96,6 +113,8 @@ export interface ListViewChrome {
96
113
  /** Per visible column, whether it stacks under the first on a narrow
97
114
  * container ({@link ColumnDef.stack}); `ListRow` reads entry `i`. */
98
115
  columnStacks: boolean[];
116
+ /** The list's filters, which `ListPage` shows as a row of chips under its toolbar. */
117
+ filterState?: ListFilters | undefined;
99
118
  }
100
119
  export interface ListView<T> extends ListViewChrome {
101
120
  columns: ColumnDef[];
@@ -122,8 +141,11 @@ export interface ListView<T> extends ListViewChrome {
122
141
  sortBy: (value: string) => void;
123
142
  applyOrder: (rows: T[]) => T[];
124
143
  facets: FilterFacet<T>[];
144
+ filterState: ListFilters;
145
+ /** The values of each filter that reads "is"; `filterState.active` has every filter. */
125
146
  filters: Record<string, string[]>;
126
147
  setFacetValues: (key: string, values: string[]) => void;
148
+ /** Empties every filter but the required ones. */
127
149
  clearFilters: () => void;
128
150
  activeFilterCount: number;
129
151
  applyFilters: (rows: T[]) => T[];
@@ -145,5 +167,5 @@ export interface ListView<T> extends ListViewChrome {
145
167
  }
146
168
  /** Persisted grouping + column-visibility + field-filter state for a list page,
147
169
  * driving the shared Display/Filter menus. */
148
- export declare function useListView<T>({ storageKey, sharedFiltersKey, columns, groupBys, defaultGroupBy, orderBys, defaultOrderBy, facets, actions, actionCount, defaultShowColumnHeaders, defaultShowActions, isFavorite, isOn, createdAt, }: ListViewConfig<T>): ListView<T>;
170
+ export declare function useListView<T>({ storageKey, sharedFiltersKey, columns, groupBys, defaultGroupBy, orderBys, defaultOrderBy, facets, filterFields, defaultFilters, filterStore, rememberFilters, actions, actionCount, defaultShowColumnHeaders, defaultShowActions, isFavorite, isOn, createdAt, }: ListViewConfig<T>): ListView<T>;
149
171
  export {};