@protobase/ui 0.12.10 → 0.12.12
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/dist/app/action-icon.js +3 -7
- package/dist/app/displays/cards-display.js +43 -10
- package/dist/app/displays/list-displays.js +5 -3
- package/dist/app/field-editor.js +4 -0
- package/dist/app/named-icon.js +13 -0
- package/dist/app/pages/field-value.js +4 -4
- package/dist/app/record-page.js +4 -3
- package/dist/app/value-badge.js +14 -0
- package/dist/data-grid/cells.js +9 -6
- package/dist/filter-panel/combobox-options.js +12 -0
- package/dist/filter-panel/facet-combobox.js +131 -0
- package/dist/filter-panel/facet-group.js +1 -1
- package/dist/filter-panel/filter-menu.js +7 -2
- package/dist/filter-panel/filter-panel.js +33 -24
- package/dist/filter-panel/filter-state.js +23 -11
- package/dist/filter-panel/range-filter.js +3 -3
- package/dist/index.js +1 -1
- package/dist/lib/readable-text.js +23 -0
- package/dist/live/columns-from-model.js +3 -0
- package/dist/live/display-value.js +6 -3
- package/dist/live/filter-string.js +18 -9
- package/dist/live/reference-options.js +14 -0
- package/dist/live/use-filter-config.js +64 -35
- package/dist/live/use-reference-search.js +53 -0
- package/dist/live/value-color.js +8 -0
- package/dist/live/view-fields.js +31 -14
- package/dist/package.js +1 -1
- package/dist/primitives/badge.js +12 -3
- package/dist/shell/assets/app-BKltU-WD.js +65 -0
- package/dist/shell/assets/{index-BIjqw3oN.css → index-CP0s3cFI.css} +1 -1
- package/dist/shell/assets/{index-CuBoFEnG.js → index-D1ngvDop.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-CR4VI05-.js → shared-protobase-ui-Cv4rF2fz.js} +1 -1
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-CUxGSH9m.js +0 -65
package/dist/app/action-icon.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { namedIcon } from "./named-icon.js";
|
|
2
|
+
import { Zap } from "lucide-react";
|
|
2
3
|
//#region src/app/action-icon.ts
|
|
3
|
-
const
|
|
4
|
-
wallet: Wallet,
|
|
5
|
-
"file-text": FileText,
|
|
6
|
-
send: Send
|
|
7
|
-
};
|
|
8
|
-
const actionIcon = (name) => name ? byName[name] ?? Zap : Zap;
|
|
4
|
+
const actionIcon = (name) => namedIcon(name) ?? Zap;
|
|
9
5
|
//#endregion
|
|
10
6
|
export { actionIcon };
|
|
@@ -9,7 +9,8 @@ import { displayValue } from "../../live/display-value.js";
|
|
|
9
9
|
import { shownWidget } from "../field-types/shown-widget.js";
|
|
10
10
|
import { CustomBoundary } from "../pages/custom-boundary.js";
|
|
11
11
|
import { WidgetPart } from "../field-types/widget-part.js";
|
|
12
|
-
import {
|
|
12
|
+
import { namedIcon } from "../named-icon.js";
|
|
13
|
+
import { ValueBadge } from "../value-badge.js";
|
|
13
14
|
import { DisplayFooter, DisplayState } from "./display-frame.js";
|
|
14
15
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
15
16
|
//#region src/app/displays/cards-display.tsx
|
|
@@ -27,12 +28,23 @@ const CardImage = ({ value }) => {
|
|
|
27
28
|
})
|
|
28
29
|
});
|
|
29
30
|
};
|
|
31
|
+
const MetaValue = ({ icon, children }) => {
|
|
32
|
+
const Icon = namedIcon(icon);
|
|
33
|
+
return /* @__PURE__ */ jsxs("dd", {
|
|
34
|
+
className: "flex items-center gap-1 tabular-nums",
|
|
35
|
+
children: [Icon && /* @__PURE__ */ jsx(Icon, {
|
|
36
|
+
className: "size-3.5 shrink-0",
|
|
37
|
+
"aria-hidden": true
|
|
38
|
+
}), children]
|
|
39
|
+
});
|
|
40
|
+
};
|
|
30
41
|
/**
|
|
31
|
-
* A card per record, opening its page: its image across the top, the title over the subtitle, the badge beside it
|
|
32
|
-
* or, with a `card` component, what it draws of the
|
|
42
|
+
* A card per record, opening its page: its image across the top, the title over the subtitle, the badge beside it,
|
|
43
|
+
* small meta fields below and the labels of its flags that are true; or, with a `card` component, what it draws of the
|
|
44
|
+
* row, the built-in content again when it is missing or throws.
|
|
33
45
|
*/
|
|
34
46
|
const CardsDisplay = ({ model, view, display, list }) => {
|
|
35
|
-
const { image, title, subtitle, badge, meta } = cardFields(model, view, display);
|
|
47
|
+
const { image, title, subtitle, badge, meta, flags } = cardFields(model, view, display);
|
|
36
48
|
const label = (name) => view?.fields[name]?.label ?? humanize(name);
|
|
37
49
|
const text = (row, name) => {
|
|
38
50
|
const field = model.fields[name];
|
|
@@ -52,6 +64,17 @@ const CardsDisplay = ({ model, view, display, list }) => {
|
|
|
52
64
|
fallback: text(row, name)
|
|
53
65
|
}) : text(row, name);
|
|
54
66
|
};
|
|
67
|
+
const metaValue = (row, name) => {
|
|
68
|
+
const field = model.fields[name];
|
|
69
|
+
const hints = view?.fields[name];
|
|
70
|
+
const value = row.record[name];
|
|
71
|
+
if (!field || hints?.format !== "badge" || shownWidget(fieldTypes, field, hints) || value === null || value === void 0 || value === "") return shownValue(row, name);
|
|
72
|
+
return /* @__PURE__ */ jsx(ValueBadge, {
|
|
73
|
+
value,
|
|
74
|
+
hints,
|
|
75
|
+
children: text(row, name)
|
|
76
|
+
});
|
|
77
|
+
};
|
|
55
78
|
const plural = (view?.names?.plural ?? humanize(model.name)).toLowerCase();
|
|
56
79
|
return /* @__PURE__ */ jsxs(DisplayState, {
|
|
57
80
|
list,
|
|
@@ -65,6 +88,7 @@ const CardsDisplay = ({ model, view, display, list }) => {
|
|
|
65
88
|
"aria-label": view?.names?.plural ?? humanize(model.name),
|
|
66
89
|
children: list.rows.map((row) => {
|
|
67
90
|
const status = badge === void 0 ? void 0 : row.record[badge];
|
|
91
|
+
const raised = flags.filter((name) => row.record[name] === true);
|
|
68
92
|
const builtIn = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
69
93
|
image !== void 0 && /* @__PURE__ */ jsx(CardImage, { value: row.record[image] }),
|
|
70
94
|
/* @__PURE__ */ jsxs("span", {
|
|
@@ -72,8 +96,9 @@ const CardsDisplay = ({ model, view, display, list }) => {
|
|
|
72
96
|
children: [/* @__PURE__ */ jsx("span", {
|
|
73
97
|
className: "min-w-0 truncate font-medium",
|
|
74
98
|
children: shownValue(row, title)
|
|
75
|
-
}), badge !== void 0 && status !== null && status !== void 0 && /* @__PURE__ */ jsx(
|
|
76
|
-
|
|
99
|
+
}), badge !== void 0 && status !== null && status !== void 0 && /* @__PURE__ */ jsx(ValueBadge, {
|
|
100
|
+
value: status,
|
|
101
|
+
hints: view?.fields[badge],
|
|
77
102
|
children: text(row, badge)
|
|
78
103
|
})]
|
|
79
104
|
}),
|
|
@@ -82,17 +107,25 @@ const CardsDisplay = ({ model, view, display, list }) => {
|
|
|
82
107
|
children: shownValue(row, subtitle)
|
|
83
108
|
}),
|
|
84
109
|
meta.length > 0 && /* @__PURE__ */ jsx("dl", {
|
|
85
|
-
className: "mt-auto flex flex-wrap gap-x-3 gap-y-0.5 pt-1 text-xs text-muted-foreground",
|
|
110
|
+
className: "mt-auto flex flex-wrap items-center gap-x-3 gap-y-0.5 pt-1 text-xs text-muted-foreground",
|
|
86
111
|
children: meta.map((name) => /* @__PURE__ */ jsxs("div", {
|
|
87
112
|
className: "flex gap-1",
|
|
88
113
|
children: [/* @__PURE__ */ jsx("dt", {
|
|
89
114
|
className: "sr-only",
|
|
90
115
|
children: label(name)
|
|
91
|
-
}), /* @__PURE__ */ jsx(
|
|
92
|
-
|
|
93
|
-
children:
|
|
116
|
+
}), /* @__PURE__ */ jsx(MetaValue, {
|
|
117
|
+
icon: view?.fields[name]?.icon,
|
|
118
|
+
children: metaValue(row, name)
|
|
94
119
|
})]
|
|
95
120
|
}, name))
|
|
121
|
+
}),
|
|
122
|
+
raised.length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
123
|
+
className: "flex flex-wrap gap-1",
|
|
124
|
+
"aria-label": "Features",
|
|
125
|
+
children: raised.map((name) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Badge, {
|
|
126
|
+
dot: false,
|
|
127
|
+
children: label(name)
|
|
128
|
+
}) }, name))
|
|
96
129
|
})
|
|
97
130
|
] });
|
|
98
131
|
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AppLink, {
|
|
@@ -34,18 +34,20 @@ const cardFields = (model, view, display) => ({
|
|
|
34
34
|
title: display.title ?? recordTitleField(model, view),
|
|
35
35
|
subtitle: display.subtitle,
|
|
36
36
|
badge: display.badge,
|
|
37
|
-
meta: display.meta ?? []
|
|
37
|
+
meta: display.meta ?? [],
|
|
38
|
+
flags: display.flags ?? []
|
|
38
39
|
});
|
|
39
40
|
/** Every field a display shows, the fields its rows are loaded and labelled for. */
|
|
40
41
|
const shownFields = (model, view, display) => {
|
|
41
42
|
if (display.kind === "custom") return display.fields;
|
|
42
|
-
const { image, title, subtitle, badge, meta } = cardFields(model, view, display);
|
|
43
|
+
const { image, title, subtitle, badge, meta, flags } = cardFields(model, view, display);
|
|
43
44
|
return [
|
|
44
45
|
image,
|
|
45
46
|
title,
|
|
46
47
|
subtitle,
|
|
47
48
|
badge,
|
|
48
|
-
...meta
|
|
49
|
+
...meta,
|
|
50
|
+
...flags
|
|
49
51
|
].filter((name) => name !== void 0);
|
|
50
52
|
};
|
|
51
53
|
/** The fields to label the list's rows for: the table's columns and every display's, so one load serves them all. */
|
package/dist/app/field-editor.js
CHANGED
|
@@ -90,6 +90,10 @@ const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo,
|
|
|
90
90
|
className: "text-[13px]",
|
|
91
91
|
children: hints.prefix
|
|
92
92
|
}) : void 0,
|
|
93
|
+
trailing: hints?.suffix ? /* @__PURE__ */ jsx("span", {
|
|
94
|
+
className: "text-[13px]",
|
|
95
|
+
children: hints.suffix.trim()
|
|
96
|
+
}) : void 0,
|
|
93
97
|
placeholder,
|
|
94
98
|
className: cn(numeric.has(field.type) && "text-right tabular-nums"),
|
|
95
99
|
value: String(value),
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FileText, RulerDimensionLine, Send, Wallet } from "lucide-react";
|
|
2
|
+
//#region src/app/named-icon.ts
|
|
3
|
+
/** The icons an action's or a field's `icon` may name. */
|
|
4
|
+
const byName = {
|
|
5
|
+
wallet: Wallet,
|
|
6
|
+
"file-text": FileText,
|
|
7
|
+
send: Send,
|
|
8
|
+
ruler: RulerDimensionLine
|
|
9
|
+
};
|
|
10
|
+
/** The icon a name stands for, or nothing for a name the app does not know. */
|
|
11
|
+
const namedIcon = (name) => name !== void 0 && Object.hasOwn(byName, name) ? byName[name] : void 0;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { namedIcon };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { cn } from "../../lib/cn.js";
|
|
2
|
-
import { Badge } from "../../primitives/badge.js";
|
|
3
2
|
import { keys } from "../../data/query-keys.js";
|
|
4
3
|
import { useClient } from "../../data/api-provider.js";
|
|
5
4
|
import { fetchLabels } from "../../live/relation-labels.js";
|
|
@@ -9,7 +8,7 @@ import { fileShown } from "../../live/file-values.js";
|
|
|
9
8
|
import { displayValue } from "../../live/display-value.js";
|
|
10
9
|
import { useShownWidget } from "../field-types/shown-widget.js";
|
|
11
10
|
import { WidgetPart } from "../field-types/widget-part.js";
|
|
12
|
-
import {
|
|
11
|
+
import { ValueBadge } from "../value-badge.js";
|
|
13
12
|
import { jsx } from "react/jsx-runtime";
|
|
14
13
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
15
14
|
//#region src/app/pages/field-value.tsx
|
|
@@ -59,8 +58,9 @@ const BuiltInValue = ({ model, field, value, format, relationLabel }) => {
|
|
|
59
58
|
children: file.name
|
|
60
59
|
});
|
|
61
60
|
const text = displayValue(field, value, hints);
|
|
62
|
-
if (field.type === "enum" || hints.format === "badge") return /* @__PURE__ */ jsx(
|
|
63
|
-
|
|
61
|
+
if (field.type === "enum" || hints.format === "badge") return /* @__PURE__ */ jsx(ValueBadge, {
|
|
62
|
+
value,
|
|
63
|
+
hints,
|
|
64
64
|
children: text
|
|
65
65
|
});
|
|
66
66
|
return /* @__PURE__ */ jsx("span", {
|
package/dist/app/record-page.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Badge } from "../primitives/badge.js";
|
|
2
1
|
import { useAuth } from "../auth/auth-provider.js";
|
|
3
2
|
import { formatRelativeTime } from "../format/date.js";
|
|
4
3
|
import { Button } from "../primitives/button.js";
|
|
@@ -24,6 +23,7 @@ import { RecordHeader } from "../record-view/record-header.js";
|
|
|
24
23
|
import { toneFor } from "../live/columns-from-model.js";
|
|
25
24
|
import { useDeleteFlow } from "./delete/use-delete-flow.js";
|
|
26
25
|
import { useRestore } from "./delete/use-restore.js";
|
|
26
|
+
import { ValueBadge } from "./value-badge.js";
|
|
27
27
|
import { auditField } from "../live/audit-fields.js";
|
|
28
28
|
import { CustomSection } from "./custom-section.js";
|
|
29
29
|
import { RecordActions } from "./pages/record-actions.js";
|
|
@@ -176,8 +176,9 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
176
176
|
const field = model.fields[name];
|
|
177
177
|
if (!field || !visible(name)) return [];
|
|
178
178
|
const text = displayValue(field, stored.record[name], hint(name));
|
|
179
|
-
const builtIn = field.type === "enum" ? /* @__PURE__ */ jsx(
|
|
180
|
-
|
|
179
|
+
const builtIn = field.type === "enum" ? /* @__PURE__ */ jsx(ValueBadge, {
|
|
180
|
+
value: stored.record[name],
|
|
181
|
+
hints: hint(name),
|
|
181
182
|
children: text
|
|
182
183
|
}) : /* @__PURE__ */ jsx("strong", { children: text });
|
|
183
184
|
const shown = shownWidget(fieldTypes, field, hint(name));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Badge } from "../primitives/badge.js";
|
|
2
|
+
import { valueColor } from "../live/value-color.js";
|
|
3
|
+
import { toneFor } from "../live/columns-from-model.js";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/app/value-badge.tsx
|
|
6
|
+
/** A stored value as a badge: filled with the colour the view gives it and in its badge shape, else in its tone. */
|
|
7
|
+
const ValueBadge = ({ value, hints, children }) => /* @__PURE__ */ jsx(Badge, {
|
|
8
|
+
tone: toneFor(String(value)),
|
|
9
|
+
color: valueColor(hints?.valueColors, value),
|
|
10
|
+
shape: hints?.badgeShape,
|
|
11
|
+
children
|
|
12
|
+
});
|
|
13
|
+
//#endregion
|
|
14
|
+
export { ValueBadge };
|
package/dist/data-grid/cells.js
CHANGED
|
@@ -6,6 +6,7 @@ import { formatInt, formatPercent, formatSigned } from "../format/number.js";
|
|
|
6
6
|
import { formatDate, formatDateTime } from "../format/date.js";
|
|
7
7
|
import { fileShown, isPreviewable } from "../live/file-values.js";
|
|
8
8
|
import { userById } from "../mocks/users.js";
|
|
9
|
+
import { valueColor } from "../live/value-color.js";
|
|
9
10
|
import { Check, Paperclip } from "lucide-react";
|
|
10
11
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
12
|
import { enumLabel } from "@protobase/schema";
|
|
@@ -26,9 +27,9 @@ const renderCell = (spec, value) => {
|
|
|
26
27
|
className: "truncate font-mono text-xs text-muted-foreground",
|
|
27
28
|
children: String(value)
|
|
28
29
|
});
|
|
29
|
-
case "money": return /* @__PURE__ */
|
|
30
|
+
case "money": return /* @__PURE__ */ jsxs("span", {
|
|
30
31
|
className: "tabular-nums",
|
|
31
|
-
children: formatMoney(Number(value), spec.currency)
|
|
32
|
+
children: [formatMoney(Number(value), spec.currency), spec.suffix]
|
|
32
33
|
});
|
|
33
34
|
case "date": return /* @__PURE__ */ jsx("span", {
|
|
34
35
|
className: "tabular-nums text-muted-foreground",
|
|
@@ -38,13 +39,13 @@ const renderCell = (spec, value) => {
|
|
|
38
39
|
className: "tabular-nums text-muted-foreground",
|
|
39
40
|
children: formatDateTime(Number(value))
|
|
40
41
|
});
|
|
41
|
-
case "number": return /* @__PURE__ */
|
|
42
|
+
case "number": return /* @__PURE__ */ jsxs("span", {
|
|
42
43
|
className: "tabular-nums",
|
|
43
|
-
children: formatInt(Number(value))
|
|
44
|
+
children: [formatInt(Number(value)), spec.suffix]
|
|
44
45
|
});
|
|
45
|
-
case "signed": return /* @__PURE__ */
|
|
46
|
+
case "signed": return /* @__PURE__ */ jsxs("span", {
|
|
46
47
|
className: cn("tabular-nums", Number(value) < 0 ? "text-danger-text" : "text-success-text"),
|
|
47
|
-
children: formatSigned(Number(value))
|
|
48
|
+
children: [formatSigned(Number(value)), spec.suffix]
|
|
48
49
|
});
|
|
49
50
|
case "percent": return Number(value) === 0 ? muted("—") : /* @__PURE__ */ jsx("span", {
|
|
50
51
|
className: "tabular-nums",
|
|
@@ -52,6 +53,8 @@ const renderCell = (spec, value) => {
|
|
|
52
53
|
});
|
|
53
54
|
case "status": return /* @__PURE__ */ jsx(Badge, {
|
|
54
55
|
tone: spec.tones?.[String(value)] ?? "neutral",
|
|
56
|
+
color: valueColor(spec.colors, value),
|
|
57
|
+
shape: spec.shape,
|
|
55
58
|
children: enumLabel(String(value), spec.labels)
|
|
56
59
|
});
|
|
57
60
|
case "relation": return /* @__PURE__ */ jsx("span", {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { matchesSearch } from "./filter-state.js";
|
|
2
|
+
//#region src/filter-panel/combobox-options.ts
|
|
3
|
+
/** The options a combobox lists for its search text: as given when the group searches itself, else those whose name matches. */
|
|
4
|
+
const comboboxOptions = (group, query) => group.onSearch ? group.options : group.options.filter((option) => matchesSearch(option.label, query));
|
|
5
|
+
/** The option the arrow keys move to, wrapping around at either end; -1 when there are none. */
|
|
6
|
+
const moveActive = (active, count, key) => {
|
|
7
|
+
if (count === 0) return -1;
|
|
8
|
+
if (key === "ArrowDown") return active < 0 ? 0 : (active + 1) % count;
|
|
9
|
+
return active < 0 ? count - 1 : (active - 1 + count) % count;
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { comboboxOptions, moveActive };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { formatInt } from "../format/number.js";
|
|
3
|
+
import { facetLabel } from "./filter-state.js";
|
|
4
|
+
import { comboboxOptions, moveActive } from "./combobox-options.js";
|
|
5
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
6
|
+
import { Check, Search, X } from "lucide-react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
//#region src/filter-panel/facet-combobox.tsx
|
|
9
|
+
/** Chooses several values of a facet: the choices sit in the box as removable tokens, typing searches the options. */
|
|
10
|
+
const FacetCombobox = ({ config, selected, onToggle, inline = false }) => {
|
|
11
|
+
const id = useId();
|
|
12
|
+
const input = useRef(null);
|
|
13
|
+
const [query, setQuery] = useState("");
|
|
14
|
+
const [focused, setFocused] = useState(false);
|
|
15
|
+
const [active, setActive] = useState(-1);
|
|
16
|
+
const open = inline || focused;
|
|
17
|
+
const options = comboboxOptions(config, query);
|
|
18
|
+
const onSearch = useRef(config.onSearch);
|
|
19
|
+
onSearch.current = config.onSearch;
|
|
20
|
+
useEffect(() => () => onSearch.current?.(""), []);
|
|
21
|
+
const search = (text) => {
|
|
22
|
+
setQuery(text);
|
|
23
|
+
setActive(-1);
|
|
24
|
+
config.onSearch?.(text);
|
|
25
|
+
};
|
|
26
|
+
const onKeyDown = (event) => {
|
|
27
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
setActive(moveActive(active, options.length, event.key));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (event.key === "Enter" && options[active]) {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
onToggle(options[active].value);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (event.key === "Backspace" && query === "" && selected.length > 0) {
|
|
38
|
+
onToggle(selected[selected.length - 1]);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (event.key === "Escape" && query !== "") {
|
|
42
|
+
event.stopPropagation();
|
|
43
|
+
search("");
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
47
|
+
className: "flex flex-col gap-1",
|
|
48
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
49
|
+
className: "flex min-h-8 cursor-text flex-wrap items-center gap-1 rounded-md border border-border-strong bg-background px-2 py-1 shadow-sm focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20",
|
|
50
|
+
onClick: () => input.current?.focus(),
|
|
51
|
+
children: [
|
|
52
|
+
/* @__PURE__ */ jsx(Search, { className: "size-3.5 shrink-0 text-muted-foreground" }),
|
|
53
|
+
selected.map((value) => /* @__PURE__ */ jsxs("span", {
|
|
54
|
+
className: "inline-flex h-5 max-w-full items-center gap-0.5 rounded bg-primary-soft pl-1.5 pr-0.5 text-xs font-medium text-primary-text",
|
|
55
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
56
|
+
className: "truncate",
|
|
57
|
+
children: facetLabel(config, value)
|
|
58
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
59
|
+
type: "button",
|
|
60
|
+
"aria-label": `Remove ${facetLabel(config, value)}`,
|
|
61
|
+
className: "rounded p-0.5 hover:bg-primary-border/60",
|
|
62
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
63
|
+
onClick: () => onToggle(value),
|
|
64
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3" })
|
|
65
|
+
})]
|
|
66
|
+
}, value)),
|
|
67
|
+
/* @__PURE__ */ jsx("input", {
|
|
68
|
+
ref: input,
|
|
69
|
+
autoFocus: inline,
|
|
70
|
+
role: "combobox",
|
|
71
|
+
"aria-label": config.label,
|
|
72
|
+
"aria-expanded": open,
|
|
73
|
+
"aria-controls": `${id}-options`,
|
|
74
|
+
"aria-autocomplete": "list",
|
|
75
|
+
"aria-activedescendant": options[active] ? `${id}-${active}` : void 0,
|
|
76
|
+
placeholder: selected.length > 0 ? "" : `Search ${config.label.toLowerCase()}`,
|
|
77
|
+
className: "h-6 min-w-16 flex-1 bg-transparent text-[13px] text-foreground outline-none placeholder:text-faint-foreground",
|
|
78
|
+
value: query,
|
|
79
|
+
onChange: (event) => search(event.target.value),
|
|
80
|
+
onKeyDown,
|
|
81
|
+
onFocus: () => setFocused(true),
|
|
82
|
+
onBlur: () => {
|
|
83
|
+
setFocused(false);
|
|
84
|
+
setActive(-1);
|
|
85
|
+
if (!inline && query !== "") search("");
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
]
|
|
89
|
+
}), open && /* @__PURE__ */ jsxs("ul", {
|
|
90
|
+
id: `${id}-options`,
|
|
91
|
+
role: "listbox",
|
|
92
|
+
"aria-label": config.label,
|
|
93
|
+
"aria-multiselectable": true,
|
|
94
|
+
className: cn("flex max-h-60 flex-col gap-px overflow-y-auto", !inline && "rounded-md border bg-background p-1 shadow-sm"),
|
|
95
|
+
children: [options.map((option, index) => {
|
|
96
|
+
const chosen = selected.includes(option.value);
|
|
97
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
98
|
+
id: `${id}-${index}`,
|
|
99
|
+
role: "option",
|
|
100
|
+
"aria-selected": chosen,
|
|
101
|
+
tabIndex: -1,
|
|
102
|
+
className: cn("flex cursor-pointer items-center gap-2 rounded px-1.5 py-1 hover:bg-muted", index === active && "bg-muted"),
|
|
103
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
104
|
+
onClick: () => onToggle(option.value),
|
|
105
|
+
children: [
|
|
106
|
+
/* @__PURE__ */ jsx("span", {
|
|
107
|
+
className: cn("inline-flex size-4 shrink-0 items-center justify-center rounded border", chosen ? "border-primary bg-primary text-primary-foreground" : "border-border-strong bg-background"),
|
|
108
|
+
children: chosen && /* @__PURE__ */ jsx(Check, {
|
|
109
|
+
className: "size-3",
|
|
110
|
+
strokeWidth: 3
|
|
111
|
+
})
|
|
112
|
+
}),
|
|
113
|
+
/* @__PURE__ */ jsx("span", {
|
|
114
|
+
className: "flex-1 truncate",
|
|
115
|
+
children: option.label
|
|
116
|
+
}),
|
|
117
|
+
option.count !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
118
|
+
className: "text-xs tabular-nums text-faint-foreground",
|
|
119
|
+
children: formatInt(option.count)
|
|
120
|
+
})
|
|
121
|
+
]
|
|
122
|
+
}, option.value);
|
|
123
|
+
}), options.length === 0 && /* @__PURE__ */ jsx("li", {
|
|
124
|
+
className: "px-1.5 py-1 text-xs text-faint-foreground",
|
|
125
|
+
children: "No matches"
|
|
126
|
+
})]
|
|
127
|
+
})]
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
//#endregion
|
|
131
|
+
export { FacetCombobox };
|
|
@@ -31,7 +31,7 @@ const FacetGroup = ({ config, selected, onToggle }) => {
|
|
|
31
31
|
className: "flex-1 truncate",
|
|
32
32
|
children: option.label
|
|
33
33
|
}),
|
|
34
|
-
/* @__PURE__ */ jsx("span", {
|
|
34
|
+
option.count !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
35
35
|
className: "text-xs tabular-nums text-faint-foreground",
|
|
36
36
|
children: formatInt(option.count)
|
|
37
37
|
})
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
-
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
3
|
import { ChevronDown } from "lucide-react";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/filter-panel/filter-menu.tsx
|
|
6
6
|
const FilterMenu = ({ label, activeCount, width = 240, defaultOpen = false, children }) => {
|
|
7
7
|
const [open, setOpen] = useState(defaultOpen);
|
|
8
|
+
const [alignEnd, setAlignEnd] = useState(false);
|
|
8
9
|
const root = useRef(null);
|
|
10
|
+
useLayoutEffect(() => {
|
|
11
|
+
if (!open || !root.current) return;
|
|
12
|
+
setAlignEnd(root.current.getBoundingClientRect().left + width > document.documentElement.clientWidth);
|
|
13
|
+
}, [open, width]);
|
|
9
14
|
useEffect(() => {
|
|
10
15
|
if (!open) return;
|
|
11
16
|
const close = (event) => {
|
|
@@ -31,7 +36,7 @@ const FilterMenu = ({ label, activeCount, width = 240, defaultOpen = false, chil
|
|
|
31
36
|
/* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5 text-muted-foreground" })
|
|
32
37
|
]
|
|
33
38
|
}), open && /* @__PURE__ */ jsx("div", {
|
|
34
|
-
className: "absolute
|
|
39
|
+
className: cn("absolute top-full z-30 mt-1.5 rounded-lg border bg-background p-3 shadow-pop", alignEnd ? "right-0" : "left-0"),
|
|
35
40
|
style: { width },
|
|
36
41
|
children
|
|
37
42
|
})]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ActiveFilterChips } from "./active-filter-chips.js";
|
|
2
2
|
import { DatePresets } from "./date-presets.js";
|
|
3
|
-
import { activeChips, emptyFilterState, removeChip, toggleFacetValue, toggleFlag } from "./filter-state.js";
|
|
3
|
+
import { activeChips, emptyFilterState, removeChip, setRange, toggleFacetValue, toggleFlag } from "./filter-state.js";
|
|
4
|
+
import { FacetCombobox } from "./facet-combobox.js";
|
|
4
5
|
import { FacetGroup } from "./facet-group.js";
|
|
5
6
|
import { FilterMenu } from "./filter-menu.js";
|
|
6
7
|
import { FilterSection } from "./filter-section.js";
|
|
@@ -20,18 +21,25 @@ const FilterPanel = ({ config, layout, value, defaultValue, onChange, lockedLabe
|
|
|
20
21
|
onRemove: (key) => setState(removeChip(state, key)),
|
|
21
22
|
onClear: () => setState(emptyFilterState)
|
|
22
23
|
});
|
|
23
|
-
const facetView = (group
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
const facetView = (group, inline = false) => {
|
|
25
|
+
const selected = state.facets[group.id] ?? [];
|
|
26
|
+
const onToggle = (option) => setState(toggleFacetValue(state, group.id, option));
|
|
27
|
+
return group.combobox ? /* @__PURE__ */ jsx(FacetCombobox, {
|
|
28
|
+
config: group,
|
|
29
|
+
selected,
|
|
30
|
+
onToggle,
|
|
31
|
+
inline
|
|
32
|
+
}) : /* @__PURE__ */ jsx(FacetGroup, {
|
|
33
|
+
config: group,
|
|
34
|
+
selected,
|
|
35
|
+
onToggle
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
const ranges = config.ranges ?? [];
|
|
39
|
+
const rangeView = (range) => /* @__PURE__ */ jsx(RangeFilter, {
|
|
40
|
+
config: range,
|
|
41
|
+
value: state.ranges?.[range.id],
|
|
42
|
+
onChange: (span) => setState(setRange(state, range.id, span))
|
|
35
43
|
});
|
|
36
44
|
const dateView = config.datePresets && /* @__PURE__ */ jsx(DatePresets, {
|
|
37
45
|
value: state.date,
|
|
@@ -64,10 +72,10 @@ const FilterPanel = ({ config, layout, value, defaultValue, onChange, lockedLabe
|
|
|
64
72
|
title: group.label,
|
|
65
73
|
children: facetView(group)
|
|
66
74
|
}, group.id)),
|
|
67
|
-
|
|
68
|
-
title:
|
|
69
|
-
children: rangeView
|
|
70
|
-
}),
|
|
75
|
+
ranges.map((range) => /* @__PURE__ */ jsx(FilterSection, {
|
|
76
|
+
title: range.label,
|
|
77
|
+
children: rangeView(range)
|
|
78
|
+
}, range.id)),
|
|
71
79
|
dateView && /* @__PURE__ */ jsx(FilterSection, {
|
|
72
80
|
title: "Date",
|
|
73
81
|
children: dateView
|
|
@@ -82,16 +90,17 @@ const FilterPanel = ({ config, layout, value, defaultValue, onChange, lockedLabe
|
|
|
82
90
|
...config.facets.map((group) => /* @__PURE__ */ jsx(FilterMenu, {
|
|
83
91
|
label: group.label,
|
|
84
92
|
activeCount: (state.facets[group.id] ?? []).length,
|
|
93
|
+
width: group.combobox ? 280 : void 0,
|
|
85
94
|
defaultOpen: defaultOpenMenu === group.id,
|
|
86
|
-
children: facetView(group)
|
|
95
|
+
children: facetView(group, true)
|
|
87
96
|
}, group.id)),
|
|
88
|
-
...
|
|
89
|
-
label:
|
|
90
|
-
activeCount: state.range ? 1 : 0,
|
|
97
|
+
...ranges.map((range) => /* @__PURE__ */ jsx(FilterMenu, {
|
|
98
|
+
label: range.label,
|
|
99
|
+
activeCount: state.ranges?.[range.id] ? 1 : 0,
|
|
91
100
|
width: 280,
|
|
92
|
-
defaultOpen: defaultOpenMenu ===
|
|
93
|
-
children: rangeView
|
|
94
|
-
},
|
|
101
|
+
defaultOpen: defaultOpenMenu === range.id,
|
|
102
|
+
children: rangeView(range)
|
|
103
|
+
}, `range:${range.id}`)),
|
|
95
104
|
...dateView ? [/* @__PURE__ */ jsx(Fragment, { children: dateView }, "date")] : []
|
|
96
105
|
];
|
|
97
106
|
const toggles = (config.toggles ?? []).map((toggle) => /* @__PURE__ */ jsx("div", {
|
|
@@ -19,16 +19,31 @@ const toggleFlag = (state, id) => ({
|
|
|
19
19
|
...state,
|
|
20
20
|
toggles: state.toggles.includes(id) ? state.toggles.filter((entry) => entry !== id) : [...state.toggles, id]
|
|
21
21
|
});
|
|
22
|
+
/** Sets or, with `undefined`, clears the span of one range. */
|
|
23
|
+
const setRange = (state, id, value) => {
|
|
24
|
+
const { [id]: _, ...others } = state.ranges ?? {};
|
|
25
|
+
return {
|
|
26
|
+
...state,
|
|
27
|
+
ranges: value ? {
|
|
28
|
+
...others,
|
|
29
|
+
[id]: value
|
|
30
|
+
} : others
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/** A facet value's name: its option's label, else a name the group knows for it, else the value itself. */
|
|
34
|
+
const facetLabel = (group, value) => group.options.find((option) => option.value === value)?.label ?? group.labels?.[value] ?? value;
|
|
22
35
|
const activeChips = (config, state) => {
|
|
23
36
|
const facetChips = config.facets.flatMap((group) => (state.facets[group.id] ?? []).map((value) => ({
|
|
24
37
|
key: `facet:${group.id}:${value}`,
|
|
25
|
-
label: `${group.label}: ${group
|
|
38
|
+
label: `${group.label}: ${facetLabel(group, value)}`
|
|
26
39
|
})));
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
const rangeChips = (config.ranges ?? []).flatMap((range) => {
|
|
41
|
+
const span = state.ranges?.[range.id];
|
|
42
|
+
return span ? [{
|
|
43
|
+
key: `range:${range.id}`,
|
|
44
|
+
label: `${range.label}: ${range.format(span[0])} – ${range.format(span[1])}`
|
|
45
|
+
}] : [];
|
|
46
|
+
});
|
|
32
47
|
const dateChips = state.date ? [{
|
|
33
48
|
key: "date",
|
|
34
49
|
label: datePresetLabels[state.date] ?? state.date
|
|
@@ -45,19 +60,16 @@ const activeChips = (config, state) => {
|
|
|
45
60
|
];
|
|
46
61
|
};
|
|
47
62
|
const removeChip = (state, key) => {
|
|
48
|
-
if (key === "range") return {
|
|
49
|
-
...state,
|
|
50
|
-
range: void 0
|
|
51
|
-
};
|
|
52
63
|
if (key === "date") return {
|
|
53
64
|
...state,
|
|
54
65
|
date: void 0
|
|
55
66
|
};
|
|
56
67
|
const [kind, group, value] = key.split(":");
|
|
68
|
+
if (kind === "range") return setRange(state, group, void 0);
|
|
57
69
|
if (kind === "toggle") return toggleFlag(state, group);
|
|
58
70
|
if (kind === "facet") return toggleFacetValue(state, group, value);
|
|
59
71
|
throw new Error(`Unknown filter chip key: ${key}`);
|
|
60
72
|
};
|
|
61
73
|
const matchesSearch = (label, query) => label.toLowerCase().includes(query.trim().toLowerCase());
|
|
62
74
|
//#endregion
|
|
63
|
-
export { activeChips, emptyFilterState, matchesSearch, removeChip, toggleFacetValue, toggleFlag };
|
|
75
|
+
export { activeChips, emptyFilterState, facetLabel, matchesSearch, removeChip, setRange, toggleFacetValue, toggleFlag };
|
|
@@ -13,14 +13,14 @@ const RangeFilter = ({ config, value, onChange }) => {
|
|
|
13
13
|
children: [
|
|
14
14
|
/* @__PURE__ */ jsxs("div", {
|
|
15
15
|
className: "px-[7px]",
|
|
16
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
17
|
-
className: "flex h-12 items-end gap-0.5",
|
|
16
|
+
children: [histogram.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
17
|
+
className: "mb-1 flex h-12 items-end gap-0.5",
|
|
18
18
|
children: histogram.map((count, index) => /* @__PURE__ */ jsx("span", {
|
|
19
19
|
className: cn("flex-1 rounded-t-sm", barInRange(index, histogram.length, min, max, selected) ? "bg-primary/70" : "bg-border"),
|
|
20
20
|
style: { height: `${Math.max(6, count / peak * 100)}%` }
|
|
21
21
|
}, index))
|
|
22
22
|
}), /* @__PURE__ */ jsxs("div", {
|
|
23
|
-
className: "relative
|
|
23
|
+
className: "relative h-4",
|
|
24
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
25
|
className: "absolute top-1/2 h-1 -translate-y-1/2 rounded-full bg-primary",
|
|
26
26
|
style: {
|
package/dist/index.js
CHANGED
|
@@ -19,8 +19,8 @@ import { useAssistant } from "./assistant-dock/use-assistant.js";
|
|
|
19
19
|
import { useAssistantClient } from "./assistant-dock/use-assistant-client.js";
|
|
20
20
|
import { useMeta } from "./data/use-meta.js";
|
|
21
21
|
import { AppLink, useAppAddress } from "./app/use-app-address.js";
|
|
22
|
-
import { useFacets, useHistogram, useSeries } from "./data/use-aggregates.js";
|
|
23
22
|
import { useListDisplay } from "./app/displays/display-context.js";
|
|
23
|
+
import { useFacets, useHistogram, useSeries } from "./data/use-aggregates.js";
|
|
24
24
|
import { useLayoutRecord } from "./app/pages/record-scope.js";
|
|
25
25
|
import { uiFromModule } from "./app/pages/ui-from-module.js";
|
|
26
26
|
import { mergeUi } from "./app/pages/merge-ui.js";
|