@eifi1/ui-kit 0.4.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 (229) hide show
  1. package/README.md +219 -0
  2. package/dist/components/account-settings.d.ts +71 -0
  3. package/dist/components/account-settings.js +159 -0
  4. package/dist/components/account-settings.js.map +1 -0
  5. package/dist/components/alert-banner.d.ts +20 -0
  6. package/dist/components/alert-banner.js +39 -0
  7. package/dist/components/alert-banner.js.map +1 -0
  8. package/dist/components/amount-input.d.ts +117 -0
  9. package/dist/components/amount-input.js +243 -0
  10. package/dist/components/amount-input.js.map +1 -0
  11. package/dist/components/calculator.d.ts +42 -0
  12. package/dist/components/calculator.js +161 -0
  13. package/dist/components/calculator.js.map +1 -0
  14. package/dist/components/chart.d.ts +58 -0
  15. package/dist/components/chart.js +158 -0
  16. package/dist/components/chart.js.map +1 -0
  17. package/dist/components/combobox-core.d.ts +77 -0
  18. package/dist/components/combobox-core.js +263 -0
  19. package/dist/components/combobox-core.js.map +1 -0
  20. package/dist/components/combobox.d.ts +141 -0
  21. package/dist/components/combobox.js +550 -0
  22. package/dist/components/combobox.js.map +1 -0
  23. package/dist/components/currency-select.d.ts +37 -0
  24. package/dist/components/currency-select.js +131 -0
  25. package/dist/components/currency-select.js.map +1 -0
  26. package/dist/components/data-table-filter-popover.d.ts +28 -0
  27. package/dist/components/data-table-filter-popover.js +201 -0
  28. package/dist/components/data-table-filter-popover.js.map +1 -0
  29. package/dist/components/data-table-filters.d.ts +5 -0
  30. package/dist/components/data-table-filters.js +118 -0
  31. package/dist/components/data-table-filters.js.map +1 -0
  32. package/dist/components/data-table-labels.d.ts +33 -0
  33. package/dist/components/data-table-labels.js +53 -0
  34. package/dist/components/data-table-labels.js.map +1 -0
  35. package/dist/components/data-table-pagination.d.ts +18 -0
  36. package/dist/components/data-table-pagination.js +92 -0
  37. package/dist/components/data-table-pagination.js.map +1 -0
  38. package/dist/components/data-table-sort.d.ts +23 -0
  39. package/dist/components/data-table-sort.js +55 -0
  40. package/dist/components/data-table-sort.js.map +1 -0
  41. package/dist/components/data-table.d.ts +5 -0
  42. package/dist/components/data-table.js +926 -0
  43. package/dist/components/data-table.js.map +1 -0
  44. package/dist/components/date-picker.d.ts +85 -0
  45. package/dist/components/date-picker.js +253 -0
  46. package/dist/components/date-picker.js.map +1 -0
  47. package/dist/components/dropdown.d.ts +98 -0
  48. package/dist/components/dropdown.js +122 -0
  49. package/dist/components/dropdown.js.map +1 -0
  50. package/dist/components/entity-combobox.d.ts +49 -0
  51. package/dist/components/entity-combobox.js +115 -0
  52. package/dist/components/entity-combobox.js.map +1 -0
  53. package/dist/components/file-dropzone.d.ts +18 -0
  54. package/dist/components/file-dropzone.js +101 -0
  55. package/dist/components/file-dropzone.js.map +1 -0
  56. package/dist/components/full-bleed-dialog.d.ts +57 -0
  57. package/dist/components/full-bleed-dialog.js +68 -0
  58. package/dist/components/full-bleed-dialog.js.map +1 -0
  59. package/dist/components/grouped-picker.d.ts +37 -0
  60. package/dist/components/grouped-picker.js +87 -0
  61. package/dist/components/grouped-picker.js.map +1 -0
  62. package/dist/components/hover-menu.d.ts +16 -0
  63. package/dist/components/hover-menu.js +113 -0
  64. package/dist/components/hover-menu.js.map +1 -0
  65. package/dist/components/mini-calendar.d.ts +33 -0
  66. package/dist/components/mini-calendar.js +133 -0
  67. package/dist/components/mini-calendar.js.map +1 -0
  68. package/dist/components/modal.d.ts +76 -0
  69. package/dist/components/modal.js +156 -0
  70. package/dist/components/modal.js.map +1 -0
  71. package/dist/components/multi-entity-combobox.d.ts +47 -0
  72. package/dist/components/multi-entity-combobox.js +116 -0
  73. package/dist/components/multi-entity-combobox.js.map +1 -0
  74. package/dist/components/multi-select.d.ts +39 -0
  75. package/dist/components/multi-select.js +132 -0
  76. package/dist/components/multi-select.js.map +1 -0
  77. package/dist/components/number-input.d.ts +74 -0
  78. package/dist/components/number-input.js +114 -0
  79. package/dist/components/number-input.js.map +1 -0
  80. package/dist/components/numpad-sheet.d.ts +45 -0
  81. package/dist/components/numpad-sheet.js +102 -0
  82. package/dist/components/numpad-sheet.js.map +1 -0
  83. package/dist/components/picker-sheet.d.ts +44 -0
  84. package/dist/components/picker-sheet.js +78 -0
  85. package/dist/components/picker-sheet.js.map +1 -0
  86. package/dist/components/popover.d.ts +20 -0
  87. package/dist/components/popover.js +46 -0
  88. package/dist/components/popover.js.map +1 -0
  89. package/dist/components/search-field.d.ts +52 -0
  90. package/dist/components/search-field.js +64 -0
  91. package/dist/components/search-field.js.map +1 -0
  92. package/dist/components/settings-fields.d.ts +39 -0
  93. package/dist/components/settings-fields.js +17 -0
  94. package/dist/components/settings-fields.js.map +1 -0
  95. package/dist/components/swipeable-row.d.ts +60 -0
  96. package/dist/components/swipeable-row.js +115 -0
  97. package/dist/components/swipeable-row.js.map +1 -0
  98. package/dist/components/toggle-group.d.ts +34 -0
  99. package/dist/components/toggle-group.js +90 -0
  100. package/dist/components/toggle-group.js.map +1 -0
  101. package/dist/components/tooltip.d.ts +86 -0
  102. package/dist/components/tooltip.js +171 -0
  103. package/dist/components/tooltip.js.map +1 -0
  104. package/dist/components/ui.d.ts +239 -0
  105. package/dist/components/ui.js +512 -0
  106. package/dist/components/ui.js.map +1 -0
  107. package/dist/components/use-mobile-reveal.d.ts +31 -0
  108. package/dist/components/use-mobile-reveal.js +33 -0
  109. package/dist/components/use-mobile-reveal.js.map +1 -0
  110. package/dist/components/use-table-state.d.ts +54 -0
  111. package/dist/components/use-table-state.js +153 -0
  112. package/dist/components/use-table-state.js.map +1 -0
  113. package/dist/components/user-avatar.d.ts +23 -0
  114. package/dist/components/user-avatar.js +33 -0
  115. package/dist/components/user-avatar.js.map +1 -0
  116. package/dist/components/wizard-stepper.d.ts +14 -0
  117. package/dist/components/wizard-stepper.js +27 -0
  118. package/dist/components/wizard-stepper.js.map +1 -0
  119. package/dist/data-table-filters-aXyf0Xub.d.ts +261 -0
  120. package/dist/feedback/feedback-attachment.d.ts +63 -0
  121. package/dist/feedback/feedback-attachment.js +160 -0
  122. package/dist/feedback/feedback-attachment.js.map +1 -0
  123. package/dist/feedback/feedback-dialog.d.ts +72 -0
  124. package/dist/feedback/feedback-dialog.js +109 -0
  125. package/dist/feedback/feedback-dialog.js.map +1 -0
  126. package/dist/feedback/feedback-inbox.d.ts +226 -0
  127. package/dist/feedback/feedback-inbox.js +315 -0
  128. package/dist/feedback/feedback-inbox.js.map +1 -0
  129. package/dist/hooks/use-anchored-panel.d.ts +80 -0
  130. package/dist/hooks/use-anchored-panel.js +71 -0
  131. package/dist/hooks/use-anchored-panel.js.map +1 -0
  132. package/dist/hooks/use-anchored-rect.d.ts +24 -0
  133. package/dist/hooks/use-anchored-rect.js +40 -0
  134. package/dist/hooks/use-anchored-rect.js.map +1 -0
  135. package/dist/hooks/use-body-scroll-lock.d.ts +9 -0
  136. package/dist/hooks/use-body-scroll-lock.js +41 -0
  137. package/dist/hooks/use-body-scroll-lock.js.map +1 -0
  138. package/dist/hooks/use-close-transition.d.ts +41 -0
  139. package/dist/hooks/use-close-transition.js +36 -0
  140. package/dist/hooks/use-close-transition.js.map +1 -0
  141. package/dist/hooks/use-dismiss.d.ts +34 -0
  142. package/dist/hooks/use-dismiss.js +41 -0
  143. package/dist/hooks/use-dismiss.js.map +1 -0
  144. package/dist/hooks/use-media-query.d.ts +8 -0
  145. package/dist/hooks/use-media-query.js +18 -0
  146. package/dist/hooks/use-media-query.js.map +1 -0
  147. package/dist/hooks/use-overlay-history.d.ts +10 -0
  148. package/dist/hooks/use-overlay-history.js +99 -0
  149. package/dist/hooks/use-overlay-history.js.map +1 -0
  150. package/dist/hooks/use-row-swipe.d.ts +30 -0
  151. package/dist/hooks/use-row-swipe.js +128 -0
  152. package/dist/hooks/use-row-swipe.js.map +1 -0
  153. package/dist/index.d.ts +75 -0
  154. package/dist/index.js +90 -0
  155. package/dist/index.js.map +1 -0
  156. package/dist/lib/calc.d.ts +89 -0
  157. package/dist/lib/calc.js +180 -0
  158. package/dist/lib/calc.js.map +1 -0
  159. package/dist/lib/cn.d.ts +6 -0
  160. package/dist/lib/cn.js +7 -0
  161. package/dist/lib/cn.js.map +1 -0
  162. package/dist/lib/dates.d.ts +84 -0
  163. package/dist/lib/dates.js +108 -0
  164. package/dist/lib/dates.js.map +1 -0
  165. package/dist/lib/logger.d.ts +17 -0
  166. package/dist/lib/logger.js +30 -0
  167. package/dist/lib/logger.js.map +1 -0
  168. package/dist/lib/safe-storage.d.ts +23 -0
  169. package/dist/lib/safe-storage.js +20 -0
  170. package/dist/lib/safe-storage.js.map +1 -0
  171. package/dist/search/command-palette.d.ts +71 -0
  172. package/dist/search/command-palette.js +198 -0
  173. package/dist/search/command-palette.js.map +1 -0
  174. package/dist/shell/app-shell.d.ts +64 -0
  175. package/dist/shell/app-shell.js +237 -0
  176. package/dist/shell/app-shell.js.map +1 -0
  177. package/dist/shell/option-switcher-menu.d.ts +24 -0
  178. package/dist/shell/option-switcher-menu.js +44 -0
  179. package/dist/shell/option-switcher-menu.js.map +1 -0
  180. package/dist/shell/role-switcher.d.ts +19 -0
  181. package/dist/shell/role-switcher.js +28 -0
  182. package/dist/shell/role-switcher.js.map +1 -0
  183. package/dist/shell/top-bar.d.ts +15 -0
  184. package/dist/shell/top-bar.js +29 -0
  185. package/dist/shell/top-bar.js.map +1 -0
  186. package/dist/shell/topbar-action-menu.d.ts +44 -0
  187. package/dist/shell/topbar-action-menu.js +49 -0
  188. package/dist/shell/topbar-action-menu.js.map +1 -0
  189. package/dist/shell/topbar-controls.d.ts +54 -0
  190. package/dist/shell/topbar-controls.js +152 -0
  191. package/dist/shell/topbar-controls.js.map +1 -0
  192. package/dist/theme/chart-palette.d.ts +60 -0
  193. package/dist/theme/chart-palette.js +79 -0
  194. package/dist/theme/chart-palette.js.map +1 -0
  195. package/dist/theme/palette-presets.d.ts +39 -0
  196. package/dist/theme/palette-presets.js +440 -0
  197. package/dist/theme/palette-presets.js.map +1 -0
  198. package/dist/theme/palette-store.d.ts +45 -0
  199. package/dist/theme/palette-store.js +52 -0
  200. package/dist/theme/palette-store.js.map +1 -0
  201. package/dist/theme/theme-store.d.ts +57 -0
  202. package/dist/theme/theme-store.js +96 -0
  203. package/dist/theme/theme-store.js.map +1 -0
  204. package/dist/tour/tour.d.ts +104 -0
  205. package/dist/tour/tour.js +351 -0
  206. package/dist/tour/tour.js.map +1 -0
  207. package/dist/wizard/stepper-nav.d.ts +30 -0
  208. package/dist/wizard/stepper-nav.js +146 -0
  209. package/dist/wizard/stepper-nav.js.map +1 -0
  210. package/dist/wizard/types.d.ts +124 -0
  211. package/dist/wizard/types.js +24 -0
  212. package/dist/wizard/types.js.map +1 -0
  213. package/dist/wizard/use-wizard.d.ts +6 -0
  214. package/dist/wizard/use-wizard.js +222 -0
  215. package/dist/wizard/use-wizard.js.map +1 -0
  216. package/dist/wizard/validation.d.ts +23 -0
  217. package/dist/wizard/validation.js +13 -0
  218. package/dist/wizard/validation.js.map +1 -0
  219. package/dist/wizard/wizard-context.d.ts +19 -0
  220. package/dist/wizard/wizard-context.js +15 -0
  221. package/dist/wizard/wizard-context.js.map +1 -0
  222. package/dist/wizard/wizard-step.d.ts +11 -0
  223. package/dist/wizard/wizard-step.js +12 -0
  224. package/dist/wizard/wizard-step.js.map +1 -0
  225. package/dist/wizard/wizard-summary.d.ts +18 -0
  226. package/dist/wizard/wizard-summary.js +41 -0
  227. package/dist/wizard/wizard-summary.js.map +1 -0
  228. package/package.json +87 -0
  229. package/tokens.css +254 -0
