@protobase/ui 0.0.0-stage → 0.1.1
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/LICENSE +202 -0
- package/dist/activity-chart/activity-chart.js +125 -0
- package/dist/activity-chart/activity-math.js +11 -0
- package/dist/activity-chart/activity-tooltip.js +24 -0
- package/dist/app/action-icon.js +10 -0
- package/dist/app/app.d.ts +24 -0
- package/dist/app/app.js +123 -0
- package/dist/app/batch-failure.js +21 -0
- package/dist/app/breadcrumb-for.js +27 -0
- package/dist/app/create/create-page.js +118 -0
- package/dist/app/create/create-record-form.js +68 -0
- package/dist/app/create/create-values.js +52 -0
- package/dist/app/delete/dialogs.js +109 -0
- package/dist/app/delete/record-diff.js +17 -0
- package/dist/app/delete/use-delete-flow.js +193 -0
- package/dist/app/delete/use-restore.js +9 -0
- package/dist/app/draft-storage.js +18 -0
- package/dist/app/error-banner.js +28 -0
- package/dist/app/field-editor.js +88 -0
- package/dist/app/icons.js +27 -0
- package/dist/app/index.d.ts +2 -0
- package/dist/app/index.html +12 -0
- package/dist/app/list-chart.js +36 -0
- package/dist/app/list-order.js +15 -0
- package/dist/app/list-page.js +213 -0
- package/dist/app/list-search.js +30 -0
- package/dist/app/main.d.ts +1 -0
- package/dist/app/main.js +14 -0
- package/dist/app/menu-icons.js +21 -0
- package/dist/app/meta-gate.js +46 -0
- package/dist/app/nav-from-meta.js +65 -0
- package/dist/app/nav-recent.js +33 -0
- package/dist/app/pages/action-href.js +7 -0
- package/dist/app/pages/blocks/action.js +38 -0
- package/dist/app/pages/blocks/block-props.js +5 -0
- package/dist/app/pages/blocks/card-frame.js +34 -0
- package/dist/app/pages/blocks/card-row.js +55 -0
- package/dist/app/pages/blocks/card.js +19 -0
- package/dist/app/pages/blocks/field.js +45 -0
- package/dist/app/pages/blocks/grid.js +34 -0
- package/dist/app/pages/blocks/link.js +27 -0
- package/dist/app/pages/blocks/modal-form.js +149 -0
- package/dist/app/pages/blocks/page-block.js +20 -0
- package/dist/app/pages/blocks/progress.js +54 -0
- package/dist/app/pages/blocks/record-card.js +56 -0
- package/dist/app/pages/blocks/show.js +43 -0
- package/dist/app/pages/blocks/stat.js +78 -0
- package/dist/app/pages/blocks/table.js +65 -0
- package/dist/app/pages/composed-page.js +19 -0
- package/dist/app/pages/confirm-action.js +20 -0
- package/dist/app/pages/field-value.js +58 -0
- package/dist/app/pages/project-ui.d.ts +30 -0
- package/dist/app/pages/project-ui.js +13 -0
- package/dist/app/pages/record-actions.js +40 -0
- package/dist/app/pages/record-scope.d.ts +16 -0
- package/dist/app/pages/record-scope.js +12 -0
- package/dist/app/pages/record-table.js +110 -0
- package/dist/app/pages/render-node.js +84 -0
- package/dist/app/pages/use-layout-data.js +73 -0
- package/dist/app/pages/use-path-value.js +65 -0
- package/dist/app/pages/use-relation-labels.js +30 -0
- package/dist/app/pages/use-run-action.js +88 -0
- package/dist/app/permissions.js +15 -0
- package/dist/app/record-lines.js +39 -0
- package/dist/app/record-page.js +318 -0
- package/dist/app/related-section.js +126 -0
- package/dist/app/relation-picker.js +69 -0
- package/dist/app/router.js +155 -0
- package/dist/app/search/search-model.js +70 -0
- package/dist/app/search/use-global-search.js +68 -0
- package/dist/app/sensitive-field.js +65 -0
- package/dist/app/use-lines-draft.js +45 -0
- package/dist/app/use-lines.js +19 -0
- package/dist/app/use-live-save.js +54 -0
- package/dist/app/use-nav-recent.js +41 -0
- package/dist/app/use-record-editor.js +154 -0
- package/dist/app/use-related-records.js +31 -0
- package/dist/app/user-menu-from-meta.js +36 -0
- package/dist/app-shell/app-shell.d.ts +4 -0
- package/dist/app-shell/app-shell.js +57 -0
- package/dist/app-shell/breadcrumb.js +37 -0
- package/dist/app-shell/drawer.js +30 -0
- package/dist/app-shell/global-search.d.ts +1 -0
- package/dist/app-shell/global-search.js +145 -0
- package/dist/app-shell/index.d.ts +9 -0
- package/dist/app-shell/logo.js +46 -0
- package/dist/app-shell/nav-open-storage.js +10 -0
- package/dist/app-shell/nav.d.ts +2 -0
- package/dist/app-shell/nav.js +85 -0
- package/dist/app-shell/page-header.d.ts +1 -0
- package/dist/app-shell/page-header.js +17 -0
- package/dist/app-shell/profile-menu.d.ts +2 -0
- package/dist/app-shell/profile-menu.js +115 -0
- package/dist/app-shell/sidebar-group.js +94 -0
- package/dist/app-shell/sidebar-item.js +46 -0
- package/dist/app-shell/sidebar-mode.d.ts +4 -0
- package/dist/app-shell/sidebar-mode.js +13 -0
- package/dist/app-shell/sidebar.d.ts +2 -0
- package/dist/app-shell/sidebar.js +64 -0
- package/dist/app-shell/status-dot.d.ts +1 -0
- package/dist/app-shell/status-dot.js +20 -0
- package/dist/app-shell/top-bar.js +82 -0
- package/dist/assistant-dock/assistant-dock.d.ts +15 -0
- package/dist/assistant-dock/assistant-dock.js +81 -0
- package/dist/assistant-dock/chat-message.js +9 -0
- package/dist/assistant-dock/composer.js +39 -0
- package/dist/assistant-dock/dock-item.d.ts +4 -0
- package/dist/assistant-dock/dock-item.js +37 -0
- package/dist/assistant-dock/model.d.ts +84 -0
- package/dist/assistant-dock/model.js +26 -0
- package/dist/assistant-dock/plan-card.d.ts +13 -0
- package/dist/assistant-dock/plan-card.js +133 -0
- package/dist/assistant-dock/progress-card.js +18 -0
- package/dist/assistant-dock/query-card.js +52 -0
- package/dist/assistant-dock/queue-card.js +31 -0
- package/dist/assistant-dock/result-card.js +77 -0
- package/dist/assistant-panel/content.d.ts +7 -0
- package/dist/assistant-panel/step-list.js +30 -0
- package/dist/auth/auth-gate.js +70 -0
- package/dist/auth/auth-layout.js +29 -0
- package/dist/auth/auth-provider.js +71 -0
- package/dist/auth/first-run-page.js +51 -0
- package/dist/auth/forgot-password-form.js +75 -0
- package/dist/auth/forgot-password-page.js +35 -0
- package/dist/auth/reset-link.js +29 -0
- package/dist/auth/reset-password-form.js +88 -0
- package/dist/auth/reset-password-page.js +43 -0
- package/dist/auth/sign-in-form.js +90 -0
- package/dist/auth/sign-in-page.js +39 -0
- package/dist/auth/user-to-shell.js +18 -0
- package/dist/data/api-provider.d.ts +54 -0
- package/dist/data/api-provider.js +53 -0
- package/dist/data/meta-data.d.ts +14 -0
- package/dist/data/meta-data.js +9 -0
- package/dist/data/query-keys.js +95 -0
- package/dist/data/use-aggregates.d.ts +8 -0
- package/dist/data/use-aggregates.js +43 -0
- package/dist/data/use-list.d.ts +231 -0
- package/dist/data/use-list.js +31 -0
- package/dist/data/use-meta.d.ts +8 -0
- package/dist/data/use-meta.js +35 -0
- package/dist/data/use-record.d.ts +6 -0
- package/dist/data/use-record.js +16 -0
- package/dist/data/use-update-record.d.ts +15 -0
- package/dist/data/use-update-record.js +26 -0
- package/dist/data-grid/cells.js +85 -0
- package/dist/data-grid/column-spec.js +4 -0
- package/dist/data-grid/columns.js +16 -0
- package/dist/data-grid/data-grid.js +238 -0
- package/dist/data-grid/geometry.js +32 -0
- package/dist/data-grid/grid-footer.js +33 -0
- package/dist/data-grid/grid-header.js +69 -0
- package/dist/data-grid/grid-row.js +83 -0
- package/dist/data-grid/grid-template.js +12 -0
- package/dist/data-grid/header-menu.js +52 -0
- package/dist/data-grid/page-cache.js +27 -0
- package/dist/data-grid/pinned-storage.js +13 -0
- package/dist/data-grid/row-menu.js +88 -0
- package/dist/data-grid/use-grid-scroll.js +80 -0
- package/dist/data-grid/use-horizontal-scroll.js +38 -0
- package/dist/data-grid/use-paged-rows.js +50 -0
- package/dist/data-grid/use-pinned-columns.js +16 -0
- package/dist/filter-panel/active-filter-chips.js +34 -0
- package/dist/filter-panel/date-presets.js +20 -0
- package/dist/filter-panel/facet-group.js +48 -0
- package/dist/filter-panel/filter-config.js +18 -0
- package/dist/filter-panel/filter-menu.js +41 -0
- package/dist/filter-panel/filter-panel.js +112 -0
- package/dist/filter-panel/filter-section.js +11 -0
- package/dist/filter-panel/filter-state.js +63 -0
- package/dist/filter-panel/range-filter.js +54 -0
- package/dist/filter-panel/range-math.js +11 -0
- package/dist/filter-panel/range-slider.css +32 -0
- package/dist/filter-panel/toggle-row.js +13 -0
- package/dist/filter-panel/use-filter-state.js +15 -0
- package/dist/format/date.js +37 -0
- package/dist/format/money.js +15 -0
- package/dist/format/number.js +12 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +11 -0
- package/dist/inline-child-table/columns.js +4 -0
- package/dist/inline-child-table/inline-child-table.js +120 -0
- package/dist/inline-child-table/line-menu.js +54 -0
- package/dist/inline-child-table/line-row.js +91 -0
- package/dist/inline-child-table/number-cell.js +25 -0
- package/dist/inline-child-table/reorder.js +10 -0
- package/dist/inline-child-table/totals-footer.js +42 -0
- package/dist/lib/cn.js +4 -0
- package/dist/lib/guarded.js +12 -0
- package/dist/lib/use-debounced.js +13 -0
- package/dist/lib/use-focus-trap.js +46 -0
- package/dist/lib/use-media-query.js +12 -0
- package/dist/live/columns-from-model.js +93 -0
- package/dist/live/display-value.js +24 -0
- package/dist/live/field-values.js +32 -0
- package/dist/live/filter-string.js +135 -0
- package/dist/live/grid-loader.js +63 -0
- package/dist/live/model-helpers.js +30 -0
- package/dist/live/naming.js +9 -0
- package/dist/live/relation-labels.js +53 -0
- package/dist/live/use-filter-config.js +88 -0
- package/dist/live/use-live-rows.js +45 -0
- package/dist/live/view-fields.js +35 -0
- package/dist/mocks/activity.js +24 -0
- package/dist/mocks/invoices.js +13 -0
- package/dist/mocks/products.js +167 -0
- package/dist/mocks/rng.js +20 -0
- package/dist/mocks/users.js +63 -0
- package/dist/primitives/avatar.js +22 -0
- package/dist/primitives/badge.d.ts +1 -0
- package/dist/primitives/badge.js +25 -0
- package/dist/primitives/button.js +23 -0
- package/dist/primitives/checkbox.js +21 -0
- package/dist/primitives/dialog.js +41 -0
- package/dist/primitives/input.js +16 -0
- package/dist/primitives/kbd.js +8 -0
- package/dist/primitives/segmented.js +14 -0
- package/dist/primitives/sheet.js +44 -0
- package/dist/primitives/skeleton.js +6 -0
- package/dist/primitives/spinner.js +10 -0
- package/dist/primitives/switch.js +14 -0
- package/dist/primitives/tooltip.js +13 -0
- package/dist/record-view/action-rail.js +28 -0
- package/dist/record-view/field.js +62 -0
- package/dist/record-view/record-header.js +50 -0
- package/dist/record-view/record-sidebar.js +58 -0
- package/dist/record-view/section.js +21 -0
- package/dist/styles.css +132 -0
- package/dist/toasts/toast-provider.js +69 -0
- package/dist/toasts/toast.js +45 -0
- package/package.json +45 -3
- package/README.md +0 -3
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { clampTop, gridGeometry, scrollFromTop, topFromScroll } from "./geometry.js";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
//#region src/data-grid/use-grid-scroll.ts
|
|
4
|
+
/**
|
|
5
|
+
* Owns the scroll position of a grid whose true height may exceed what browsers can scroll.
|
|
6
|
+
* The native scroller is a proxy: its position is mapped onto a virtual pixel offset `top`.
|
|
7
|
+
*/
|
|
8
|
+
const useGridScroll = (total, rowHeight, initialIndex) => {
|
|
9
|
+
const ref = useRef(null);
|
|
10
|
+
const [viewport, setViewport] = useState(0);
|
|
11
|
+
const [top, setTop] = useState(initialIndex * rowHeight);
|
|
12
|
+
const geometry = useMemo(() => gridGeometry(total, rowHeight, viewport), [
|
|
13
|
+
total,
|
|
14
|
+
rowHeight,
|
|
15
|
+
viewport
|
|
16
|
+
]);
|
|
17
|
+
const latest = useRef({
|
|
18
|
+
geometry,
|
|
19
|
+
top
|
|
20
|
+
});
|
|
21
|
+
latest.current = {
|
|
22
|
+
geometry,
|
|
23
|
+
top
|
|
24
|
+
};
|
|
25
|
+
const ignoreScrollUntil = useRef(0);
|
|
26
|
+
const positioned = useRef(false);
|
|
27
|
+
const syncNative = useCallback((nextTop) => {
|
|
28
|
+
const element = ref.current;
|
|
29
|
+
if (!element) return;
|
|
30
|
+
element.scrollTop = scrollFromTop(latest.current.geometry, nextTop);
|
|
31
|
+
ignoreScrollUntil.current = performance.now() + 150;
|
|
32
|
+
}, []);
|
|
33
|
+
useLayoutEffect(() => {
|
|
34
|
+
const element = ref.current;
|
|
35
|
+
if (!element) return;
|
|
36
|
+
const observer = new ResizeObserver(() => setViewport(element.clientHeight));
|
|
37
|
+
observer.observe(element);
|
|
38
|
+
setViewport(element.clientHeight);
|
|
39
|
+
return () => observer.disconnect();
|
|
40
|
+
}, []);
|
|
41
|
+
useLayoutEffect(() => {
|
|
42
|
+
if (viewport === 0 || positioned.current) return;
|
|
43
|
+
positioned.current = true;
|
|
44
|
+
const next = clampTop(geometry, top);
|
|
45
|
+
setTop(next);
|
|
46
|
+
syncNative(next);
|
|
47
|
+
}, [
|
|
48
|
+
viewport,
|
|
49
|
+
geometry,
|
|
50
|
+
top,
|
|
51
|
+
syncNative
|
|
52
|
+
]);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const element = ref.current;
|
|
55
|
+
if (!element) return;
|
|
56
|
+
const onWheel = (event) => {
|
|
57
|
+
const { geometry: current, top: currentTop } = latest.current;
|
|
58
|
+
if (!current.scaled || Math.abs(event.deltaX) > Math.abs(event.deltaY)) return;
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
const next = clampTop(current, currentTop + event.deltaY);
|
|
61
|
+
setTop(next);
|
|
62
|
+
syncNative(next);
|
|
63
|
+
};
|
|
64
|
+
element.addEventListener("wheel", onWheel, { passive: false });
|
|
65
|
+
return () => element.removeEventListener("wheel", onWheel);
|
|
66
|
+
}, [syncNative]);
|
|
67
|
+
return {
|
|
68
|
+
ref,
|
|
69
|
+
onScroll: useCallback(() => {
|
|
70
|
+
const element = ref.current;
|
|
71
|
+
if (!element || performance.now() < ignoreScrollUntil.current) return;
|
|
72
|
+
setTop(topFromScroll(latest.current.geometry, element.scrollTop));
|
|
73
|
+
}, []),
|
|
74
|
+
viewport,
|
|
75
|
+
top,
|
|
76
|
+
geometry
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
//#endregion
|
|
80
|
+
export { useGridScroll };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from "react";
|
|
2
|
+
//#region src/data-grid/use-horizontal-scroll.ts
|
|
3
|
+
/** Tracks whether the grid is scrolled sideways (`scrolled`) and whether more lies to the right (`more`). */
|
|
4
|
+
const useHorizontalScroll = () => {
|
|
5
|
+
const ref = useRef(null);
|
|
6
|
+
const observer = useRef(null);
|
|
7
|
+
const [edges, setEdges] = useState({
|
|
8
|
+
scrolled: false,
|
|
9
|
+
more: false
|
|
10
|
+
});
|
|
11
|
+
const measure = useCallback(() => {
|
|
12
|
+
const element = ref.current;
|
|
13
|
+
if (!element) return;
|
|
14
|
+
const next = {
|
|
15
|
+
scrolled: element.scrollLeft > 1,
|
|
16
|
+
more: element.scrollLeft + element.clientWidth < element.scrollWidth - 1
|
|
17
|
+
};
|
|
18
|
+
setEdges((current) => current.scrolled === next.scrolled && current.more === next.more ? current : next);
|
|
19
|
+
}, []);
|
|
20
|
+
return {
|
|
21
|
+
ref,
|
|
22
|
+
attach: useCallback((element) => {
|
|
23
|
+
observer.current?.disconnect();
|
|
24
|
+
observer.current = null;
|
|
25
|
+
ref.current = element;
|
|
26
|
+
if (!element) return;
|
|
27
|
+
const next = new ResizeObserver(measure);
|
|
28
|
+
next.observe(element);
|
|
29
|
+
for (const child of Array.from(element.children)) next.observe(child);
|
|
30
|
+
observer.current = next;
|
|
31
|
+
measure();
|
|
32
|
+
}, [measure]),
|
|
33
|
+
onScroll: measure,
|
|
34
|
+
...edges
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
export { useHorizontalScroll };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createPageCache, pagesForRange } from "./page-cache.js";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Serves rows by absolute index. Mock sources compute rows (after `latencyMs`, or at once when 0);
|
|
5
|
+
* live sources load pages through `loadRows`. Unloaded rows read as undefined, so fast scrolling shows skeletons.
|
|
6
|
+
*/
|
|
7
|
+
const usePagedRows = ({ total, rowAt, loadRows, latencyMs }, start, end) => {
|
|
8
|
+
const cache = useMemo(() => createPageCache(40, 60), [rowAt, loadRows]);
|
|
9
|
+
const [version, setVersion] = useState(0);
|
|
10
|
+
const firstPage = Math.floor(start / 40);
|
|
11
|
+
const lastPage = Math.floor(end / 40);
|
|
12
|
+
const synchronous = !loadRows && latencyMs === 0;
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (synchronous) return;
|
|
15
|
+
const pages = pagesForRange(firstPage * 40, lastPage * 40, 40).filter((page) => !cache.hasPage(page));
|
|
16
|
+
const length = (page) => Math.max(0, Math.min(40, total - page * 40));
|
|
17
|
+
const store = (page, rows) => {
|
|
18
|
+
cache.setPage(page, rows);
|
|
19
|
+
setVersion((current) => current + 1);
|
|
20
|
+
};
|
|
21
|
+
if (loadRows) {
|
|
22
|
+
let cancelled = false;
|
|
23
|
+
for (const page of pages) loadRows(page * 40, length(page)).then((rows) => {
|
|
24
|
+
if (!cancelled) store(page, rows);
|
|
25
|
+
});
|
|
26
|
+
return () => {
|
|
27
|
+
cancelled = true;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const timers = pages.map((page) => setTimeout(() => store(page, Array.from({ length: length(page) }, (_, offset) => rowAt(page * 40 + offset))), latencyMs));
|
|
31
|
+
return () => timers.forEach(clearTimeout);
|
|
32
|
+
}, [
|
|
33
|
+
cache,
|
|
34
|
+
firstPage,
|
|
35
|
+
lastPage,
|
|
36
|
+
latencyMs,
|
|
37
|
+
total,
|
|
38
|
+
rowAt,
|
|
39
|
+
loadRows,
|
|
40
|
+
synchronous
|
|
41
|
+
]);
|
|
42
|
+
return useCallback((index) => synchronous ? rowAt(index) : cache.get(index), [
|
|
43
|
+
cache,
|
|
44
|
+
synchronous,
|
|
45
|
+
rowAt,
|
|
46
|
+
version
|
|
47
|
+
]);
|
|
48
|
+
};
|
|
49
|
+
//#endregion
|
|
50
|
+
export { usePagedRows };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readPinned, writePinned } from "./pinned-storage.js";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
//#region src/data-grid/use-pinned-columns.ts
|
|
4
|
+
/**
|
|
5
|
+
* The ids of the columns pinned to the left, in pin order. With a `storageKey` the choice is kept in local storage
|
|
6
|
+
* (per user and resource); `initial` applies until the user has chosen.
|
|
7
|
+
*/
|
|
8
|
+
const usePinnedColumns = (storageKey, initial = []) => {
|
|
9
|
+
const [pinned, setPinned] = useState(() => (storageKey ? readPinned(localStorage, storageKey) : void 0) ?? initial);
|
|
10
|
+
return [pinned, useCallback((next) => {
|
|
11
|
+
setPinned(next);
|
|
12
|
+
if (storageKey) writePinned(localStorage, storageKey, next);
|
|
13
|
+
}, [storageKey])];
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { usePinnedColumns };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Lock, X } from "lucide-react";
|
|
3
|
+
//#region src/filter-panel/active-filter-chips.tsx
|
|
4
|
+
const chipClass = "inline-flex h-6 items-center gap-1 rounded-full border border-primary-border bg-primary-soft pl-2.5 pr-1.5 text-xs font-medium text-primary-text";
|
|
5
|
+
const ActiveFilterChips = ({ chips, lockedLabels = [], onRemove, onClear, scroll }) => {
|
|
6
|
+
if (chips.length === 0 && lockedLabels.length === 0) return null;
|
|
7
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
8
|
+
className: scroll ? "flex flex-nowrap items-center gap-1.5 overflow-x-auto pb-1 [&>*]:shrink-0" : "flex flex-wrap items-center gap-1.5",
|
|
9
|
+
children: [
|
|
10
|
+
lockedLabels.map((label) => /* @__PURE__ */ jsxs("span", {
|
|
11
|
+
className: "inline-flex h-6 items-center gap-1.5 rounded-full border border-border-strong bg-muted px-2.5 text-xs font-medium text-foreground",
|
|
12
|
+
children: [/* @__PURE__ */ jsx(Lock, { className: "size-3 text-muted-foreground" }), label]
|
|
13
|
+
}, label)),
|
|
14
|
+
chips.map((chip) => /* @__PURE__ */ jsxs("span", {
|
|
15
|
+
className: chipClass,
|
|
16
|
+
children: [chip.label, /* @__PURE__ */ jsx("button", {
|
|
17
|
+
type: "button",
|
|
18
|
+
"aria-label": `Remove ${chip.label}`,
|
|
19
|
+
onClick: () => onRemove(chip.key),
|
|
20
|
+
className: "rounded-full p-0.5 hover:bg-primary-border/60",
|
|
21
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3" })
|
|
22
|
+
})]
|
|
23
|
+
}, chip.key)),
|
|
24
|
+
chips.length > 0 && /* @__PURE__ */ jsx("button", {
|
|
25
|
+
type: "button",
|
|
26
|
+
onClick: onClear,
|
|
27
|
+
className: "ml-1 text-xs font-medium text-muted-foreground hover:text-foreground",
|
|
28
|
+
children: "Clear all"
|
|
29
|
+
})
|
|
30
|
+
]
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { ActiveFilterChips };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { datePresetLabels, defaultDatePresets } from "./filter-config.js";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
//#region src/filter-panel/date-presets.tsx
|
|
5
|
+
const DatePresets = ({ value, presets = defaultDatePresets, onChange }) => /* @__PURE__ */ jsx("div", {
|
|
6
|
+
className: "flex flex-wrap gap-1.5",
|
|
7
|
+
children: presets.map((id) => {
|
|
8
|
+
const label = datePresetLabels[id];
|
|
9
|
+
return /* @__PURE__ */ jsx("button", {
|
|
10
|
+
type: "button",
|
|
11
|
+
disabled: !label,
|
|
12
|
+
title: label ? void 0 : `The server offers the preset "${id}", which this UI does not know`,
|
|
13
|
+
onClick: () => onChange(value === id ? void 0 : id),
|
|
14
|
+
className: cn("h-7 rounded-md border px-2.5 text-xs font-medium transition-colors", !label && "border-dashed text-danger-text", value === id ? "border-primary-border bg-primary-soft text-primary-text" : "border-border-strong bg-background text-muted-foreground hover:bg-muted"),
|
|
15
|
+
children: label ?? `${id} (unsupported)`
|
|
16
|
+
}, id);
|
|
17
|
+
})
|
|
18
|
+
});
|
|
19
|
+
//#endregion
|
|
20
|
+
export { DatePresets };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Input } from "../primitives/input.js";
|
|
2
|
+
import { formatInt } from "../format/number.js";
|
|
3
|
+
import { Checkbox } from "../primitives/checkbox.js";
|
|
4
|
+
import { matchesSearch } from "./filter-state.js";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { Search } from "lucide-react";
|
|
8
|
+
//#region src/filter-panel/facet-group.tsx
|
|
9
|
+
const FacetGroup = ({ config, selected, onToggle }) => {
|
|
10
|
+
const [query, setQuery] = useState("");
|
|
11
|
+
const options = config.options.filter((option) => matchesSearch(option.label, query));
|
|
12
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
13
|
+
className: "flex flex-col gap-1",
|
|
14
|
+
children: [
|
|
15
|
+
config.searchable && /* @__PURE__ */ jsx(Input, {
|
|
16
|
+
wrapperClassName: "mb-1 h-7",
|
|
17
|
+
leading: /* @__PURE__ */ jsx(Search, { className: "size-3.5" }),
|
|
18
|
+
placeholder: `Search ${config.label.toLowerCase()}`,
|
|
19
|
+
value: query,
|
|
20
|
+
onChange: (event) => setQuery(event.target.value)
|
|
21
|
+
}),
|
|
22
|
+
options.map((option) => /* @__PURE__ */ jsxs("label", {
|
|
23
|
+
className: "flex cursor-pointer items-center gap-2 rounded px-1 py-1 hover:bg-muted",
|
|
24
|
+
children: [
|
|
25
|
+
/* @__PURE__ */ jsx(Checkbox, {
|
|
26
|
+
checked: selected.includes(option.value),
|
|
27
|
+
onChange: () => onToggle(option.value),
|
|
28
|
+
label: option.label
|
|
29
|
+
}),
|
|
30
|
+
/* @__PURE__ */ jsx("span", {
|
|
31
|
+
className: "flex-1 truncate",
|
|
32
|
+
children: option.label
|
|
33
|
+
}),
|
|
34
|
+
/* @__PURE__ */ jsx("span", {
|
|
35
|
+
className: "text-xs tabular-nums text-faint-foreground",
|
|
36
|
+
children: formatInt(option.count)
|
|
37
|
+
})
|
|
38
|
+
]
|
|
39
|
+
}, option.value)),
|
|
40
|
+
options.length === 0 && /* @__PURE__ */ jsx("span", {
|
|
41
|
+
className: "px-1 py-1 text-xs text-faint-foreground",
|
|
42
|
+
children: "No matches"
|
|
43
|
+
})
|
|
44
|
+
]
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
export { FacetGroup };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/filter-panel/filter-config.ts
|
|
2
|
+
const defaultDatePresets = [
|
|
3
|
+
"7d",
|
|
4
|
+
"30d",
|
|
5
|
+
"year"
|
|
6
|
+
];
|
|
7
|
+
const datePresetLabels = {
|
|
8
|
+
today: "Today",
|
|
9
|
+
yesterday: "Yesterday",
|
|
10
|
+
"7d": "Last 7 days",
|
|
11
|
+
"30d": "Last 30 days",
|
|
12
|
+
"90d": "Last 90 days",
|
|
13
|
+
month: "This month",
|
|
14
|
+
quarter: "This quarter",
|
|
15
|
+
year: "This year"
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { datePresetLabels, defaultDatePresets };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { ChevronDown } from "lucide-react";
|
|
5
|
+
//#region src/filter-panel/filter-menu.tsx
|
|
6
|
+
const FilterMenu = ({ label, activeCount, width = 240, defaultOpen = false, children }) => {
|
|
7
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
8
|
+
const root = useRef(null);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!open) return;
|
|
11
|
+
const close = (event) => {
|
|
12
|
+
if (event.target instanceof Node && !root.current?.contains(event.target)) setOpen(false);
|
|
13
|
+
};
|
|
14
|
+
document.addEventListener("pointerdown", close);
|
|
15
|
+
return () => document.removeEventListener("pointerdown", close);
|
|
16
|
+
}, [open]);
|
|
17
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18
|
+
ref: root,
|
|
19
|
+
className: "relative",
|
|
20
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
21
|
+
type: "button",
|
|
22
|
+
"aria-expanded": open,
|
|
23
|
+
onClick: () => setOpen(!open),
|
|
24
|
+
className: cn("inline-flex h-8 items-center gap-1.5 rounded-md border bg-background px-2.5 text-[13px] font-medium shadow-sm transition-colors hover:bg-muted", activeCount > 0 ? "border-primary-border text-primary-text" : "border-border-strong text-foreground"),
|
|
25
|
+
children: [
|
|
26
|
+
label,
|
|
27
|
+
activeCount > 0 && /* @__PURE__ */ jsx("span", {
|
|
28
|
+
className: "rounded-full bg-primary px-1.5 text-[11px] leading-4 text-primary-foreground",
|
|
29
|
+
children: activeCount
|
|
30
|
+
}),
|
|
31
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5 text-muted-foreground" })
|
|
32
|
+
]
|
|
33
|
+
}), open && /* @__PURE__ */ jsx("div", {
|
|
34
|
+
className: "absolute left-0 top-full z-30 mt-1.5 rounded-lg border bg-background p-3 shadow-pop",
|
|
35
|
+
style: { width },
|
|
36
|
+
children
|
|
37
|
+
})]
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { FilterMenu };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ActiveFilterChips } from "./active-filter-chips.js";
|
|
2
|
+
import { DatePresets } from "./date-presets.js";
|
|
3
|
+
import { activeChips, emptyFilterState, removeChip, toggleFacetValue, toggleFlag } from "./filter-state.js";
|
|
4
|
+
import { FacetGroup } from "./facet-group.js";
|
|
5
|
+
import { FilterMenu } from "./filter-menu.js";
|
|
6
|
+
import { FilterSection } from "./filter-section.js";
|
|
7
|
+
import { RangeFilter } from "./range-filter.js";
|
|
8
|
+
import { ToggleRow } from "./toggle-row.js";
|
|
9
|
+
import { useFilterState } from "./use-filter-state.js";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
//#region src/filter-panel/filter-panel.tsx
|
|
12
|
+
const FilterPanel = ({ config, layout, value, defaultValue, onChange, lockedLabels, defaultOpenMenu, fullWidth }) => {
|
|
13
|
+
const [state, setState] = useFilterState(value, defaultValue, onChange);
|
|
14
|
+
const chips = activeChips(config, state);
|
|
15
|
+
const chipsView = /* @__PURE__ */ jsx(ActiveFilterChips, {
|
|
16
|
+
chips,
|
|
17
|
+
lockedLabels,
|
|
18
|
+
onRemove: (key) => setState(removeChip(state, key)),
|
|
19
|
+
onClear: () => setState(emptyFilterState)
|
|
20
|
+
});
|
|
21
|
+
const facetView = (group) => /* @__PURE__ */ jsx(FacetGroup, {
|
|
22
|
+
config: group,
|
|
23
|
+
selected: state.facets[group.id] ?? [],
|
|
24
|
+
onToggle: (option) => setState(toggleFacetValue(state, group.id, option))
|
|
25
|
+
});
|
|
26
|
+
const rangeView = config.range && /* @__PURE__ */ jsx(RangeFilter, {
|
|
27
|
+
config: config.range,
|
|
28
|
+
value: state.range,
|
|
29
|
+
onChange: (range) => setState({
|
|
30
|
+
...state,
|
|
31
|
+
range
|
|
32
|
+
})
|
|
33
|
+
});
|
|
34
|
+
const dateView = config.datePresets && /* @__PURE__ */ jsx(DatePresets, {
|
|
35
|
+
value: state.date,
|
|
36
|
+
presets: Array.isArray(config.datePresets) ? config.datePresets : void 0,
|
|
37
|
+
onChange: (date) => setState({
|
|
38
|
+
...state,
|
|
39
|
+
date
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
const toggleViews = (config.toggles ?? []).map((toggle) => /* @__PURE__ */ jsx(ToggleRow, {
|
|
43
|
+
label: toggle.label,
|
|
44
|
+
checked: state.toggles.includes(toggle.id),
|
|
45
|
+
onChange: () => setState(toggleFlag(state, toggle.id))
|
|
46
|
+
}, toggle.id));
|
|
47
|
+
if (layout === "panel") return /* @__PURE__ */ jsxs("aside", {
|
|
48
|
+
className: fullWidth ? "flex w-full flex-col gap-1" : "flex w-64 shrink-0 flex-col gap-1 overflow-y-auto rounded-lg border bg-background px-4 py-3",
|
|
49
|
+
children: [
|
|
50
|
+
/* @__PURE__ */ jsx("div", {
|
|
51
|
+
className: "flex items-center justify-between pb-1",
|
|
52
|
+
children: /* @__PURE__ */ jsx("h2", {
|
|
53
|
+
className: "text-[13px] font-semibold",
|
|
54
|
+
children: "Filters"
|
|
55
|
+
})
|
|
56
|
+
}),
|
|
57
|
+
(chips.length > 0 || (lockedLabels?.length ?? 0) > 0) && /* @__PURE__ */ jsx("div", {
|
|
58
|
+
className: "border-b pb-3",
|
|
59
|
+
children: chipsView
|
|
60
|
+
}),
|
|
61
|
+
config.facets.map((group) => /* @__PURE__ */ jsx(FilterSection, {
|
|
62
|
+
title: group.label,
|
|
63
|
+
children: facetView(group)
|
|
64
|
+
}, group.id)),
|
|
65
|
+
config.range && /* @__PURE__ */ jsx(FilterSection, {
|
|
66
|
+
title: config.range.label,
|
|
67
|
+
children: rangeView
|
|
68
|
+
}),
|
|
69
|
+
dateView && /* @__PURE__ */ jsx(FilterSection, {
|
|
70
|
+
title: "Date",
|
|
71
|
+
children: dateView
|
|
72
|
+
}),
|
|
73
|
+
toggleViews.length > 0 && /* @__PURE__ */ jsx(FilterSection, {
|
|
74
|
+
title: "Show only",
|
|
75
|
+
children: toggleViews
|
|
76
|
+
})
|
|
77
|
+
]
|
|
78
|
+
});
|
|
79
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
80
|
+
className: "flex flex-col gap-2",
|
|
81
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
82
|
+
className: "flex flex-wrap items-center gap-2",
|
|
83
|
+
children: [
|
|
84
|
+
config.facets.map((group) => /* @__PURE__ */ jsx(FilterMenu, {
|
|
85
|
+
label: group.label,
|
|
86
|
+
activeCount: (state.facets[group.id] ?? []).length,
|
|
87
|
+
defaultOpen: defaultOpenMenu === group.id,
|
|
88
|
+
children: facetView(group)
|
|
89
|
+
}, group.id)),
|
|
90
|
+
config.range && /* @__PURE__ */ jsx(FilterMenu, {
|
|
91
|
+
label: config.range.label,
|
|
92
|
+
activeCount: state.range ? 1 : 0,
|
|
93
|
+
width: 280,
|
|
94
|
+
defaultOpen: defaultOpenMenu === config.range.id,
|
|
95
|
+
children: rangeView
|
|
96
|
+
}),
|
|
97
|
+
dateView,
|
|
98
|
+
config.toggles && /* @__PURE__ */ jsx("span", { className: "mx-1 h-5 w-px bg-border" }),
|
|
99
|
+
config.toggles?.map((toggle) => /* @__PURE__ */ jsx("div", {
|
|
100
|
+
className: "flex items-center gap-2 text-[13px] text-muted-foreground",
|
|
101
|
+
children: /* @__PURE__ */ jsx(ToggleRow, {
|
|
102
|
+
label: toggle.label,
|
|
103
|
+
checked: state.toggles.includes(toggle.id),
|
|
104
|
+
onChange: () => setState(toggleFlag(state, toggle.id))
|
|
105
|
+
})
|
|
106
|
+
}, toggle.id))
|
|
107
|
+
]
|
|
108
|
+
}), chipsView]
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
//#endregion
|
|
112
|
+
export { FilterPanel };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
//#region src/filter-panel/filter-section.tsx
|
|
3
|
+
const FilterSection = ({ title, children }) => /* @__PURE__ */ jsxs("section", {
|
|
4
|
+
className: "flex flex-col gap-2 border-b py-3 last:border-b-0",
|
|
5
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
6
|
+
className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground",
|
|
7
|
+
children: title
|
|
8
|
+
}), children]
|
|
9
|
+
});
|
|
10
|
+
//#endregion
|
|
11
|
+
export { FilterSection };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { datePresetLabels } from "./filter-config.js";
|
|
2
|
+
//#region src/filter-panel/filter-state.ts
|
|
3
|
+
const emptyFilterState = {
|
|
4
|
+
facets: {},
|
|
5
|
+
toggles: []
|
|
6
|
+
};
|
|
7
|
+
const toggleFacetValue = (state, group, value) => {
|
|
8
|
+
const current = state.facets[group] ?? [];
|
|
9
|
+
const next = current.includes(value) ? current.filter((entry) => entry !== value) : [...current, value];
|
|
10
|
+
return {
|
|
11
|
+
...state,
|
|
12
|
+
facets: {
|
|
13
|
+
...state.facets,
|
|
14
|
+
[group]: next
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
const toggleFlag = (state, id) => ({
|
|
19
|
+
...state,
|
|
20
|
+
toggles: state.toggles.includes(id) ? state.toggles.filter((entry) => entry !== id) : [...state.toggles, id]
|
|
21
|
+
});
|
|
22
|
+
const activeChips = (config, state) => {
|
|
23
|
+
const facetChips = config.facets.flatMap((group) => (state.facets[group.id] ?? []).map((value) => ({
|
|
24
|
+
key: `facet:${group.id}:${value}`,
|
|
25
|
+
label: `${group.label}: ${group.options.find((option) => option.value === value)?.label ?? value}`
|
|
26
|
+
})));
|
|
27
|
+
const range = config.range;
|
|
28
|
+
const rangeChips = range && state.range ? [{
|
|
29
|
+
key: "range",
|
|
30
|
+
label: `${range.label}: ${range.format(state.range[0])} – ${range.format(state.range[1])}`
|
|
31
|
+
}] : [];
|
|
32
|
+
const dateChips = state.date ? [{
|
|
33
|
+
key: "date",
|
|
34
|
+
label: datePresetLabels[state.date] ?? state.date
|
|
35
|
+
}] : [];
|
|
36
|
+
const toggleChips = state.toggles.map((id) => ({
|
|
37
|
+
key: `toggle:${id}`,
|
|
38
|
+
label: config.toggles?.find((toggle) => toggle.id === id)?.label ?? id
|
|
39
|
+
}));
|
|
40
|
+
return [
|
|
41
|
+
...facetChips,
|
|
42
|
+
...rangeChips,
|
|
43
|
+
...dateChips,
|
|
44
|
+
...toggleChips
|
|
45
|
+
];
|
|
46
|
+
};
|
|
47
|
+
const removeChip = (state, key) => {
|
|
48
|
+
if (key === "range") return {
|
|
49
|
+
...state,
|
|
50
|
+
range: void 0
|
|
51
|
+
};
|
|
52
|
+
if (key === "date") return {
|
|
53
|
+
...state,
|
|
54
|
+
date: void 0
|
|
55
|
+
};
|
|
56
|
+
const [kind, group, value] = key.split(":");
|
|
57
|
+
if (kind === "toggle") return toggleFlag(state, group);
|
|
58
|
+
if (kind === "facet") return toggleFacetValue(state, group, value);
|
|
59
|
+
throw new Error(`Unknown filter chip key: ${key}`);
|
|
60
|
+
};
|
|
61
|
+
const matchesSearch = (label, query) => label.toLowerCase().includes(query.trim().toLowerCase());
|
|
62
|
+
//#endregion
|
|
63
|
+
export { activeChips, emptyFilterState, matchesSearch, removeChip, toggleFacetValue, toggleFlag };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { barInRange, fraction, moveThumb } from "./range-math.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import "./range-slider.css";
|
|
5
|
+
//#region src/filter-panel/range-filter.tsx
|
|
6
|
+
const RangeFilter = ({ config, value, onChange }) => {
|
|
7
|
+
const { min, max, step, histogram, format } = config;
|
|
8
|
+
const selected = value ?? [min, max];
|
|
9
|
+
const peak = Math.max(...histogram);
|
|
10
|
+
const commit = (next) => onChange(next[0] === min && next[1] === max ? void 0 : next);
|
|
11
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
12
|
+
className: "w-full",
|
|
13
|
+
children: [
|
|
14
|
+
/* @__PURE__ */ jsxs("div", {
|
|
15
|
+
className: "px-[7px]",
|
|
16
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
17
|
+
className: "flex h-12 items-end gap-0.5",
|
|
18
|
+
children: histogram.map((count, index) => /* @__PURE__ */ jsx("span", {
|
|
19
|
+
className: cn("flex-1 rounded-t-sm", barInRange(index, histogram.length, min, max, selected) ? "bg-primary/70" : "bg-border"),
|
|
20
|
+
style: { height: `${Math.max(6, count / peak * 100)}%` }
|
|
21
|
+
}, index))
|
|
22
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
23
|
+
className: "relative mt-1 h-4",
|
|
24
|
+
children: [/* @__PURE__ */ jsx("span", { className: "absolute inset-x-0 top-1/2 h-1 -translate-y-1/2 rounded-full bg-border" }), /* @__PURE__ */ jsx("span", {
|
|
25
|
+
className: "absolute top-1/2 h-1 -translate-y-1/2 rounded-full bg-primary",
|
|
26
|
+
style: {
|
|
27
|
+
left: `${fraction(selected[0], min, max) * 100}%`,
|
|
28
|
+
right: `${100 - fraction(selected[1], min, max) * 100}%`
|
|
29
|
+
}
|
|
30
|
+
})]
|
|
31
|
+
})]
|
|
32
|
+
}),
|
|
33
|
+
/* @__PURE__ */ jsx("div", {
|
|
34
|
+
className: "relative -mt-4 h-4",
|
|
35
|
+
children: [0, 1].map((thumb) => /* @__PURE__ */ jsx("input", {
|
|
36
|
+
type: "range",
|
|
37
|
+
className: "pb-range",
|
|
38
|
+
"aria-label": `${config.label} ${thumb === 0 ? "minimum" : "maximum"}`,
|
|
39
|
+
min,
|
|
40
|
+
max,
|
|
41
|
+
step,
|
|
42
|
+
value: selected[thumb],
|
|
43
|
+
onChange: (event) => commit(moveThumb(selected, thumb, Number(event.target.value)))
|
|
44
|
+
}, thumb))
|
|
45
|
+
}),
|
|
46
|
+
/* @__PURE__ */ jsxs("div", {
|
|
47
|
+
className: "mt-2 flex justify-between text-xs tabular-nums text-muted-foreground",
|
|
48
|
+
children: [/* @__PURE__ */ jsx("span", { children: format(selected[0]) }), /* @__PURE__ */ jsx("span", { children: format(selected[1]) })]
|
|
49
|
+
})
|
|
50
|
+
]
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { RangeFilter };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/filter-panel/range-math.ts
|
|
2
|
+
/** Which histogram bars lie inside the selected range, for tinting. */
|
|
3
|
+
const barInRange = (barIndex, bars, min, max, selected) => {
|
|
4
|
+
const barStart = min + (max - min) * barIndex / bars;
|
|
5
|
+
return min + (max - min) * (barIndex + 1) / bars > selected[0] && barStart < selected[1];
|
|
6
|
+
};
|
|
7
|
+
const fraction = (value, min, max) => max === min ? 0 : (value - min) / (max - min);
|
|
8
|
+
/** Keeps the two thumbs ordered, moving the one being dragged. */
|
|
9
|
+
const moveThumb = (range, thumb, value) => thumb === 0 ? [Math.min(value, range[1]), range[1]] : [range[0], Math.max(value, range[0])];
|
|
10
|
+
//#endregion
|
|
11
|
+
export { barInRange, fraction, moveThumb };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
.pb-range {
|
|
2
|
+
position: absolute;
|
|
3
|
+
inset: 0;
|
|
4
|
+
width: 100%;
|
|
5
|
+
height: 100%;
|
|
6
|
+
margin: 0;
|
|
7
|
+
appearance: none;
|
|
8
|
+
background: transparent;
|
|
9
|
+
pointer-events: none;
|
|
10
|
+
}
|
|
11
|
+
.pb-range::-webkit-slider-thumb {
|
|
12
|
+
appearance: none;
|
|
13
|
+
pointer-events: auto;
|
|
14
|
+
width: 14px;
|
|
15
|
+
height: 14px;
|
|
16
|
+
border-radius: 9999px;
|
|
17
|
+
background: var(--background);
|
|
18
|
+
border: 2px solid var(--primary);
|
|
19
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 0.2);
|
|
20
|
+
cursor: grab;
|
|
21
|
+
}
|
|
22
|
+
.pb-range::-moz-range-thumb {
|
|
23
|
+
pointer-events: auto;
|
|
24
|
+
width: 10px;
|
|
25
|
+
height: 10px;
|
|
26
|
+
border-radius: 9999px;
|
|
27
|
+
background: var(--background);
|
|
28
|
+
border: 2px solid var(--primary);
|
|
29
|
+
cursor: grab;
|
|
30
|
+
}
|
|
31
|
+
.pb-range::-webkit-slider-runnable-track { appearance: none; background: transparent; }
|
|
32
|
+
.pb-range::-moz-range-track { background: transparent; }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Switch } from "../primitives/switch.js";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/filter-panel/toggle-row.tsx
|
|
4
|
+
const ToggleRow = ({ label, checked, onChange }) => /* @__PURE__ */ jsxs("label", {
|
|
5
|
+
className: "flex cursor-pointer items-center justify-between gap-3 py-1",
|
|
6
|
+
children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx(Switch, {
|
|
7
|
+
checked,
|
|
8
|
+
onChange,
|
|
9
|
+
label
|
|
10
|
+
})]
|
|
11
|
+
});
|
|
12
|
+
//#endregion
|
|
13
|
+
export { ToggleRow };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { emptyFilterState } from "./filter-state.js";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
//#region src/filter-panel/use-filter-state.ts
|
|
4
|
+
/** Controlled when `value` is given, otherwise keeps its own state. */
|
|
5
|
+
const useFilterState = (value, defaultValue, onChange) => {
|
|
6
|
+
const [local, setLocal] = useState(defaultValue ?? emptyFilterState);
|
|
7
|
+
const state = value ?? local;
|
|
8
|
+
const update = (next) => {
|
|
9
|
+
setLocal(next);
|
|
10
|
+
onChange?.(next);
|
|
11
|
+
};
|
|
12
|
+
return [state, update];
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { useFilterState };
|