@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.
- package/README.md +219 -0
- package/dist/components/account-settings.d.ts +71 -0
- package/dist/components/account-settings.js +159 -0
- package/dist/components/account-settings.js.map +1 -0
- package/dist/components/alert-banner.d.ts +20 -0
- package/dist/components/alert-banner.js +39 -0
- package/dist/components/alert-banner.js.map +1 -0
- package/dist/components/amount-input.d.ts +117 -0
- package/dist/components/amount-input.js +243 -0
- package/dist/components/amount-input.js.map +1 -0
- package/dist/components/calculator.d.ts +42 -0
- package/dist/components/calculator.js +161 -0
- package/dist/components/calculator.js.map +1 -0
- package/dist/components/chart.d.ts +58 -0
- package/dist/components/chart.js +158 -0
- package/dist/components/chart.js.map +1 -0
- package/dist/components/combobox-core.d.ts +77 -0
- package/dist/components/combobox-core.js +263 -0
- package/dist/components/combobox-core.js.map +1 -0
- package/dist/components/combobox.d.ts +141 -0
- package/dist/components/combobox.js +550 -0
- package/dist/components/combobox.js.map +1 -0
- package/dist/components/currency-select.d.ts +37 -0
- package/dist/components/currency-select.js +131 -0
- package/dist/components/currency-select.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +28 -0
- package/dist/components/data-table-filter-popover.js +201 -0
- package/dist/components/data-table-filter-popover.js.map +1 -0
- package/dist/components/data-table-filters.d.ts +5 -0
- package/dist/components/data-table-filters.js +118 -0
- package/dist/components/data-table-filters.js.map +1 -0
- package/dist/components/data-table-labels.d.ts +33 -0
- package/dist/components/data-table-labels.js +53 -0
- package/dist/components/data-table-labels.js.map +1 -0
- package/dist/components/data-table-pagination.d.ts +18 -0
- package/dist/components/data-table-pagination.js +92 -0
- package/dist/components/data-table-pagination.js.map +1 -0
- package/dist/components/data-table-sort.d.ts +23 -0
- package/dist/components/data-table-sort.js +55 -0
- package/dist/components/data-table-sort.js.map +1 -0
- package/dist/components/data-table.d.ts +5 -0
- package/dist/components/data-table.js +926 -0
- package/dist/components/data-table.js.map +1 -0
- package/dist/components/date-picker.d.ts +85 -0
- package/dist/components/date-picker.js +253 -0
- package/dist/components/date-picker.js.map +1 -0
- package/dist/components/dropdown.d.ts +98 -0
- package/dist/components/dropdown.js +122 -0
- package/dist/components/dropdown.js.map +1 -0
- package/dist/components/entity-combobox.d.ts +49 -0
- package/dist/components/entity-combobox.js +115 -0
- package/dist/components/entity-combobox.js.map +1 -0
- package/dist/components/file-dropzone.d.ts +18 -0
- package/dist/components/file-dropzone.js +101 -0
- package/dist/components/file-dropzone.js.map +1 -0
- package/dist/components/full-bleed-dialog.d.ts +57 -0
- package/dist/components/full-bleed-dialog.js +68 -0
- package/dist/components/full-bleed-dialog.js.map +1 -0
- package/dist/components/grouped-picker.d.ts +37 -0
- package/dist/components/grouped-picker.js +87 -0
- package/dist/components/grouped-picker.js.map +1 -0
- package/dist/components/hover-menu.d.ts +16 -0
- package/dist/components/hover-menu.js +113 -0
- package/dist/components/hover-menu.js.map +1 -0
- package/dist/components/mini-calendar.d.ts +33 -0
- package/dist/components/mini-calendar.js +133 -0
- package/dist/components/mini-calendar.js.map +1 -0
- package/dist/components/modal.d.ts +76 -0
- package/dist/components/modal.js +156 -0
- package/dist/components/modal.js.map +1 -0
- package/dist/components/multi-entity-combobox.d.ts +47 -0
- package/dist/components/multi-entity-combobox.js +116 -0
- package/dist/components/multi-entity-combobox.js.map +1 -0
- package/dist/components/multi-select.d.ts +39 -0
- package/dist/components/multi-select.js +132 -0
- package/dist/components/multi-select.js.map +1 -0
- package/dist/components/number-input.d.ts +74 -0
- package/dist/components/number-input.js +114 -0
- package/dist/components/number-input.js.map +1 -0
- package/dist/components/numpad-sheet.d.ts +45 -0
- package/dist/components/numpad-sheet.js +102 -0
- package/dist/components/numpad-sheet.js.map +1 -0
- package/dist/components/picker-sheet.d.ts +44 -0
- package/dist/components/picker-sheet.js +78 -0
- package/dist/components/picker-sheet.js.map +1 -0
- package/dist/components/popover.d.ts +20 -0
- package/dist/components/popover.js +46 -0
- package/dist/components/popover.js.map +1 -0
- package/dist/components/search-field.d.ts +52 -0
- package/dist/components/search-field.js +64 -0
- package/dist/components/search-field.js.map +1 -0
- package/dist/components/settings-fields.d.ts +39 -0
- package/dist/components/settings-fields.js +17 -0
- package/dist/components/settings-fields.js.map +1 -0
- package/dist/components/swipeable-row.d.ts +60 -0
- package/dist/components/swipeable-row.js +115 -0
- package/dist/components/swipeable-row.js.map +1 -0
- package/dist/components/toggle-group.d.ts +34 -0
- package/dist/components/toggle-group.js +90 -0
- package/dist/components/toggle-group.js.map +1 -0
- package/dist/components/tooltip.d.ts +86 -0
- package/dist/components/tooltip.js +171 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/components/ui.d.ts +239 -0
- package/dist/components/ui.js +512 -0
- package/dist/components/ui.js.map +1 -0
- package/dist/components/use-mobile-reveal.d.ts +31 -0
- package/dist/components/use-mobile-reveal.js +33 -0
- package/dist/components/use-mobile-reveal.js.map +1 -0
- package/dist/components/use-table-state.d.ts +54 -0
- package/dist/components/use-table-state.js +153 -0
- package/dist/components/use-table-state.js.map +1 -0
- package/dist/components/user-avatar.d.ts +23 -0
- package/dist/components/user-avatar.js +33 -0
- package/dist/components/user-avatar.js.map +1 -0
- package/dist/components/wizard-stepper.d.ts +14 -0
- package/dist/components/wizard-stepper.js +27 -0
- package/dist/components/wizard-stepper.js.map +1 -0
- package/dist/data-table-filters-aXyf0Xub.d.ts +261 -0
- package/dist/feedback/feedback-attachment.d.ts +63 -0
- package/dist/feedback/feedback-attachment.js +160 -0
- package/dist/feedback/feedback-attachment.js.map +1 -0
- package/dist/feedback/feedback-dialog.d.ts +72 -0
- package/dist/feedback/feedback-dialog.js +109 -0
- package/dist/feedback/feedback-dialog.js.map +1 -0
- package/dist/feedback/feedback-inbox.d.ts +226 -0
- package/dist/feedback/feedback-inbox.js +315 -0
- package/dist/feedback/feedback-inbox.js.map +1 -0
- package/dist/hooks/use-anchored-panel.d.ts +80 -0
- package/dist/hooks/use-anchored-panel.js +71 -0
- package/dist/hooks/use-anchored-panel.js.map +1 -0
- package/dist/hooks/use-anchored-rect.d.ts +24 -0
- package/dist/hooks/use-anchored-rect.js +40 -0
- package/dist/hooks/use-anchored-rect.js.map +1 -0
- package/dist/hooks/use-body-scroll-lock.d.ts +9 -0
- package/dist/hooks/use-body-scroll-lock.js +41 -0
- package/dist/hooks/use-body-scroll-lock.js.map +1 -0
- package/dist/hooks/use-close-transition.d.ts +41 -0
- package/dist/hooks/use-close-transition.js +36 -0
- package/dist/hooks/use-close-transition.js.map +1 -0
- package/dist/hooks/use-dismiss.d.ts +34 -0
- package/dist/hooks/use-dismiss.js +41 -0
- package/dist/hooks/use-dismiss.js.map +1 -0
- package/dist/hooks/use-media-query.d.ts +8 -0
- package/dist/hooks/use-media-query.js +18 -0
- package/dist/hooks/use-media-query.js.map +1 -0
- package/dist/hooks/use-overlay-history.d.ts +10 -0
- package/dist/hooks/use-overlay-history.js +99 -0
- package/dist/hooks/use-overlay-history.js.map +1 -0
- package/dist/hooks/use-row-swipe.d.ts +30 -0
- package/dist/hooks/use-row-swipe.js +128 -0
- package/dist/hooks/use-row-swipe.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +90 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/calc.d.ts +89 -0
- package/dist/lib/calc.js +180 -0
- package/dist/lib/calc.js.map +1 -0
- package/dist/lib/cn.d.ts +6 -0
- package/dist/lib/cn.js +7 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/lib/dates.d.ts +84 -0
- package/dist/lib/dates.js +108 -0
- package/dist/lib/dates.js.map +1 -0
- package/dist/lib/logger.d.ts +17 -0
- package/dist/lib/logger.js +30 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/safe-storage.d.ts +23 -0
- package/dist/lib/safe-storage.js +20 -0
- package/dist/lib/safe-storage.js.map +1 -0
- package/dist/search/command-palette.d.ts +71 -0
- package/dist/search/command-palette.js +198 -0
- package/dist/search/command-palette.js.map +1 -0
- package/dist/shell/app-shell.d.ts +64 -0
- package/dist/shell/app-shell.js +237 -0
- package/dist/shell/app-shell.js.map +1 -0
- package/dist/shell/option-switcher-menu.d.ts +24 -0
- package/dist/shell/option-switcher-menu.js +44 -0
- package/dist/shell/option-switcher-menu.js.map +1 -0
- package/dist/shell/role-switcher.d.ts +19 -0
- package/dist/shell/role-switcher.js +28 -0
- package/dist/shell/role-switcher.js.map +1 -0
- package/dist/shell/top-bar.d.ts +15 -0
- package/dist/shell/top-bar.js +29 -0
- package/dist/shell/top-bar.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +44 -0
- package/dist/shell/topbar-action-menu.js +49 -0
- package/dist/shell/topbar-action-menu.js.map +1 -0
- package/dist/shell/topbar-controls.d.ts +54 -0
- package/dist/shell/topbar-controls.js +152 -0
- package/dist/shell/topbar-controls.js.map +1 -0
- package/dist/theme/chart-palette.d.ts +60 -0
- package/dist/theme/chart-palette.js +79 -0
- package/dist/theme/chart-palette.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +39 -0
- package/dist/theme/palette-presets.js +440 -0
- package/dist/theme/palette-presets.js.map +1 -0
- package/dist/theme/palette-store.d.ts +45 -0
- package/dist/theme/palette-store.js +52 -0
- package/dist/theme/palette-store.js.map +1 -0
- package/dist/theme/theme-store.d.ts +57 -0
- package/dist/theme/theme-store.js +96 -0
- package/dist/theme/theme-store.js.map +1 -0
- package/dist/tour/tour.d.ts +104 -0
- package/dist/tour/tour.js +351 -0
- package/dist/tour/tour.js.map +1 -0
- package/dist/wizard/stepper-nav.d.ts +30 -0
- package/dist/wizard/stepper-nav.js +146 -0
- package/dist/wizard/stepper-nav.js.map +1 -0
- package/dist/wizard/types.d.ts +124 -0
- package/dist/wizard/types.js +24 -0
- package/dist/wizard/types.js.map +1 -0
- package/dist/wizard/use-wizard.d.ts +6 -0
- package/dist/wizard/use-wizard.js +222 -0
- package/dist/wizard/use-wizard.js.map +1 -0
- package/dist/wizard/validation.d.ts +23 -0
- package/dist/wizard/validation.js +13 -0
- package/dist/wizard/validation.js.map +1 -0
- package/dist/wizard/wizard-context.d.ts +19 -0
- package/dist/wizard/wizard-context.js +15 -0
- package/dist/wizard/wizard-context.js.map +1 -0
- package/dist/wizard/wizard-step.d.ts +11 -0
- package/dist/wizard/wizard-step.js +12 -0
- package/dist/wizard/wizard-step.js.map +1 -0
- package/dist/wizard/wizard-summary.d.ts +18 -0
- package/dist/wizard/wizard-summary.js +41 -0
- package/dist/wizard/wizard-summary.js.map +1 -0
- package/package.json +87 -0
- 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":"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","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 };
|