@@ -0,0 +1,33 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ function useMobileReveal({
3
+ rows,
4
+ slice,
5
+ isServer,
6
+ isMdUp,
7
+ defaultPageSize
8
+ }) {
9
+ const [mobileLimit, setMobileLimit] = useState(defaultPageSize);
10
+ const mobileSlice = isServer ? slice : rows.slice(0, mobileLimit);
11
+ const canRevealMoreMobile = !isServer && !isMdUp && mobileLimit < rows.length;
12
+ const loadMoreRef = useRef(null);
13
+ useEffect(() => {
14
+ if (!canRevealMoreMobile) return;
15
+ const el = loadMoreRef.current;
16
+ if (!el || typeof IntersectionObserver === "undefined") return;
17
+ const io = new IntersectionObserver(
18
+ (entries) => {
19
+ if (entries.some((e) => e.isIntersecting)) {
20
+ setMobileLimit((n) => n + (defaultPageSize || 25));
21
+ }
22
+ },
23
+ { rootMargin: "300px" }
24
+ );
25
+ io.observe(el);
26
+ return () => io.disconnect();
27
+ }, [canRevealMoreMobile, mobileLimit, defaultPageSize]);
28
+ return { mobileSlice, canRevealMoreMobile, loadMoreRef };
29
+ }
30
+ export {
31
+ useMobileReveal
32
+ };
33
+ //# sourceMappingURL=use-mobile-reveal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/use-mobile-reveal.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\n/**\n * The phone's endless scroll, in place of the desktop pager (feedback #232).\n *\n * A client-side table already holds every row, so there is nothing to fetch — it simply\n * reveals more of the sorted list as a sentinel scrolls into view. A SERVER-paginated\n * table keeps its pager instead, because the rest of the rows genuinely are not here.\n *\n * The reveal count only ever grows and self-caps at the (re-filtered) list length, which\n * is why nothing resets it: narrowing a filter cannot leave the limit pointing past the\n * end, and widening one does not have to re-reveal what the user already scrolled past.\n *\n * Lifted out of `DataTable` with the rest of its state; the sentinel ref belongs to the\n * caller's `<li>`.\n */\nexport function useMobileReveal<T>({\n rows,\n slice,\n isServer,\n isMdUp,\n defaultPageSize,\n}: {\n /** The whole sorted list — what there is to reveal. */\n rows: T[];\n /** What a server-paginated table shows instead. */\n slice: T[];\n isServer: boolean;\n isMdUp: boolean;\n defaultPageSize: number;\n}) {\n const [mobileLimit, setMobileLimit] = useState(defaultPageSize);\n const mobileSlice = isServer ? slice : rows.slice(0, mobileLimit);\n const canRevealMoreMobile = !isServer && !isMdUp && mobileLimit < rows.length;\n const loadMoreRef = useRef<HTMLLIElement | null>(null);\n\n useEffect(() => {\n if (!canRevealMoreMobile) return;\n const el = loadMoreRef.current;\n if (!el || typeof IntersectionObserver === \"undefined\") return;\n const io = new IntersectionObserver(\n (entries) => {\n if (entries.some((e) => e.isIntersecting)) {\n setMobileLimit((n) => n + (defaultPageSize || 25));\n }\n },\n { rootMargin: \"300px\" },\n );\n io.observe(el);\n return () => io.disconnect();\n }, [canRevealMoreMobile, mobileLimit, defaultPageSize]);\n\n return { mobileSlice, canRevealMoreMobile, loadMoreRef };\n}\n"],"mappings":"AAAA,SAAS,WAAW,QAAQ,gBAAgB;AAgBrC,SAAS,gBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,eAAe;AAC9D,QAAM,cAAc,WAAW,QAAQ,KAAK,MAAM,GAAG,WAAW;AAChE,QAAM,sBAAsB,CAAC,YAAY,CAAC,UAAU,cAAc,KAAK;AACvE,QAAM,cAAc,OAA6B,IAAI;AAErD,YAAU,MAAM;AACd,QAAI,CAAC,oBAAqB;AAC1B,UAAM,KAAK,YAAY;AACvB,QAAI,CAAC,MAAM,OAAO,yBAAyB,YAAa;AACxD,UAAM,KAAK,IAAI;AAAA,MACb,CAAC,YAAY;AACX,YAAI,QAAQ,KAAK,CAAC,MAAM,EAAE,cAAc,GAAG;AACzC,yBAAe,CAAC,MAAM,KAAK,mBAAmB,GAAG;AAAA,QACnD;AAAA,MACF;AAAA,MACA,EAAE,YAAY,QAAQ;AAAA,IACxB;AACA,OAAG,QAAQ,EAAE;AACb,WAAO,MAAM,GAAG,WAAW;AAAA,EAC7B,GAAG,CAAC,qBAAqB,aAAa,eAAe,CAAC;AAEtD,SAAO,EAAE,aAAa,qBAAqB,YAAY;AACzD;","names":[]}
@@ -0,0 +1,54 @@
1
+ import * as react from 'react';
2
+ import { useSearchParams } from 'react-router';
3
+ import { SortState } from './data-table-sort.js';
4
+ import { a as DataTableColumn, F as FilterState } from '../data-table-filters-aXyf0Xub.js';
5
+ import './swipeable-row.js';
6
+ import './data-table-labels.js';
7
+
8
+ declare const DEFAULT_PERSIST_PREFIX = "hbui-table:";
9
+ /**
10
+ * Where a table's view lives: what it is sorted and filtered by, which page, how wide
11
+ * the columns are and which are hidden — plus the two places that outlives a render.
12
+ *
13
+ * Lifted out of `DataTable`, which was 984 lines and twenty hooks in one function. This
14
+ * is the part that is genuinely about STATE rather than about drawing, and the three
15
+ * rules it encodes are easy to lose among the markup:
16
+ *
17
+ * * **The URL is read once, on mount.** A shared or deep link populates the initial view;
18
+ * after that the state is local, or the browser's own history would fight the user's
19
+ * next click.
20
+ * * **Sorting and filtering are optionally CONTROLLED.** A server-driven table passes
21
+ * state and a callback and owns them; everything else is self-owned. That is the
22
+ * `sortsProp ?? internalSorts` pair, and it is why the setters are exposed separately.
23
+ * * **Persistence writes on every change, the URL only when asked.** `storageKey` is the
24
+ * per-table identity; `urlSync` is opt-in because only some tables want their view in
25
+ * a shareable address.
26
+ */
27
+ declare function useTableState<T>({ columns, storageKey, storageKeyPrefix, urlSync, setSearchParams, defaultPageSize, sortsProp, filtersProp, }: {
28
+ columns: DataTableColumn<T>[];
29
+ storageKey?: string;
30
+ storageKeyPrefix: string;
31
+ urlSync?: boolean;
32
+ setSearchParams: ReturnType<typeof useSearchParams>[1];
33
+ defaultPageSize: number;
34
+ sortsProp?: SortState[];
35
+ filtersProp?: FilterState;
36
+ }): {
37
+ sorts: SortState[];
38
+ filters: FilterState;
39
+ setInternalSorts: react.Dispatch<react.SetStateAction<SortState[]>>;
40
+ setInternalFilters: react.Dispatch<react.SetStateAction<FilterState>>;
41
+ page: number;
42
+ setPage: react.Dispatch<react.SetStateAction<number>>;
43
+ pageSize: number;
44
+ setPageSize: react.Dispatch<react.SetStateAction<number>>;
45
+ widths: Record<string, number>;
46
+ setWidths: react.Dispatch<react.SetStateAction<Record<string, number>>>;
47
+ hiddenCols: Set<string>;
48
+ setHiddenCols: react.Dispatch<react.SetStateAction<Set<string>>>;
49
+ showSettings: boolean;
50
+ setShowSettings: react.Dispatch<react.SetStateAction<boolean>>;
51
+ headRefs: Map<string, HTMLTableCellElement>;
52
+ };
53
+
54
+ export { DEFAULT_PERSIST_PREFIX, useTableState };
@@ -0,0 +1,153 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { readStored, writeStored } from "../lib/safe-storage.js";
3
+ import {
4
+ decodeFilterValue,
5
+ encodeFilterValue,
6
+ isFilterActive,
7
+ resolveFilter
8
+ } from "./data-table-filters.js";
9
+ import { decodeSorts, encodeSorts, normalizeSorts } from "./data-table-sort.js";
10
+ const DEFAULT_PERSIST_PREFIX = "hbui-table:";
11
+ function loadPersisted(storageKey, prefix) {
12
+ if (!storageKey) return {};
13
+ const raw = readStored(prefix + storageKey);
14
+ if (!raw) return {};
15
+ try {
16
+ const parsed = JSON.parse(raw);
17
+ return parsed && typeof parsed === "object" ? parsed : {};
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
22
+ function savePersisted(storageKey, state, prefix) {
23
+ if (!storageKey) return;
24
+ writeStored(prefix + storageKey, JSON.stringify(state));
25
+ }
26
+ const URL_FILTER_PREFIX = "f.";
27
+ const URL_SORT_KEY = "sort";
28
+ const URL_PAGE_KEY = "p";
29
+ const URL_PAGE_SIZE_KEY = "ps";
30
+ function readUrlState(columns) {
31
+ if (typeof window === "undefined") return {};
32
+ const sp = new URLSearchParams(window.location.search);
33
+ const out = {};
34
+ const filters = {};
35
+ for (const col of columns) {
36
+ const filter = resolveFilter(col);
37
+ if (!filter) continue;
38
+ const raw = sp.get(URL_FILTER_PREFIX + col.key);
39
+ if (raw == null) continue;
40
+ filters[col.key] = decodeFilterValue(filter, raw);
41
+ }
42
+ if (Object.keys(filters).length) out.filters = filters;
43
+ const sortRaw = sp.get(URL_SORT_KEY);
44
+ if (sortRaw) {
45
+ const decoded = decodeSorts(sortRaw, new Set(columns.map((c) => c.key)));
46
+ if (decoded.length) out.sorts = decoded;
47
+ }
48
+ const psRaw = sp.get(URL_PAGE_SIZE_KEY);
49
+ if (psRaw === "all") out.pageSize = Infinity;
50
+ else if (psRaw && Number(psRaw) > 0) out.pageSize = Number(psRaw);
51
+ const pRaw = sp.get(URL_PAGE_KEY);
52
+ if (pRaw && Number(pRaw) >= 1) out.page = Number(pRaw) - 1;
53
+ return out;
54
+ }
55
+ function writeUrlState(setSearchParams, columns, sorts, filters, pageSize, page, defaultPageSize) {
56
+ setSearchParams(
57
+ (prev) => {
58
+ const next = new URLSearchParams(prev);
59
+ for (const col of columns) next.delete(URL_FILTER_PREFIX + col.key);
60
+ next.delete(URL_SORT_KEY);
61
+ next.delete(URL_PAGE_KEY);
62
+ next.delete(URL_PAGE_SIZE_KEY);
63
+ for (const col of columns) {
64
+ const state = filters[col.key];
65
+ if (!state || !isFilterActive(state)) continue;
66
+ const enc = encodeFilterValue(state);
67
+ if (enc != null) next.set(URL_FILTER_PREFIX + col.key, enc);
68
+ }
69
+ const sortEnc = encodeSorts(sorts);
70
+ if (sortEnc) next.set(URL_SORT_KEY, sortEnc);
71
+ if (pageSize === Infinity) next.set(URL_PAGE_SIZE_KEY, "all");
72
+ else if (pageSize !== defaultPageSize) next.set(URL_PAGE_SIZE_KEY, String(pageSize));
73
+ if (page > 0) next.set(URL_PAGE_KEY, String(page + 1));
74
+ return next;
75
+ },
76
+ { replace: true }
77
+ );
78
+ }
79
+ function useTableState({
80
+ columns,
81
+ storageKey,
82
+ storageKeyPrefix,
83
+ urlSync,
84
+ setSearchParams,
85
+ defaultPageSize,
86
+ sortsProp,
87
+ filtersProp
88
+ }) {
89
+ const [urlInitial] = useState(() => urlSync ? readUrlState(columns) : {});
90
+ const initial = useMemo(
91
+ () => loadPersisted(storageKey, storageKeyPrefix),
92
+ [storageKey, storageKeyPrefix]
93
+ );
94
+ const [internalSorts, setInternalSorts] = useState(
95
+ () => urlInitial.sorts !== void 0 ? urlInitial.sorts : normalizeSorts(initial.sort)
96
+ );
97
+ const [internalFilters, setInternalFilters] = useState(
98
+ () => urlInitial.filters ?? initial.filters ?? {}
99
+ );
100
+ const sorts = sortsProp ?? internalSorts;
101
+ const filters = filtersProp ?? internalFilters;
102
+ const [page, setPage] = useState(() => urlInitial.page ?? 0);
103
+ const [pageSize, setPageSize] = useState(() => {
104
+ if (urlInitial.pageSize !== void 0) return urlInitial.pageSize;
105
+ if (initial.pageSize === "all") return Infinity;
106
+ if (typeof initial.pageSize === "number" && initial.pageSize > 0) return initial.pageSize;
107
+ return defaultPageSize;
108
+ });
109
+ const [widths, setWidths] = useState(() => initial.widths ?? {});
110
+ const [hiddenCols, setHiddenCols] = useState(() => new Set(initial.hidden ?? []));
111
+ const [showSettings, setShowSettings] = useState(false);
112
+ const headRefs = useMemo(() => /* @__PURE__ */ new Map(), []);
113
+ useEffect(() => {
114
+ if (!storageKey) return;
115
+ savePersisted(
116
+ storageKey,
117
+ {
118
+ sort: sorts,
119
+ filters,
120
+ pageSize: pageSize === Infinity ? "all" : pageSize,
121
+ widths,
122
+ hidden: Array.from(hiddenCols)
123
+ },
124
+ storageKeyPrefix
125
+ );
126
+ }, [storageKey, storageKeyPrefix, sorts, filters, pageSize, widths, hiddenCols]);
127
+ useEffect(() => {
128
+ if (!urlSync) return;
129
+ writeUrlState(setSearchParams, columns, sorts, filters, pageSize, page, defaultPageSize);
130
+ }, [urlSync, sorts, filters, pageSize, page, defaultPageSize]);
131
+ return {
132
+ sorts,
133
+ filters,
134
+ setInternalSorts,
135
+ setInternalFilters,
136
+ page,
137
+ setPage,
138
+ pageSize,
139
+ setPageSize,
140
+ widths,
141
+ setWidths,
142
+ hiddenCols,
143
+ setHiddenCols,
144
+ showSettings,
145
+ setShowSettings,
146
+ headRefs
147
+ };
148
+ }
149
+ export {
150
+ DEFAULT_PERSIST_PREFIX,
151
+ useTableState
152
+ };
153
+ //# sourceMappingURL=use-table-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/use-table-state.ts"],"sourcesContent":["import { useEffect, useMemo, useState } from \"react\";\nimport type { useSearchParams } from \"react-router\";\nimport { readStored, writeStored } from \"../lib/safe-storage\";\nimport {\n decodeFilterValue,\n encodeFilterValue,\n isFilterActive,\n resolveFilter,\n} from \"./data-table-filters\";\nimport { decodeSorts, encodeSorts, normalizeSorts, type SortState } from \"./data-table-sort\";\nimport type { DataTableColumn, FilterState } from \"./data-table\";\n\n// ---------- Persisted + URL-encoded view state ----------\n//\n// Moved here with `useTableState` when `DataTable` was split: these are what the hook\n// reads and writes, and nothing else in the table ever called them.\n\ninterface PersistedState {\n // Multi-sort array; blobs from before the upgrade hold a single {key, dir}\n // object (or null) — read back through normalizeSorts.\n sort: SortState[] | { key: string; dir: string } | null;\n filters: FilterState;\n pageSize: number | \"all\";\n widths?: Record<string, number>;\n hidden?: string[];\n}\n\nexport const DEFAULT_PERSIST_PREFIX = \"hbui-table:\";\n\nfunction loadPersisted(storageKey: string | undefined, prefix: string): Partial<PersistedState> {\n if (!storageKey) return {};\n const raw = readStored(prefix + storageKey);\n if (!raw) return {};\n try {\n const parsed = JSON.parse(raw) as Partial<PersistedState>;\n return parsed && typeof parsed === \"object\" ? parsed : {};\n } catch {\n return {}; // corrupt JSON — the storage access itself is guarded by readStored\n }\n}\n\nfunction savePersisted(storageKey: string | undefined, state: PersistedState, prefix: string): void {\n if (!storageKey) return;\n writeStored(prefix + storageKey, JSON.stringify(state));\n}\n\n// ---------- URL sync helpers ----------\n\nconst URL_FILTER_PREFIX = \"f.\";\nconst URL_SORT_KEY = \"sort\";\nconst URL_PAGE_KEY = \"p\";\nconst URL_PAGE_SIZE_KEY = \"ps\";\n\ninterface UrlState {\n sorts?: SortState[];\n filters?: FilterState;\n pageSize?: number; // Infinity for \"all\"\n page?: number; // 0-based\n}\n\nfunction readUrlState<T>(columns: DataTableColumn<T>[]): UrlState {\n if (typeof window === \"undefined\") return {};\n const sp = new URLSearchParams(window.location.search);\n const out: UrlState = {};\n\n const filters: FilterState = {};\n for (const col of columns) {\n const filter = resolveFilter(col);\n if (!filter) continue;\n const raw = sp.get(URL_FILTER_PREFIX + col.key);\n if (raw == null) continue;\n filters[col.key] = decodeFilterValue(filter, raw);\n }\n if (Object.keys(filters).length) out.filters = filters;\n\n const sortRaw = sp.get(URL_SORT_KEY);\n if (sortRaw) {\n const decoded = decodeSorts(sortRaw, new Set(columns.map((c) => c.key)));\n if (decoded.length) out.sorts = decoded;\n }\n\n const psRaw = sp.get(URL_PAGE_SIZE_KEY);\n if (psRaw === \"all\") out.pageSize = Infinity;\n else if (psRaw && Number(psRaw) > 0) out.pageSize = Number(psRaw);\n\n const pRaw = sp.get(URL_PAGE_KEY);\n if (pRaw && Number(pRaw) >= 1) out.page = Number(pRaw) - 1;\n\n return out;\n}\n\nfunction writeUrlState<T>(\n setSearchParams: ReturnType<typeof useSearchParams>[1],\n columns: DataTableColumn<T>[],\n sorts: SortState[],\n filters: FilterState,\n pageSize: number,\n page: number,\n defaultPageSize: number,\n): void {\n setSearchParams(\n (prev) => {\n const next = new URLSearchParams(prev);\n // clear managed keys\n for (const col of columns) next.delete(URL_FILTER_PREFIX + col.key);\n next.delete(URL_SORT_KEY);\n next.delete(URL_PAGE_KEY);\n next.delete(URL_PAGE_SIZE_KEY);\n // write filters\n for (const col of columns) {\n const state = filters[col.key];\n if (!state || !isFilterActive(state)) continue;\n const enc = encodeFilterValue(state);\n if (enc != null) next.set(URL_FILTER_PREFIX + col.key, enc);\n }\n // write sort\n const sortEnc = encodeSorts(sorts);\n if (sortEnc) next.set(URL_SORT_KEY, sortEnc);\n // write page size if non-default\n if (pageSize === Infinity) next.set(URL_PAGE_SIZE_KEY, \"all\");\n else if (pageSize !== defaultPageSize) next.set(URL_PAGE_SIZE_KEY, String(pageSize));\n // write page if not first\n if (page > 0) next.set(URL_PAGE_KEY, String(page + 1));\n return next;\n },\n { replace: true },\n );\n}\n\n/**\n * Where a table's view lives: what it is sorted and filtered by, which page, how wide\n * the columns are and which are hidden — plus the two places that outlives a render.\n *\n * Lifted out of `DataTable`, which was 984 lines and twenty hooks in one function. This\n * is the part that is genuinely about STATE rather than about drawing, and the three\n * rules it encodes are easy to lose among the markup:\n *\n * * **The URL is read once, on mount.** A shared or deep link populates the initial view;\n * after that the state is local, or the browser's own history would fight the user's\n * next click.\n * * **Sorting and filtering are optionally CONTROLLED.** A server-driven table passes\n * state and a callback and owns them; everything else is self-owned. That is the\n * `sortsProp ?? internalSorts` pair, and it is why the setters are exposed separately.\n * * **Persistence writes on every change, the URL only when asked.** `storageKey` is the\n * per-table identity; `urlSync` is opt-in because only some tables want their view in\n * a shareable address.\n */\nexport function useTableState<T>({\n columns,\n storageKey,\n storageKeyPrefix,\n urlSync,\n setSearchParams,\n defaultPageSize,\n sortsProp,\n filtersProp,\n}: {\n columns: DataTableColumn<T>[];\n storageKey?: string;\n storageKeyPrefix: string;\n urlSync?: boolean;\n setSearchParams: ReturnType<typeof useSearchParams>[1];\n defaultPageSize: number;\n sortsProp?: SortState[];\n filtersProp?: FilterState;\n}) {\n // Read URL once on mount so URL-encoded views (e.g. shared links, deep links from\n // other pages) populate initial state. After mount, state is local.\n const [urlInitial] = useState<UrlState>(() => (urlSync ? readUrlState(columns) : {}));\n const initial = useMemo(\n () => loadPersisted(storageKey, storageKeyPrefix),\n [storageKey, storageKeyPrefix],\n );\n const [internalSorts, setInternalSorts] = useState<SortState[]>(() =>\n urlInitial.sorts !== undefined ? urlInitial.sorts : normalizeSorts(initial.sort),\n );\n const [internalFilters, setInternalFilters] = useState<FilterState>(\n () => urlInitial.filters ?? initial.filters ?? {},\n );\n // Controlled when the owner passes state + callback (server-driven tables);\n // self-owned otherwise.\n const sorts = sortsProp ?? internalSorts;\n const filters = filtersProp ?? internalFilters;\n const [page, setPage] = useState(() => urlInitial.page ?? 0);\n const [pageSize, setPageSize] = useState<number>(() => {\n if (urlInitial.pageSize !== undefined) return urlInitial.pageSize;\n if (initial.pageSize === \"all\") return Infinity;\n if (typeof initial.pageSize === \"number\" && initial.pageSize > 0) return initial.pageSize;\n return defaultPageSize;\n });\n const [widths, setWidths] = useState<Record<string, number>>(() => initial.widths ?? {});\n const [hiddenCols, setHiddenCols] = useState<Set<string>>(() => new Set(initial.hidden ?? []));\n const [showSettings, setShowSettings] = useState(false);\n const headRefs = useMemo(() => new Map<string, HTMLTableCellElement>(), []);\n\n useEffect(() => {\n if (!storageKey) return;\n savePersisted(\n storageKey,\n {\n sort: sorts,\n filters,\n pageSize: pageSize === Infinity ? \"all\" : pageSize,\n widths,\n hidden: Array.from(hiddenCols),\n },\n storageKeyPrefix,\n );\n }, [storageKey, storageKeyPrefix, sorts, filters, pageSize, widths, hiddenCols]);\n\n useEffect(() => {\n if (!urlSync) return;\n writeUrlState(setSearchParams, columns, sorts, filters, pageSize, page, defaultPageSize);\n // columns identity changes per render but URL output only depends on column keys;\n // including columns would re-run on every render. We rely on data state only.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [urlSync, sorts, filters, pageSize, page, defaultPageSize]);\n\n return {\n sorts,\n filters,\n setInternalSorts,\n setInternalFilters,\n page,\n setPage,\n pageSize,\n setPageSize,\n widths,\n setWidths,\n hiddenCols,\n setHiddenCols,\n showSettings,\n setShowSettings,\n headRefs,\n };\n}\n"],"mappings":"AAAA,SAAS,WAAW,SAAS,gBAAgB;AAE7C,SAAS,YAAY,mBAAmB;AACxC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa,aAAa,sBAAsC;AAkBlE,MAAM,yBAAyB;AAEtC,SAAS,cAAc,YAAgC,QAAyC;AAC9F,MAAI,CAAC,WAAY,QAAO,CAAC;AACzB,QAAM,MAAM,WAAW,SAAS,UAAU;AAC1C,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,WAAO,UAAU,OAAO,WAAW,WAAW,SAAS,CAAC;AAAA,EAC1D,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,SAAS,cAAc,YAAgC,OAAuB,QAAsB;AAClG,MAAI,CAAC,WAAY;AACjB,cAAY,SAAS,YAAY,KAAK,UAAU,KAAK,CAAC;AACxD;AAIA,MAAM,oBAAoB;AAC1B,MAAM,eAAe;AACrB,MAAM,eAAe;AACrB,MAAM,oBAAoB;AAS1B,SAAS,aAAgB,SAAyC;AAChE,MAAI,OAAO,WAAW,YAAa,QAAO,CAAC;AAC3C,QAAM,KAAK,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACrD,QAAM,MAAgB,CAAC;AAEvB,QAAM,UAAuB,CAAC;AAC9B,aAAW,OAAO,SAAS;AACzB,UAAM,SAAS,cAAc,GAAG;AAChC,QAAI,CAAC,OAAQ;AACb,UAAM,MAAM,GAAG,IAAI,oBAAoB,IAAI,GAAG;AAC9C,QAAI,OAAO,KAAM;AACjB,YAAQ,IAAI,GAAG,IAAI,kBAAkB,QAAQ,GAAG;AAAA,EAClD;AACA,MAAI,OAAO,KAAK,OAAO,EAAE,OAAQ,KAAI,UAAU;AAE/C,QAAM,UAAU,GAAG,IAAI,YAAY;AACnC,MAAI,SAAS;AACX,UAAM,UAAU,YAAY,SAAS,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACvE,QAAI,QAAQ,OAAQ,KAAI,QAAQ;AAAA,EAClC;AAEA,QAAM,QAAQ,GAAG,IAAI,iBAAiB;AACtC,MAAI,UAAU,MAAO,KAAI,WAAW;AAAA,WAC3B,SAAS,OAAO,KAAK,IAAI,EAAG,KAAI,WAAW,OAAO,KAAK;AAEhE,QAAM,OAAO,GAAG,IAAI,YAAY;AAChC,MAAI,QAAQ,OAAO,IAAI,KAAK,EAAG,KAAI,OAAO,OAAO,IAAI,IAAI;AAEzD,SAAO;AACT;AAEA,SAAS,cACP,iBACA,SACA,OACA,SACA,UACA,MACA,iBACM;AACN;AAAA,IACE,CAAC,SAAS;AACR,YAAM,OAAO,IAAI,gBAAgB,IAAI;AAErC,iBAAW,OAAO,QAAS,MAAK,OAAO,oBAAoB,IAAI,GAAG;AAClE,WAAK,OAAO,YAAY;AACxB,WAAK,OAAO,YAAY;AACxB,WAAK,OAAO,iBAAiB;AAE7B,iBAAW,OAAO,SAAS;AACzB,cAAM,QAAQ,QAAQ,IAAI,GAAG;AAC7B,YAAI,CAAC,SAAS,CAAC,eAAe,KAAK,EAAG;AACtC,cAAM,MAAM,kBAAkB,KAAK;AACnC,YAAI,OAAO,KAAM,MAAK,IAAI,oBAAoB,IAAI,KAAK,GAAG;AAAA,MAC5D;AAEA,YAAM,UAAU,YAAY,KAAK;AACjC,UAAI,QAAS,MAAK,IAAI,cAAc,OAAO;AAE3C,UAAI,aAAa,SAAU,MAAK,IAAI,mBAAmB,KAAK;AAAA,eACnD,aAAa,gBAAiB,MAAK,IAAI,mBAAmB,OAAO,QAAQ,CAAC;AAEnF,UAAI,OAAO,EAAG,MAAK,IAAI,cAAc,OAAO,OAAO,CAAC,CAAC;AACrD,aAAO;AAAA,IACT;AAAA,IACA,EAAE,SAAS,KAAK;AAAA,EAClB;AACF;AAoBO,SAAS,cAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GASG;AAGD,QAAM,CAAC,UAAU,IAAI,SAAmB,MAAO,UAAU,aAAa,OAAO,IAAI,CAAC,CAAE;AACpF,QAAM,UAAU;AAAA,IACd,MAAM,cAAc,YAAY,gBAAgB;AAAA,IAChD,CAAC,YAAY,gBAAgB;AAAA,EAC/B;AACA,QAAM,CAAC,eAAe,gBAAgB,IAAI;AAAA,IAAsB,MAC9D,WAAW,UAAU,SAAY,WAAW,QAAQ,eAAe,QAAQ,IAAI;AAAA,EACjF;AACA,QAAM,CAAC,iBAAiB,kBAAkB,IAAI;AAAA,IAC5C,MAAM,WAAW,WAAW,QAAQ,WAAW,CAAC;AAAA,EAClD;AAGA,QAAM,QAAQ,aAAa;AAC3B,QAAM,UAAU,eAAe;AAC/B,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,MAAM,WAAW,QAAQ,CAAC;AAC3D,QAAM,CAAC,UAAU,WAAW,IAAI,SAAiB,MAAM;AACrD,QAAI,WAAW,aAAa,OAAW,QAAO,WAAW;AACzD,QAAI,QAAQ,aAAa,MAAO,QAAO;AACvC,QAAI,OAAO,QAAQ,aAAa,YAAY,QAAQ,WAAW,EAAG,QAAO,QAAQ;AACjF,WAAO;AAAA,EACT,CAAC;AACD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAiC,MAAM,QAAQ,UAAU,CAAC,CAAC;AACvF,QAAM,CAAC,YAAY,aAAa,IAAI,SAAsB,MAAM,IAAI,IAAI,QAAQ,UAAU,CAAC,CAAC,CAAC;AAC7F,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AACtD,QAAM,WAAW,QAAQ,MAAM,oBAAI,IAAkC,GAAG,CAAC,CAAC;AAE1E,YAAU,MAAM;AACd,QAAI,CAAC,WAAY;AACjB;AAAA,MACE;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN;AAAA,QACA,UAAU,aAAa,WAAW,QAAQ;AAAA,QAC1C;AAAA,QACA,QAAQ,MAAM,KAAK,UAAU;AAAA,MAC/B;AAAA,MACA;AAAA,IACF;AAAA,EACF,GAAG,CAAC,YAAY,kBAAkB,OAAO,SAAS,UAAU,QAAQ,UAAU,CAAC;AAE/E,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,kBAAc,iBAAiB,SAAS,OAAO,SAAS,UAAU,MAAM,eAAe;AAAA,EAIzF,GAAG,CAAC,SAAS,OAAO,SAAS,UAAU,MAAM,eAAe,CAAC;AAE7D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,23 @@
1
+ import * as react from 'react';
2
+
3
+ /** Initials from a display name (first + last) or, failing that, an email —
4
+ * e.g. "Marcel Eifert" → "ME", "marcel@x.com" → "MA". App-agnostic. */
5
+ declare function avatarInitials(name?: string | null, email?: string | null): string;
6
+ declare const AVATAR_SIZES: {
7
+ readonly sm: "size-7 text-[11px]";
8
+ readonly md: "size-8 text-xs";
9
+ readonly lg: "size-12 text-base";
10
+ };
11
+ interface UserAvatarProps {
12
+ name?: string | null;
13
+ email?: string | null;
14
+ size?: keyof typeof AVATAR_SIZES;
15
+ className?: string;
16
+ }
17
+ /**
18
+ * A round initials avatar — the common user chip shared across apps (feedback
19
+ * #333). Purely presentational; wrap it in a button for the account menu trigger.
20
+ */
21
+ declare function UserAvatar({ name, email, size, className }: UserAvatarProps): react.JSX.Element;
22
+
23
+ export { UserAvatar, type UserAvatarProps, avatarInitials };
@@ -0,0 +1,33 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { cn } from "../lib/cn.js";
3
+ function avatarInitials(name, email) {
4
+ const source = (name ?? email ?? "?").trim();
5
+ if (!source) return "?";
6
+ const parts = source.split(/\s+/).filter(Boolean);
7
+ if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
8
+ return source.slice(0, 2).toUpperCase();
9
+ }
10
+ const AVATAR_SIZES = {
11
+ sm: "size-7 text-[11px]",
12
+ md: "size-8 text-xs",
13
+ lg: "size-12 text-base"
14
+ };
15
+ function UserAvatar({ name, email, size = "md", className }) {
16
+ return /* @__PURE__ */ jsx(
17
+ "span",
18
+ {
19
+ "aria-hidden": true,
20
+ className: cn(
21
+ "inline-flex shrink-0 items-center justify-center rounded-full bg-slate-900 font-semibold text-white dark:bg-slate-100 dark:text-slate-900",
22
+ AVATAR_SIZES[size],
23
+ className
24
+ ),
25
+ children: avatarInitials(name, email)
26
+ }
27
+ );
28
+ }
29
+ export {
30
+ UserAvatar,
31
+ avatarInitials
32
+ };
33
+ //# sourceMappingURL=user-avatar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/user-avatar.tsx"],"sourcesContent":["import { cn } from \"../lib/cn\";\n\n/** Initials from a display name (first + last) or, failing that, an email —\n * e.g. \"Marcel Eifert\" → \"ME\", \"marcel@x.com\" → \"MA\". App-agnostic. */\nexport function avatarInitials(name?: string | null, email?: string | null): string {\n const source = (name ?? email ?? \"?\").trim();\n if (!source) return \"?\";\n const parts = source.split(/\\s+/).filter(Boolean);\n if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();\n return source.slice(0, 2).toUpperCase();\n}\n\nconst AVATAR_SIZES = {\n sm: \"size-7 text-[11px]\",\n md: \"size-8 text-xs\",\n lg: \"size-12 text-base\",\n} as const;\n\nexport interface UserAvatarProps {\n name?: string | null;\n email?: string | null;\n size?: keyof typeof AVATAR_SIZES;\n className?: string;\n}\n\n/**\n * A round initials avatar — the common user chip shared across apps (feedback\n * #333). Purely presentational; wrap it in a button for the account menu trigger.\n */\nexport function UserAvatar({ name, email, size = \"md\", className }: UserAvatarProps) {\n return (\n <span\n aria-hidden\n className={cn(\n \"inline-flex shrink-0 items-center justify-center rounded-full bg-slate-900 font-semibold text-white dark:bg-slate-100 dark:text-slate-900\",\n AVATAR_SIZES[size],\n className,\n )}\n >\n {avatarInitials(name, email)}\n </span>\n );\n}\n"],"mappings":"AA+BI;AA/BJ,SAAS,UAAU;AAIZ,SAAS,eAAe,MAAsB,OAA+B;AAClF,QAAM,UAAU,QAAQ,SAAS,KAAK,KAAK;AAC3C,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,QAAQ,OAAO,MAAM,KAAK,EAAE,OAAO,OAAO;AAChD,MAAI,MAAM,UAAU,EAAG,SAAQ,MAAM,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,EAAE,CAAC,GAAG,YAAY;AACtE,SAAO,OAAO,MAAM,GAAG,CAAC,EAAE,YAAY;AACxC;AAEA,MAAM,eAAe;AAAA,EACnB,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAaO,SAAS,WAAW,EAAE,MAAM,OAAO,OAAO,MAAM,UAAU,GAAoB;AACnF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAW;AAAA,MACX,WAAW;AAAA,QACT;AAAA,QACA,aAAa,IAAI;AAAA,QACjB;AAAA,MACF;AAAA,MAEC,yBAAe,MAAM,KAAK;AAAA;AAAA,EAC7B;AAEJ;","names":[]}
@@ -0,0 +1,14 @@
1
+ import * as react from 'react';
2
+
3
+ /** Compact step indicator for the import wizards. Past steps are plain, the
4
+ * current one is highlighted, future ones muted. */
5
+ declare function WizardStepper({ steps, current, ariaLabel, }: {
6
+ steps: {
7
+ key: string;
8
+ label: string;
9
+ }[];
10
+ current: string;
11
+ ariaLabel?: string;
12
+ }): react.JSX.Element;
13
+
14
+ export { WizardStepper };
@@ -0,0 +1,27 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../lib/cn.js";
3
+ function WizardStepper({
4
+ steps,
5
+ current,
6
+ ariaLabel
7
+ }) {
8
+ const currentIndex = steps.findIndex((s) => s.key === current);
9
+ return /* @__PURE__ */ jsx("ol", { className: "flex items-center gap-1 text-xs", "aria-label": ariaLabel, children: steps.map((step, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-1", children: [
10
+ i > 0 && /* @__PURE__ */ jsx("span", { className: "w-4 border-t border-slate-300 dark:border-slate-600" }),
11
+ /* @__PURE__ */ jsx(
12
+ "span",
13
+ {
14
+ "aria-current": step.key === current ? "step" : void 0,
15
+ className: cn(
16
+ "rounded-full px-2 py-0.5",
17
+ i === currentIndex ? "bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-900 font-medium" : i < currentIndex ? "text-slate-700 dark:text-slate-200" : "text-slate-400 dark:text-slate-500"
18
+ ),
19
+ children: step.label
20
+ }
21
+ )
22
+ ] }, step.key)) });
23
+ }
24
+ export {
25
+ WizardStepper
26
+ };
27
+ //# sourceMappingURL=wizard-stepper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/wizard-stepper.tsx"],"sourcesContent":["import { cn } from \"../lib/cn\";\n\n/** Compact step indicator for the import wizards. Past steps are plain, the\n * current one is highlighted, future ones muted. */\nexport function WizardStepper({\n steps,\n current,\n ariaLabel,\n}: {\n steps: { key: string; label: string }[];\n current: string;\n ariaLabel?: string;\n}) {\n const currentIndex = steps.findIndex((s) => s.key === current);\n return (\n <ol className=\"flex items-center gap-1 text-xs\" aria-label={ariaLabel}>\n {steps.map((step, i) => (\n <li key={step.key} className=\"flex items-center gap-1\">\n {i > 0 && <span className=\"w-4 border-t border-slate-300 dark:border-slate-600\" />}\n <span\n aria-current={step.key === current ? \"step\" : undefined}\n className={cn(\n \"rounded-full px-2 py-0.5\",\n i === currentIndex\n ? \"bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-900 font-medium\"\n : i < currentIndex\n ? \"text-slate-700 dark:text-slate-200\"\n : \"text-slate-400 dark:text-slate-500\",\n )}\n >\n {step.label}\n </span>\n </li>\n ))}\n </ol>\n );\n}\n"],"mappings":"AAiBQ,SACY,KADZ;AAjBR,SAAS,UAAU;AAIZ,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,eAAe,MAAM,UAAU,CAAC,MAAM,EAAE,QAAQ,OAAO;AAC7D,SACE,oBAAC,QAAG,WAAU,mCAAkC,cAAY,WACzD,gBAAM,IAAI,CAAC,MAAM,MAChB,qBAAC,QAAkB,WAAU,2BAC1B;AAAA,QAAI,KAAK,oBAAC,UAAK,WAAU,uDAAsD;AAAA,IAChF;AAAA,MAAC;AAAA;AAAA,QACC,gBAAc,KAAK,QAAQ,UAAU,SAAS;AAAA,QAC9C,WAAW;AAAA,UACT;AAAA,UACA,MAAM,eACF,8EACA,IAAI,eACF,uCACA;AAAA,QACR;AAAA,QAEC,eAAK;AAAA;AAAA,IACR;AAAA,OAdO,KAAK,GAed,CACD,GACH;AAEJ;","names":[]}
@@ -0,0 +1,261 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { SortState } from './components/data-table-sort.js';
4
+ import { SwipeAction } from './components/swipeable-row.js';
5
+ import { DataTableLabels } from './components/data-table-labels.js';
6
+
7
+ interface DataTableColumn<T> {
8
+ key: string;
9
+ header: ReactNode;
10
+ cell: (row: T) => ReactNode;
11
+ sortBy?: (row: T) => string | number | null | undefined;
12
+ filter?: ColumnFilter<T>;
13
+ filterBy?: (row: T) => string;
14
+ className?: string;
15
+ headClassName?: string;
16
+ mobilePrimary?: boolean;
17
+ mobileHidden?: boolean;
18
+ /**
19
+ * Set on a column whose cell renders its own `<a>` (or anything else that must
20
+ * keep its own click): the row anchor of `rowHref` then skips it and lands on
21
+ * the next column that allows wrapping (Keksdose feedback #451). Without it the
22
+ * fallback can wrap such a cell the moment the headline column is hidden from
23
+ * the settings panel — `<a>` inside `<a>`, which React warns about and which
24
+ * kills the inner link outright, since RowLink cancels the click on the capture
25
+ * phase and the cell's own `stopPropagation` then blocks the row handler too.
26
+ * The rejected alternative was sniffing the rendered output for an anchor: a
27
+ * cell that renders a link is a fact its author knows and the renderer cannot
28
+ * see without reaching into React elements it deliberately treats as opaque.
29
+ */
30
+ noRowLink?: boolean;
31
+ }
32
+ interface ServerPagination {
33
+ page: number;
34
+ pageSize: number;
35
+ total: number;
36
+ onPageChange: (page: number) => void;
37
+ onPageSizeChange?: (pageSize: number) => void;
38
+ isLoading?: boolean;
39
+ }
40
+ interface DataTableProps<T> {
41
+ rows: T[];
42
+ columns: DataTableColumn<T>[];
43
+ rowKey: (row: T) => string | number;
44
+ defaultPageSize?: number;
45
+ onRowClick?: (row: T) => void;
46
+ /**
47
+ * The URL that shows this row, when one exists — the table then renders a REAL
48
+ * anchor for it (Keksdose feedback #451, "to be able to middle click with the
49
+ * mouse and open in a new tab"). A row that opens through `onRowClick` alone is
50
+ * invisible to the browser: no middle click, no ⌘/Ctrl-click, no "copy link
51
+ * address", no status-bar preview, no long-press menu on a phone. The
52
+ * alternative was an `onAuxClick` that calls `window.open` — it buys back only
53
+ * the middle click and loses the other four, none of which JavaScript can fake.
54
+ *
55
+ * Return `undefined` for a row that has no address of its own. Only wire this up
56
+ * where the OPEN row is genuinely in the URL (a `?row=` param, a detail route);
57
+ * a row whose expansion lives in local component state has no link to hand out,
58
+ * and a link the target page cannot honour is worse than no link.
59
+ *
60
+ * The anchor covers the row's primary cell, not the whole `<tr>`: an `<a>` may
61
+ * not wrap table cells, and the absolutely-positioned row overlay that is the
62
+ * usual workaround would sit on top of the per-cell controls these rows carry
63
+ * (status toggles, the selection checkbox, nested links) and swallow them.
64
+ */
65
+ rowHref?: (row: T) => string | undefined;
66
+ expandedRow?: (row: T) => ReactNode | null;
67
+ isExpanded?: (row: T) => boolean;
68
+ rowClassName?: (row: T) => string | undefined;
69
+ /**
70
+ * Extra DOM attributes for a row's container — the `<tr>` on desktop, the
71
+ * `<li>` on mobile. For hooks that have to sit on the row itself rather than
72
+ * inside a cell: guided-tour anchors, analytics ids, e2e selectors. Undefined
73
+ * values are dropped, so a per-row conditional needs no filtering at the call
74
+ * site. Not a styling escape hatch — that is `rowClassName`.
75
+ */
76
+ rowAttributes?: (row: T) => Record<string, string | undefined> | undefined;
77
+ /**
78
+ * Set false for a table that is short by construction — an invoice's line
79
+ * items, a wizard's picks — where the pager is chrome around a list that can
80
+ * never need one. Renders every row and drops the footer entirely. Ignored in
81
+ * `serverPagination` mode, where the pager is the only way to reach the rest
82
+ * of the data.
83
+ */
84
+ paginated?: boolean;
85
+ empty?: ReactNode;
86
+ /**
87
+ * If set, the table persists filter / sort / page-size state to localStorage under this key.
88
+ * Use a stable, unique key per table (e.g. "transactions-table").
89
+ */
90
+ storageKey?: string;
91
+ /**
92
+ * When true, mirror filter/sort/page state into URL search params so views are
93
+ * shareable and bookmarkable. URL params used: `f.<column>`, `sort`, `p`, `ps`.
94
+ * URL state takes precedence over localStorage on initial load. Requires the
95
+ * consuming app to render inside a react-router Router.
96
+ */
97
+ urlSync?: boolean;
98
+ /**
99
+ * When true (desktop only), the table fills its parent's height and scrolls
100
+ * INTERNALLY, with the header pinned — so the page itself doesn't add a second,
101
+ * outer scrollbar. The parent must give it a bounded height (e.g. a flex
102
+ * column inside the viewport-locked app shell). See feedback #207.
103
+ */
104
+ fillHeight?: boolean;
105
+ /** How tall the scrolling body may get before it scrolls, as a CSS length.
106
+ *
107
+ * Default `calc(100dvh - 12rem)`, which is what every caller had before this
108
+ * existed: a bound is what makes the sticky header work, because a header
109
+ * can only stick to a wrapper that actually scrolls. The cap assumes the
110
+ * table has page chrome above and below it, and a table that is the last
111
+ * thing on its page can afford more — ASPICE Atlas report #12, "for the 25
112
+ * elements there should in best case be no scrolling necessary".
113
+ *
114
+ * Ignored under `fillHeight`, which bounds the body by its flex parent
115
+ * instead and is the stronger statement of the same intent. */
116
+ maxBodyHeight?: string;
117
+ /**
118
+ * When provided, the table renders the supplied rows as-is (no client-side
119
+ * filtering/sorting/slicing) and the pagination footer is driven by these
120
+ * server-controlled values. Column-level filters and sorting are hidden
121
+ * UNLESS the caller takes them over via `onFiltersChange`/`onSortsChange` —
122
+ * the caller is then expected to translate the state into the server query
123
+ * that produces `rows`.
124
+ */
125
+ serverPagination?: ServerPagination;
126
+ /**
127
+ * Controlled filter/sort state. When the `on*Change` callback is provided
128
+ * the table stops owning that piece of state: it renders `filters`/`sorts`
129
+ * as given and reports user interactions through the callback (including
130
+ * in serverPagination mode, where the UI is otherwise disabled). The owner
131
+ * is responsible for any page reset on change.
132
+ */
133
+ filters?: FilterState;
134
+ onFiltersChange?: (next: FilterState) => void;
135
+ sorts?: SortState[];
136
+ onSortsChange?: (next: SortState[]) => void;
137
+ /**
138
+ * Opt-in multi-row selection (desktop table only). When provided, a leading
139
+ * checkbox column is rendered with a select-all box in the header; the owner
140
+ * holds the selected set and reacts via the callbacks. Rows for which
141
+ * `isSelectable` returns false render no checkbox and are excluded from
142
+ * select-all. See feedback #285 (bulk edit).
143
+ */
144
+ selection?: {
145
+ isSelectable?: (row: T) => boolean;
146
+ isSelected: (row: T) => boolean;
147
+ onToggle: (row: T, checked: boolean) => void;
148
+ allSelected: boolean;
149
+ someSelected: boolean;
150
+ onToggleAll: (checked: boolean) => void;
151
+ };
152
+ /** User-facing strings (English defaults); pass translated overrides. */
153
+ labels?: Partial<DataTableLabels>;
154
+ /** BCP-47 locale for the date-filter picker. Defaults to "en". */
155
+ locale?: string;
156
+ /** localStorage namespace prefix for `storageKey` persistence. Defaults to
157
+ * "hbui-table:"; pass your app's own prefix to keep a stable namespace. */
158
+ storageKeyPrefix?: string;
159
+ /**
160
+ * On phones, surface the expanded row's detail in a full-screen dialog instead
161
+ * of unfolding it inline (feedback #204). Desktop always uses inline expansion.
162
+ */
163
+ mobileExpandAsDialog?: boolean;
164
+ /**
165
+ * Group the mobile card list into sections, each with a sticky header — the
166
+ * "assistance"-style list (like the transactions list grouped by date, but by
167
+ * whatever key you return, e.g. a name's first letter). The returned string is
168
+ * the section a row belongs to; rows must already be ordered so equal keys are
169
+ * contiguous (pair with a matching default sort). `mobileGroupLabel` formats
170
+ * the header. Desktop is unaffected. Feedback #317.
171
+ */
172
+ mobileGroupBy?: (row: T) => string;
173
+ mobileGroupLabel?: (key: string) => ReactNode;
174
+ /**
175
+ * Fully replace the default mobile card body (bold primary + labelled
176
+ * key/value rows) with a compact custom layout, while keeping the shared
177
+ * clickable/expandable row wrapper. Use when the stacked label/value grid
178
+ * wastes space (feedback #317). Desktop is unaffected.
179
+ */
180
+ mobileCard?: (row: T) => ReactNode;
181
+ /**
182
+ * Swipe actions for a mobile row, revealed by dragging it horizontally. Return
183
+ * `null` (or empty sides) for rows that should not move — a locked row, one whose
184
+ * mutation is in flight, one the user has expanded.
185
+ *
186
+ * Mobile only, and deliberately so: the desktop table already has room for an
187
+ * actions column, and a drag gesture on a pointer device is a worse version of a
188
+ * button. Wraps only the row body, so an expansion panel below stays put while the
189
+ * row above it slides.
190
+ */
191
+ mobileSwipeActions?: (row: T) => {
192
+ left?: SwipeAction[];
193
+ right?: SwipeAction[];
194
+ } | null;
195
+ /**
196
+ * Renders as a full-width row above the data rows in the desktop table
197
+ * (mobile has no equivalent list-row slot, so it's desktop-only). For a
198
+ * toggle that shows/hides a slice of rows, style it as an expand/collapse
199
+ * disclosure (chevron + label) so it reads as part of the table rather
200
+ * than a floating control above it. See feedback #10/#11 (transactions'
201
+ * "hide upcoming/scheduled" toggle).
202
+ */
203
+ leadingRow?: ReactNode;
204
+ }
205
+ type FilterState = Record<string, FilterValue>;
206
+
207
+ declare function DataTable<T>({ rows, columns, rowKey, defaultPageSize, onRowClick, rowHref, expandedRow, isExpanded, rowClassName, rowAttributes, paginated, empty, storageKey, urlSync, fillHeight, maxBodyHeight, serverPagination, filters: filtersProp, onFiltersChange, sorts: sortsProp, onSortsChange, selection, labels: labelsProp, locale, storageKeyPrefix, mobileExpandAsDialog, mobileGroupBy, mobileGroupLabel, mobileCard, mobileSwipeActions, leadingRow, }: DataTableProps<T>): react.JSX.Element;
208
+
209
+ /** How a column is filtered. `text`/`select`/`date`/`number` each read a value
210
+ * off the row and compare it against the user's {@link FilterValue}. */
211
+ type ColumnFilter<T> = {
212
+ type: "text";
213
+ getValue: (row: T) => string;
214
+ } | {
215
+ type: "select";
216
+ getValue: (row: T) => string;
217
+ options?: {
218
+ value: string;
219
+ label?: string;
220
+ }[];
221
+ } | {
222
+ type: "date";
223
+ getValue: (row: T) => string | null | undefined;
224
+ } | {
225
+ type: "number";
226
+ getValue: (row: T) => number | null | undefined;
227
+ };
228
+ /** The current state of a column's filter (what the user has entered). */
229
+ type FilterValue = {
230
+ type: "text";
231
+ q: string;
232
+ } | {
233
+ type: "select";
234
+ values: string[];
235
+ } | {
236
+ type: "date";
237
+ from: string;
238
+ to: string;
239
+ } | {
240
+ type: "number";
241
+ min: string;
242
+ max: string;
243
+ abs: boolean;
244
+ };
245
+ /** A column's filter config, falling back to a plain text filter over `filterBy`. */
246
+ declare function resolveFilter<T>(col: DataTableColumn<T>): ColumnFilter<T> | null;
247
+ /** Whether a filter value is actually narrowing anything (vs its empty default). */
248
+ declare function isFilterActive(v: FilterValue | undefined): boolean;
249
+ /** Whether `row` passes column `col`'s filter given the user's `state`. */
250
+ declare function rowMatches<T>(col: DataTableColumn<T>, row: T, state: FilterValue): boolean;
251
+ /** The empty filter state for a column's filter type. */
252
+ declare function defaultFilterState<T>(filter: ColumnFilter<T>): FilterValue;
253
+ /** URL-safe encoding of a filter value (`f.<column>` param), null when empty. */
254
+ declare function encodeFilterValue(v: FilterValue): string | null;
255
+ /** Inverse of {@link encodeFilterValue} given just the filter type — for
256
+ * callers that know a column's type without holding its full config. */
257
+ declare function decodeFilterValueOfType(type: FilterValue["type"], raw: string): FilterValue;
258
+ /** Inverse of {@link encodeFilterValue}, typed by the column's filter config. */
259
+ declare function decodeFilterValue<T>(filter: ColumnFilter<T>, raw: string): FilterValue;
260
+
261
+ export { type ColumnFilter as C, DataTable as D, type FilterState as F, type ServerPagination as S, type DataTableColumn as a, type DataTableProps as b, type FilterValue as c, decodeFilterValue as d, decodeFilterValueOfType as e, defaultFilterState as f, encodeFilterValue as g, rowMatches as h, isFilterActive as i, resolveFilter as r };