@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,158 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useId, useRef } from "react";
|
|
3
|
+
import {
|
|
4
|
+
Legend as RechartsLegend,
|
|
5
|
+
ResponsiveContainer,
|
|
6
|
+
Tooltip as RechartsTooltip
|
|
7
|
+
} from "recharts";
|
|
8
|
+
import { cn } from "../lib/cn.js";
|
|
9
|
+
const ChartContext = createContext(null);
|
|
10
|
+
function useChart() {
|
|
11
|
+
const ctx = useContext(ChartContext);
|
|
12
|
+
if (!ctx) throw new Error("useChart must be used within a <ChartContainer />");
|
|
13
|
+
return ctx;
|
|
14
|
+
}
|
|
15
|
+
function ChartContainer({ id, className, children, config, ...props }) {
|
|
16
|
+
const uid = useId();
|
|
17
|
+
const chartId = `chart-${(id ?? uid).replace(/:/g, "")}`;
|
|
18
|
+
return /* @__PURE__ */ jsx(ChartContext.Provider, { value: config, children: /* @__PURE__ */ jsxs(
|
|
19
|
+
"div",
|
|
20
|
+
{
|
|
21
|
+
"data-chart": chartId,
|
|
22
|
+
className: cn(
|
|
23
|
+
// touch-pan-y lets a vertical finger drag still scroll the page while
|
|
24
|
+
// a horizontal drag is handed to recharts for tooltip scrubbing,
|
|
25
|
+
// instead of the page scrolling underneath the gesture (feedback #292).
|
|
26
|
+
"flex w-full touch-pan-y justify-center text-xs",
|
|
27
|
+
// muted axis labels + gridlines, light and dark
|
|
28
|
+
"[&_.recharts-cartesian-axis-tick_text]:fill-slate-500 dark:[&_.recharts-cartesian-axis-tick_text]:fill-slate-400",
|
|
29
|
+
"[&_.recharts-cartesian-grid_line]:stroke-slate-200/70 dark:[&_.recharts-cartesian-grid_line]:stroke-slate-700/60",
|
|
30
|
+
// hairline hover cursor instead of the heavy default
|
|
31
|
+
"[&_.recharts-curve.recharts-tooltip-cursor]:stroke-slate-300 dark:[&_.recharts-curve.recharts-tooltip-cursor]:stroke-slate-600",
|
|
32
|
+
"[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-slate-100 dark:[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-slate-800",
|
|
33
|
+
// kill the focus outlines & white sector strokes Recharts adds
|
|
34
|
+
"[&_.recharts-sector]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent",
|
|
35
|
+
"[&_.recharts-layer]:outline-none [&_.recharts-surface]:outline-none [&_.recharts-dot[stroke='#fff']]:stroke-transparent",
|
|
36
|
+
className
|
|
37
|
+
),
|
|
38
|
+
...props,
|
|
39
|
+
children: [
|
|
40
|
+
/* @__PURE__ */ jsx(ChartStyle, { id: chartId, config }),
|
|
41
|
+
/* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children })
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
) });
|
|
45
|
+
}
|
|
46
|
+
function ChartStyle({ id, config }) {
|
|
47
|
+
const entries = Object.entries(config).filter(([, c]) => c.color);
|
|
48
|
+
if (!entries.length) return null;
|
|
49
|
+
const css = `[data-chart=${id}] {
|
|
50
|
+
${entries.map(([key, c]) => ` --color-${key}: ${c.color};`).join("\n")}
|
|
51
|
+
}`;
|
|
52
|
+
return /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: css } });
|
|
53
|
+
}
|
|
54
|
+
const ChartTooltip = RechartsTooltip;
|
|
55
|
+
const ChartLegend = RechartsLegend;
|
|
56
|
+
function ChartTooltipContent({
|
|
57
|
+
active,
|
|
58
|
+
payload,
|
|
59
|
+
label,
|
|
60
|
+
indicator = "dot",
|
|
61
|
+
hideLabel = false,
|
|
62
|
+
labelFormatter,
|
|
63
|
+
valueFormatter,
|
|
64
|
+
coordinate,
|
|
65
|
+
boundaryRef
|
|
66
|
+
}) {
|
|
67
|
+
const config = useChart();
|
|
68
|
+
const tipRef = useRef(null);
|
|
69
|
+
if (!active || !payload?.length) return null;
|
|
70
|
+
let flip = false;
|
|
71
|
+
const boundary = boundaryRef?.current;
|
|
72
|
+
if (boundary && coordinate?.x != null) {
|
|
73
|
+
const visibleX = coordinate.x - boundary.scrollLeft;
|
|
74
|
+
const tipWidth = tipRef.current?.offsetWidth ?? 160;
|
|
75
|
+
flip = visibleX + tipWidth + 12 > boundary.clientWidth;
|
|
76
|
+
}
|
|
77
|
+
return /* @__PURE__ */ jsxs(
|
|
78
|
+
"div",
|
|
79
|
+
{
|
|
80
|
+
ref: tipRef,
|
|
81
|
+
style: boundaryRef ? { transform: flip ? "translateX(calc(-100% - 12px))" : "translateX(12px)" } : void 0,
|
|
82
|
+
className: "min-w-[9rem] rounded-lg border border-slate-200 bg-white px-2.5 py-2 text-xs shadow-xl dark:border-slate-700 dark:bg-slate-900",
|
|
83
|
+
children: [
|
|
84
|
+
!hideLabel && label != null && /* @__PURE__ */ jsx("div", { className: "mb-1.5 font-medium text-slate-900 dark:text-slate-100", children: labelFormatter ? labelFormatter(label) : label }),
|
|
85
|
+
/* @__PURE__ */ jsx("div", { className: "grid gap-1.5", children: payload.map((item, i) => {
|
|
86
|
+
const dk = item.dataKey != null ? String(item.dataKey) : void 0;
|
|
87
|
+
const nm = item.name != null ? String(item.name) : void 0;
|
|
88
|
+
const cfgKey = dk && config[dk] ? dk : nm && config[nm] ? nm : dk ?? nm ?? String(i);
|
|
89
|
+
const series = config[cfgKey];
|
|
90
|
+
const fill = typeof item.payload?.fill === "string" ? item.payload.fill : void 0;
|
|
91
|
+
const color = series?.color ?? item.color ?? fill ?? "#64748b";
|
|
92
|
+
const name = series?.label ?? nm ?? cfgKey;
|
|
93
|
+
const raw = typeof item.value === "string" ? Number(item.value) : item.value ?? 0;
|
|
94
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
95
|
+
/* @__PURE__ */ jsx(
|
|
96
|
+
"span",
|
|
97
|
+
{
|
|
98
|
+
className: cn(
|
|
99
|
+
"shrink-0 rounded-[2px]",
|
|
100
|
+
indicator === "dot" ? "h-2.5 w-2.5 rounded-full" : "h-2.5 w-1"
|
|
101
|
+
),
|
|
102
|
+
style: { backgroundColor: color }
|
|
103
|
+
}
|
|
104
|
+
),
|
|
105
|
+
/* @__PURE__ */ jsx("span", { className: "text-slate-500 dark:text-slate-400", children: name }),
|
|
106
|
+
/* @__PURE__ */ jsx("span", { "data-private": true, className: "ml-auto font-mono font-medium tabular-nums text-slate-900 dark:text-slate-100", children: valueFormatter ? valueFormatter(raw) : raw })
|
|
107
|
+
] }, `${cfgKey}-${i}`);
|
|
108
|
+
}) })
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
function ChartLegendContent({ payload, onItemClick, activeKey }) {
|
|
114
|
+
const config = useChart();
|
|
115
|
+
if (!payload?.length) return null;
|
|
116
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pt-3", children: payload.map((item, i) => {
|
|
117
|
+
const dk = item.dataKey != null ? String(item.dataKey) : void 0;
|
|
118
|
+
const vv = item.value != null ? String(item.value) : void 0;
|
|
119
|
+
const cfgKey = dk && config[dk] ? dk : vv && config[vv] ? vv : dk ?? vv ?? String(i);
|
|
120
|
+
const series = config[cfgKey];
|
|
121
|
+
const color = series?.color ?? item.color ?? "#64748b";
|
|
122
|
+
const label = series?.label ?? vv ?? cfgKey;
|
|
123
|
+
const dimmed = activeKey != null && activeKey !== cfgKey;
|
|
124
|
+
return /* @__PURE__ */ jsxs(
|
|
125
|
+
"button",
|
|
126
|
+
{
|
|
127
|
+
type: "button",
|
|
128
|
+
onClick: onItemClick ? () => onItemClick(cfgKey) : void 0,
|
|
129
|
+
className: cn(
|
|
130
|
+
"flex items-center gap-1.5 text-slate-600 transition-opacity dark:text-slate-300",
|
|
131
|
+
onItemClick && "cursor-pointer hover:opacity-80",
|
|
132
|
+
!onItemClick && "cursor-default",
|
|
133
|
+
dimmed && "opacity-40"
|
|
134
|
+
),
|
|
135
|
+
children: [
|
|
136
|
+
/* @__PURE__ */ jsx(
|
|
137
|
+
"span",
|
|
138
|
+
{
|
|
139
|
+
className: "h-2.5 w-2.5 shrink-0 rounded-[3px]",
|
|
140
|
+
style: { backgroundColor: color }
|
|
141
|
+
}
|
|
142
|
+
),
|
|
143
|
+
label
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
`${cfgKey}-${i}`
|
|
147
|
+
);
|
|
148
|
+
}) });
|
|
149
|
+
}
|
|
150
|
+
export {
|
|
151
|
+
ChartContainer,
|
|
152
|
+
ChartLegend,
|
|
153
|
+
ChartLegendContent,
|
|
154
|
+
ChartTooltip,
|
|
155
|
+
ChartTooltipContent,
|
|
156
|
+
useChart
|
|
157
|
+
};
|
|
158
|
+
//# sourceMappingURL=chart.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/chart.tsx"],"sourcesContent":["// shadcn/ui chart kit — vendored (MIT) and adapted for this app:\n// - Recharts 3 instead of 2\n// - the original keys its styling off shadcn theme tokens (--muted-foreground,\n// --border, bg-background …). This app has no shadcn token layer, so those\n// are mapped to the existing slate palette + class-based `dark:` variants.\n// The value on show is the *structure*: config-driven colors injected as CSS\n// vars, muted axis/grid styling, and a polished tooltip/legend — the things\n// that make shadcn charts read as \"designed\" rather than \"default Recharts\".\nimport { createContext, useContext, useId, useRef } from \"react\";\nimport type { ComponentProps, CSSProperties, ReactNode, RefObject } from \"react\";\nimport {\n Legend as RechartsLegend,\n ResponsiveContainer,\n Tooltip as RechartsTooltip,\n} from \"recharts\";\nimport { cn } from \"../lib/cn\";\n\nexport interface ChartSeriesConfig {\n label: ReactNode;\n color: string;\n}\n\nexport type ChartConfig = Record<string, ChartSeriesConfig>;\n\nconst ChartContext = createContext<ChartConfig | null>(null);\n\nexport function useChart(): ChartConfig {\n const ctx = useContext(ChartContext);\n if (!ctx) throw new Error(\"useChart must be used within a <ChartContainer />\");\n return ctx;\n}\n\ninterface ChartContainerProps extends ComponentProps<\"div\"> {\n config: ChartConfig;\n children: ComponentProps<typeof ResponsiveContainer>[\"children\"];\n}\n\nexport function ChartContainer({ id, className, children, config, ...props }: ChartContainerProps) {\n const uid = useId();\n const chartId = `chart-${(id ?? uid).replace(/:/g, \"\")}`;\n return (\n <ChartContext.Provider value={config}>\n <div\n data-chart={chartId}\n className={cn(\n // touch-pan-y lets a vertical finger drag still scroll the page while\n // a horizontal drag is handed to recharts for tooltip scrubbing,\n // instead of the page scrolling underneath the gesture (feedback #292).\n \"flex w-full touch-pan-y justify-center text-xs\",\n // muted axis labels + gridlines, light and dark\n \"[&_.recharts-cartesian-axis-tick_text]:fill-slate-500 dark:[&_.recharts-cartesian-axis-tick_text]:fill-slate-400\",\n \"[&_.recharts-cartesian-grid_line]:stroke-slate-200/70 dark:[&_.recharts-cartesian-grid_line]:stroke-slate-700/60\",\n // hairline hover cursor instead of the heavy default\n \"[&_.recharts-curve.recharts-tooltip-cursor]:stroke-slate-300 dark:[&_.recharts-curve.recharts-tooltip-cursor]:stroke-slate-600\",\n \"[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-slate-100 dark:[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-slate-800\",\n // kill the focus outlines & white sector strokes Recharts adds\n \"[&_.recharts-sector]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent\",\n \"[&_.recharts-layer]:outline-none [&_.recharts-surface]:outline-none [&_.recharts-dot[stroke='#fff']]:stroke-transparent\",\n className,\n )}\n {...props}\n >\n <ChartStyle id={chartId} config={config} />\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n {children}\n </ResponsiveContainer>\n </div>\n </ChartContext.Provider>\n );\n}\n\n// Inject each series colour as a `--color-<key>` custom property scoped to this\n// chart, so chart JSX can refer to `var(--color-total)` and the legend/tooltip\n// stay in sync from one source of truth.\nfunction ChartStyle({ id, config }: { id: string; config: ChartConfig }) {\n const entries = Object.entries(config).filter(([, c]) => c.color);\n if (!entries.length) return null;\n const css = `[data-chart=${id}] {\\n${entries\n .map(([key, c]) => ` --color-${key}: ${c.color};`)\n .join(\"\\n\")}\\n}`;\n return <style dangerouslySetInnerHTML={{ __html: css }} />;\n}\n\nexport const ChartTooltip = RechartsTooltip;\nexport const ChartLegend = RechartsLegend;\n\ninterface TooltipPayloadItem {\n dataKey?: string | number;\n name?: string | number;\n value?: number | string;\n color?: string;\n payload?: Record<string, unknown>;\n}\n\ninterface ChartTooltipContentProps {\n active?: boolean;\n payload?: TooltipPayloadItem[];\n label?: string | number;\n indicator?: \"dot\" | \"line\";\n hideLabel?: boolean;\n labelFormatter?: (label: string | number) => ReactNode;\n valueFormatter?: (value: number) => string;\n /** Cursor position within the chart, injected by recharts. Used with\n * {@link boundaryRef} to decide whether to flip the tooltip. */\n coordinate?: { x?: number; y?: number };\n /** When set, the tooltip flips to the LEFT of the cursor if it would otherwise\n * spill past the right edge of this (usually horizontally-scrolling) container\n * — e.g. a wide chart whose rightmost bars sat off-screen (feedback #77). */\n boundaryRef?: RefObject<HTMLElement | null>;\n}\n\nexport function ChartTooltipContent({\n active,\n payload,\n label,\n indicator = \"dot\",\n hideLabel = false,\n labelFormatter,\n valueFormatter,\n coordinate,\n boundaryRef,\n}: ChartTooltipContentProps) {\n const config = useChart();\n const tipRef = useRef<HTMLDivElement>(null);\n if (!active || !payload?.length) return null;\n // Recharts anchors the tooltip at coordinate.x inside the full (scrolled) chart\n // width; subtract the container's scrollLeft to get its on-screen x, then flip\n // left if the tooltip's own width would run past the visible right edge.\n let flip = false;\n const boundary = boundaryRef?.current;\n if (boundary && coordinate?.x != null) {\n const visibleX = coordinate.x - boundary.scrollLeft;\n const tipWidth = tipRef.current?.offsetWidth ?? 160;\n flip = visibleX + tipWidth + 12 > boundary.clientWidth;\n }\n return (\n <div\n ref={tipRef}\n style={boundaryRef ? { transform: flip ? \"translateX(calc(-100% - 12px))\" : \"translateX(12px)\" } : undefined}\n className=\"min-w-[9rem] rounded-lg border border-slate-200 bg-white px-2.5 py-2 text-xs shadow-xl dark:border-slate-700 dark:bg-slate-900\"\n >\n {!hideLabel && label != null && (\n <div className=\"mb-1.5 font-medium text-slate-900 dark:text-slate-100\">\n {labelFormatter ? labelFormatter(label) : label}\n </div>\n )}\n <div className=\"grid gap-1.5\">\n {payload.map((item, i) => {\n // Bars/lines/areas identify a series by dataKey; pies/treemaps key off\n // `name` (the nameKey value). Prefer whichever the config knows.\n const dk = item.dataKey != null ? String(item.dataKey) : undefined;\n const nm = item.name != null ? String(item.name) : undefined;\n const cfgKey = dk && config[dk] ? dk : nm && config[nm] ? nm : (dk ?? nm ?? String(i));\n const series = config[cfgKey];\n const fill = typeof item.payload?.fill === \"string\" ? item.payload.fill : undefined;\n const color = series?.color ?? item.color ?? fill ?? \"#64748b\";\n const name = series?.label ?? nm ?? cfgKey;\n const raw = typeof item.value === \"string\" ? Number(item.value) : (item.value ?? 0);\n return (\n <div key={`${cfgKey}-${i}`} className=\"flex items-center gap-2\">\n <span\n className={cn(\n \"shrink-0 rounded-[2px]\",\n indicator === \"dot\" ? \"h-2.5 w-2.5 rounded-full\" : \"h-2.5 w-1\",\n )}\n style={{ backgroundColor: color }}\n />\n <span className=\"text-slate-500 dark:text-slate-400\">{name}</span>\n <span data-private className=\"ml-auto font-mono font-medium tabular-nums text-slate-900 dark:text-slate-100\">\n {valueFormatter ? valueFormatter(raw) : raw}\n </span>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n\ninterface LegendPayloadItem {\n value?: string;\n dataKey?: string | number;\n color?: string;\n}\n\ninterface ChartLegendContentProps {\n payload?: LegendPayloadItem[];\n onItemClick?: (key: string) => void;\n activeKey?: string | null;\n}\n\nexport function ChartLegendContent({ payload, onItemClick, activeKey }: ChartLegendContentProps) {\n const config = useChart();\n if (!payload?.length) return null;\n return (\n <div className=\"flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pt-3\">\n {payload.map((item, i) => {\n const dk = item.dataKey != null ? String(item.dataKey) : undefined;\n const vv = item.value != null ? String(item.value) : undefined;\n const cfgKey = dk && config[dk] ? dk : vv && config[vv] ? vv : (dk ?? vv ?? String(i));\n const series = config[cfgKey];\n const color = series?.color ?? item.color ?? \"#64748b\";\n const label = series?.label ?? vv ?? cfgKey;\n const dimmed = activeKey != null && activeKey !== cfgKey;\n return (\n <button\n key={`${cfgKey}-${i}`}\n type=\"button\"\n onClick={onItemClick ? () => onItemClick(cfgKey) : undefined}\n className={cn(\n \"flex items-center gap-1.5 text-slate-600 transition-opacity dark:text-slate-300\",\n onItemClick && \"cursor-pointer hover:opacity-80\",\n !onItemClick && \"cursor-default\",\n dimmed && \"opacity-40\",\n )}\n >\n <span\n className=\"h-2.5 w-2.5 shrink-0 rounded-[3px]\"\n style={{ backgroundColor: color } as CSSProperties}\n />\n {label}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, RefObject, Dispatch, SetStateAction } from 'react';
|
|
3
|
+
import { AnchorRect } from '../hooks/use-anchored-rect.js';
|
|
4
|
+
import { AnchoredPanel } from '../hooks/use-anchored-panel.js';
|
|
5
|
+
|
|
6
|
+
interface ComboOption<V extends string | number> {
|
|
7
|
+
value: V;
|
|
8
|
+
label: string;
|
|
9
|
+
sublabel?: string;
|
|
10
|
+
icon?: ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* Section this option belongs to. Where a consumer renders it as a heading with the
|
|
13
|
+
* members indented beneath — {@link InlineEntityCombobox} does — this is a genuine
|
|
14
|
+
* grouping, not decoration: the name appears once per section rather than trailing
|
|
15
|
+
* every row in small grey type, which is barely legible on a phone (feedback #136).
|
|
16
|
+
* Searchable like `sublabel`.
|
|
17
|
+
*/
|
|
18
|
+
group?: string;
|
|
19
|
+
}
|
|
20
|
+
interface ComboboxCoreOptions<V extends string | number> {
|
|
21
|
+
/** Already-loaded options (client-side filtered), also used to resolve labels. */
|
|
22
|
+
options?: ComboOption<V>[];
|
|
23
|
+
/** Async option source, debounced and race-safe; stale responses are ignored. */
|
|
24
|
+
loadOptions?: (query: string) => Promise<ComboOption<V>[]>;
|
|
25
|
+
/** External loading flag, OR-ed with the internal async state. */
|
|
26
|
+
loading?: boolean;
|
|
27
|
+
}
|
|
28
|
+
interface ComboboxCore<V extends string | number> {
|
|
29
|
+
triggerRef: RefObject<HTMLButtonElement | null>;
|
|
30
|
+
panelRef: RefObject<HTMLDivElement | null>;
|
|
31
|
+
inputRef: RefObject<HTMLInputElement | null>;
|
|
32
|
+
open: boolean;
|
|
33
|
+
setOpen: Dispatch<SetStateAction<boolean>>;
|
|
34
|
+
query: string;
|
|
35
|
+
setQuery: Dispatch<SetStateAction<string>>;
|
|
36
|
+
active: number;
|
|
37
|
+
setActive: Dispatch<SetStateAction<number>>;
|
|
38
|
+
results: ComboOption<V>[];
|
|
39
|
+
busy: boolean;
|
|
40
|
+
rect: AnchorRect | null;
|
|
41
|
+
/** Where the dropdown goes, clamped to the visible viewport (feedback #135). */
|
|
42
|
+
placement: AnchoredPanel;
|
|
43
|
+
cacheRef: RefObject<Map<V, ComboOption<V>>>;
|
|
44
|
+
resolve: (v: V) => ComboOption<V> | null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Shared plumbing for the entity pickers: open/query/active state, portalled
|
|
48
|
+
* anchoring + outside-click/Escape dismissal, focus-on-open, a debounced
|
|
49
|
+
* race-safe async (or client-side) result list, and a label cache so a selected
|
|
50
|
+
* value still renders its label after the async list has moved on. The single-
|
|
51
|
+
* and multi-value comboboxes render their triggers on top of this and share
|
|
52
|
+
* {@link ComboboxPanel} for the dropdown.
|
|
53
|
+
*/
|
|
54
|
+
declare function useComboboxCore<V extends string | number>({ options, loadOptions, loading, }: ComboboxCoreOptions<V>): ComboboxCore<V>;
|
|
55
|
+
/**
|
|
56
|
+
* The portalled dropdown (search header + result rows + optional create row)
|
|
57
|
+
* shared by the single- and multi-value comboboxes. Left-aligned to the trigger
|
|
58
|
+
* and sized to its width; `multi` swaps the trailing check for a leading
|
|
59
|
+
* checkbox. `onChoose` decides whether to close (single) or stay open (multi).
|
|
60
|
+
*/
|
|
61
|
+
declare function ComboboxPanel<V extends string | number>({ core, searchPlaceholder, emptyLabel, multi, isSelected, onChoose, showCreate, onCreate, createContent, sheetTitle, closeLabel, }: {
|
|
62
|
+
core: ComboboxCore<V>;
|
|
63
|
+
searchPlaceholder?: string;
|
|
64
|
+
emptyLabel: string;
|
|
65
|
+
multi?: boolean;
|
|
66
|
+
isSelected: (v: V) => boolean;
|
|
67
|
+
onChoose: (o: ComboOption<V>) => void;
|
|
68
|
+
showCreate: boolean;
|
|
69
|
+
onCreate: () => void;
|
|
70
|
+
createContent: ReactNode;
|
|
71
|
+
/** What the sheet calls itself on a phone — the field's label. Ignored on
|
|
72
|
+
* desktop, where the panel sits under the field that already says it. */
|
|
73
|
+
sheetTitle?: ReactNode;
|
|
74
|
+
closeLabel?: string;
|
|
75
|
+
}): react.JSX.Element | null;
|
|
76
|
+
|
|
77
|
+
export { type ComboOption, type ComboboxCore, type ComboboxCoreOptions, ComboboxPanel, useComboboxCore };
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { Check, Plus } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { DropdownSearchHeader } from "./dropdown.js";
|
|
7
|
+
import { PickerSheet, SHEET_ROW_CLASS } from "./picker-sheet.js";
|
|
8
|
+
import { useMediaQuery } from "../hooks/use-media-query.js";
|
|
9
|
+
import { PHONE_QUERY } from "./ui.js";
|
|
10
|
+
import { useAnchoredPanel } from "../hooks/use-anchored-panel.js";
|
|
11
|
+
import { useEscapeKey, useOutsideClick } from "../hooks/use-dismiss.js";
|
|
12
|
+
function useComboboxCore({
|
|
13
|
+
options,
|
|
14
|
+
loadOptions,
|
|
15
|
+
loading
|
|
16
|
+
}) {
|
|
17
|
+
const triggerRef = useRef(null);
|
|
18
|
+
const panelRef = useRef(null);
|
|
19
|
+
const inputRef = useRef(null);
|
|
20
|
+
const [open, setOpen] = useState(false);
|
|
21
|
+
const [query, setQuery] = useState("");
|
|
22
|
+
const [active, setActive] = useState(0);
|
|
23
|
+
const isAsync = typeof loadOptions === "function";
|
|
24
|
+
const [asyncResults, setAsyncResults] = useState([]);
|
|
25
|
+
const [asyncLoading, setAsyncLoading] = useState(false);
|
|
26
|
+
const loadRef = useRef(loadOptions);
|
|
27
|
+
loadRef.current = loadOptions;
|
|
28
|
+
const reqId = useRef(0);
|
|
29
|
+
const close = () => setOpen(false);
|
|
30
|
+
const placement = useAnchoredPanel(triggerRef, open);
|
|
31
|
+
const rect = placement.rect;
|
|
32
|
+
useOutsideClick([triggerRef, panelRef], close, open);
|
|
33
|
+
useEscapeKey(close, open);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!open) return;
|
|
36
|
+
setQuery("");
|
|
37
|
+
const id = requestAnimationFrame(() => inputRef.current?.focus());
|
|
38
|
+
return () => cancelAnimationFrame(id);
|
|
39
|
+
}, [open]);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!open || !isAsync) return;
|
|
42
|
+
const id = ++reqId.current;
|
|
43
|
+
setAsyncLoading(true);
|
|
44
|
+
const run = async () => {
|
|
45
|
+
try {
|
|
46
|
+
const r = await loadRef.current(query);
|
|
47
|
+
if (reqId.current === id) setAsyncResults(r);
|
|
48
|
+
} finally {
|
|
49
|
+
if (reqId.current === id) setAsyncLoading(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const t = setTimeout(run, query ? 150 : 0);
|
|
53
|
+
return () => clearTimeout(t);
|
|
54
|
+
}, [query, open, isAsync]);
|
|
55
|
+
const clientResults = useMemo(() => {
|
|
56
|
+
if (isAsync) return [];
|
|
57
|
+
const src = options ?? [];
|
|
58
|
+
const q = query.trim().toLowerCase();
|
|
59
|
+
if (!q) return src;
|
|
60
|
+
return src.filter(
|
|
61
|
+
(o) => o.label.toLowerCase().includes(q) || (o.sublabel ?? "").toLowerCase().includes(q)
|
|
62
|
+
);
|
|
63
|
+
}, [isAsync, options, query]);
|
|
64
|
+
const results = isAsync ? asyncResults : clientResults;
|
|
65
|
+
const busy = Boolean(loading) || isAsync && asyncLoading;
|
|
66
|
+
const cacheRef = useRef(/* @__PURE__ */ new Map());
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
for (const o of options ?? []) cacheRef.current.set(o.value, o);
|
|
69
|
+
for (const o of results) cacheRef.current.set(o.value, o);
|
|
70
|
+
}, [options, results]);
|
|
71
|
+
const resolve = (v) => options?.find((o) => o.value === v) ?? cacheRef.current.get(v) ?? null;
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
setActive(0);
|
|
74
|
+
}, [query, open]);
|
|
75
|
+
return {
|
|
76
|
+
triggerRef,
|
|
77
|
+
panelRef,
|
|
78
|
+
inputRef,
|
|
79
|
+
open,
|
|
80
|
+
setOpen,
|
|
81
|
+
query,
|
|
82
|
+
setQuery,
|
|
83
|
+
active,
|
|
84
|
+
setActive,
|
|
85
|
+
results,
|
|
86
|
+
busy,
|
|
87
|
+
rect,
|
|
88
|
+
placement,
|
|
89
|
+
cacheRef,
|
|
90
|
+
resolve
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function ComboboxPanel({
|
|
94
|
+
core,
|
|
95
|
+
searchPlaceholder,
|
|
96
|
+
emptyLabel,
|
|
97
|
+
multi,
|
|
98
|
+
isSelected,
|
|
99
|
+
onChoose,
|
|
100
|
+
showCreate,
|
|
101
|
+
onCreate,
|
|
102
|
+
createContent,
|
|
103
|
+
sheetTitle,
|
|
104
|
+
closeLabel
|
|
105
|
+
}) {
|
|
106
|
+
const { rect, placement, panelRef, inputRef, query, setQuery, results, busy, active, setActive } = core;
|
|
107
|
+
const isPhone = useMediaQuery(PHONE_QUERY, false);
|
|
108
|
+
const rowCount = results.length + (showCreate ? 1 : 0);
|
|
109
|
+
const onKeyDown = (e) => {
|
|
110
|
+
if (e.key === "ArrowDown") {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
setActive((i) => Math.min(i + 1, rowCount - 1));
|
|
113
|
+
} else if (e.key === "ArrowUp") {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
setActive((i) => Math.max(i - 1, 0));
|
|
116
|
+
} else if (e.key === "Enter") {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
if (active < results.length) onChoose(results[active]);
|
|
119
|
+
else if (showCreate) onCreate();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
if (!core.open || typeof document === "undefined") return null;
|
|
123
|
+
const list = /* @__PURE__ */ jsxs(
|
|
124
|
+
"ul",
|
|
125
|
+
{
|
|
126
|
+
role: "listbox",
|
|
127
|
+
"aria-multiselectable": multi,
|
|
128
|
+
className: cn("min-h-0 flex-1 overflow-y-auto py-1", isPhone && "flex-none"),
|
|
129
|
+
children: [
|
|
130
|
+
busy && results.length === 0 && /* @__PURE__ */ jsx("li", { className: "px-3 py-2 text-sm text-slate-500 dark:text-slate-400", children: "\u2026" }),
|
|
131
|
+
!busy && results.length === 0 && !showCreate && /* @__PURE__ */ jsx("li", { className: "px-3 py-2 text-sm text-slate-500 dark:text-slate-400", children: emptyLabel }),
|
|
132
|
+
results.map((o, i) => {
|
|
133
|
+
const selected = isSelected(o.value);
|
|
134
|
+
const startsGroup = o.group != null && o.group !== results[i - 1]?.group;
|
|
135
|
+
return (
|
|
136
|
+
// Keyed by group AND value: an option may deliberately appear twice —
|
|
137
|
+
// Keksdose repeats recently-used categories in a "Recent" group at the
|
|
138
|
+
// top (live #203) — and a bare value key would collide.
|
|
139
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
140
|
+
startsGroup && /* @__PURE__ */ jsx(
|
|
141
|
+
"li",
|
|
142
|
+
{
|
|
143
|
+
role: "presentation",
|
|
144
|
+
className: cn(
|
|
145
|
+
"font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400",
|
|
146
|
+
isPhone ? "px-4 pb-1 pt-3 text-xs" : "px-3 pb-0.5 pt-2 text-[11px] first:pt-1"
|
|
147
|
+
),
|
|
148
|
+
children: o.group
|
|
149
|
+
}
|
|
150
|
+
),
|
|
151
|
+
/* @__PURE__ */ jsx("li", { role: "option", "aria-selected": selected, children: /* @__PURE__ */ jsxs(
|
|
152
|
+
"button",
|
|
153
|
+
{
|
|
154
|
+
type: "button",
|
|
155
|
+
onMouseDown: (e) => {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
onChoose(o);
|
|
158
|
+
},
|
|
159
|
+
onMouseEnter: () => setActive(i),
|
|
160
|
+
className: cn(
|
|
161
|
+
isPhone ? SHEET_ROW_CLASS : "flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm",
|
|
162
|
+
i === active && !isPhone ? "bg-slate-100 dark:bg-slate-800" : !isPhone && "hover:bg-slate-50 dark:hover:bg-slate-800"
|
|
163
|
+
),
|
|
164
|
+
children: [
|
|
165
|
+
multi && /* @__PURE__ */ jsx(
|
|
166
|
+
"span",
|
|
167
|
+
{
|
|
168
|
+
className: cn(
|
|
169
|
+
"flex size-4 shrink-0 items-center justify-center rounded border",
|
|
170
|
+
selected ? "border-slate-900 bg-slate-900 text-white dark:border-slate-100 dark:bg-slate-100 dark:text-slate-900" : "border-slate-300 bg-white dark:border-slate-600 dark:bg-slate-900"
|
|
171
|
+
),
|
|
172
|
+
children: selected && /* @__PURE__ */ jsx(Check, { className: "size-3" })
|
|
173
|
+
}
|
|
174
|
+
),
|
|
175
|
+
o.icon && /* @__PURE__ */ jsx("span", { className: "shrink-0", children: o.icon }),
|
|
176
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
177
|
+
/* @__PURE__ */ jsx("span", { className: "block truncate text-slate-900 dark:text-slate-100", children: o.label }),
|
|
178
|
+
o.sublabel && /* @__PURE__ */ jsx("span", { className: "block truncate text-xs text-slate-400 dark:text-slate-500", children: o.sublabel })
|
|
179
|
+
] }),
|
|
180
|
+
!multi && selected && /* @__PURE__ */ jsx(Check, { className: "size-4 shrink-0 text-sky-600 dark:text-sky-400" })
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
) })
|
|
184
|
+
] }, `${o.group ?? ""}|${String(o.value)}`)
|
|
185
|
+
);
|
|
186
|
+
}),
|
|
187
|
+
showCreate && /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": active === results.length, children: /* @__PURE__ */ jsxs(
|
|
188
|
+
"button",
|
|
189
|
+
{
|
|
190
|
+
type: "button",
|
|
191
|
+
onMouseDown: (e) => {
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
onCreate();
|
|
194
|
+
},
|
|
195
|
+
onMouseEnter: () => setActive(results.length),
|
|
196
|
+
className: cn(
|
|
197
|
+
isPhone ? SHEET_ROW_CLASS : "flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm",
|
|
198
|
+
"text-slate-700 dark:text-slate-200",
|
|
199
|
+
active === results.length && !isPhone ? "bg-slate-100 dark:bg-slate-800" : !isPhone && "hover:bg-slate-50 dark:hover:bg-slate-800"
|
|
200
|
+
),
|
|
201
|
+
children: [
|
|
202
|
+
/* @__PURE__ */ jsx(Plus, { className: "size-4 shrink-0" }),
|
|
203
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: createContent })
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
) })
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
if (isPhone) {
|
|
211
|
+
return /* @__PURE__ */ jsx(
|
|
212
|
+
PickerSheet,
|
|
213
|
+
{
|
|
214
|
+
open: true,
|
|
215
|
+
onClose: () => core.setOpen(false),
|
|
216
|
+
title: sheetTitle,
|
|
217
|
+
query,
|
|
218
|
+
onQueryChange: setQuery,
|
|
219
|
+
searchPlaceholder,
|
|
220
|
+
inputRef,
|
|
221
|
+
closeLabel,
|
|
222
|
+
children: /* @__PURE__ */ jsx("div", { onKeyDown, children: list })
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (!rect) return null;
|
|
227
|
+
return createPortal(
|
|
228
|
+
/* @__PURE__ */ jsxs(
|
|
229
|
+
"div",
|
|
230
|
+
{
|
|
231
|
+
ref: panelRef,
|
|
232
|
+
onKeyDown,
|
|
233
|
+
style: {
|
|
234
|
+
position: "fixed",
|
|
235
|
+
top: placement.top,
|
|
236
|
+
left: rect.left,
|
|
237
|
+
width: rect.width,
|
|
238
|
+
minWidth: 220,
|
|
239
|
+
maxHeight: placement.maxHeight
|
|
240
|
+
},
|
|
241
|
+
className: "z-50 flex flex-col rounded-md border border-slate-200 bg-white shadow-lg dark:border-slate-700 dark:bg-slate-900",
|
|
242
|
+
children: [
|
|
243
|
+
/* @__PURE__ */ jsx(
|
|
244
|
+
DropdownSearchHeader,
|
|
245
|
+
{
|
|
246
|
+
query,
|
|
247
|
+
onQueryChange: setQuery,
|
|
248
|
+
inputRef,
|
|
249
|
+
placeholder: searchPlaceholder
|
|
250
|
+
}
|
|
251
|
+
),
|
|
252
|
+
list
|
|
253
|
+
]
|
|
254
|
+
}
|
|
255
|
+
),
|
|
256
|
+
document.body
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
export {
|
|
260
|
+
ComboboxPanel,
|
|
261
|
+
useComboboxCore
|
|
262
|
+
};
|
|
263
|
+
//# sourceMappingURL=combobox-core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/combobox-core.tsx"],"sourcesContent":["import { Fragment, useEffect, useMemo, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type {\n Dispatch,\n KeyboardEvent as ReactKeyboardEvent,\n ReactNode,\n RefObject,\n SetStateAction,\n} from \"react\";\nimport { Check, Plus } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { DropdownSearchHeader } from \"./dropdown\";\nimport { PickerSheet, SHEET_ROW_CLASS } from \"./picker-sheet\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { PHONE_QUERY } from \"./ui\";\nimport { type AnchorRect } from \"../hooks/use-anchored-rect\";\nimport { useAnchoredPanel, type AnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useEscapeKey, useOutsideClick } from \"../hooks/use-dismiss\";\n\nexport interface ComboOption<V extends string | number> {\n value: V;\n label: string;\n sublabel?: string;\n icon?: ReactNode;\n /**\n * Section this option belongs to. Where a consumer renders it as a heading with the\n * members indented beneath — {@link InlineEntityCombobox} does — this is a genuine\n * grouping, not decoration: the name appears once per section rather than trailing\n * every row in small grey type, which is barely legible on a phone (feedback #136).\n * Searchable like `sublabel`.\n */\n group?: string;\n}\n\nexport interface ComboboxCoreOptions<V extends string | number> {\n /** Already-loaded options (client-side filtered), also used to resolve labels. */\n options?: ComboOption<V>[];\n /** Async option source, debounced and race-safe; stale responses are ignored. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** External loading flag, OR-ed with the internal async state. */\n loading?: boolean;\n}\n\nexport interface ComboboxCore<V extends string | number> {\n triggerRef: RefObject<HTMLButtonElement | null>;\n panelRef: RefObject<HTMLDivElement | null>;\n inputRef: RefObject<HTMLInputElement | null>;\n open: boolean;\n setOpen: Dispatch<SetStateAction<boolean>>;\n query: string;\n setQuery: Dispatch<SetStateAction<string>>;\n active: number;\n setActive: Dispatch<SetStateAction<number>>;\n results: ComboOption<V>[];\n busy: boolean;\n rect: AnchorRect | null;\n /** Where the dropdown goes, clamped to the visible viewport (feedback #135). */\n placement: AnchoredPanel;\n cacheRef: RefObject<Map<V, ComboOption<V>>>;\n resolve: (v: V) => ComboOption<V> | null;\n}\n\n/**\n * Shared plumbing for the entity pickers: open/query/active state, portalled\n * anchoring + outside-click/Escape dismissal, focus-on-open, a debounced\n * race-safe async (or client-side) result list, and a label cache so a selected\n * value still renders its label after the async list has moved on. The single-\n * and multi-value comboboxes render their triggers on top of this and share\n * {@link ComboboxPanel} for the dropdown.\n */\nexport function useComboboxCore<V extends string | number>({\n options,\n loadOptions,\n loading,\n}: ComboboxCoreOptions<V>): ComboboxCore<V> {\n const triggerRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [active, setActive] = useState(0);\n\n const isAsync = typeof loadOptions === \"function\";\n const [asyncResults, setAsyncResults] = useState<ComboOption<V>[]>([]);\n const [asyncLoading, setAsyncLoading] = useState(false);\n const loadRef = useRef(loadOptions);\n loadRef.current = loadOptions;\n const reqId = useRef(0);\n\n const close = () => setOpen(false);\n // Placement, not just the anchor rect: the panel has to dodge the on-screen\n // keyboard its own search box summons (feedback #135).\n const placement = useAnchoredPanel(triggerRef, open);\n const rect = placement.rect;\n useOutsideClick([triggerRef, panelRef], close, open);\n useEscapeKey(close, open);\n\n // Reset the query + focus the search box each time the panel opens.\n useEffect(() => {\n if (!open) return;\n setQuery(\"\");\n const id = requestAnimationFrame(() => inputRef.current?.focus());\n return () => cancelAnimationFrame(id);\n }, [open]);\n\n // Debounced, race-safe async search (only when loadOptions is provided).\n useEffect(() => {\n if (!open || !isAsync) return;\n const id = ++reqId.current;\n setAsyncLoading(true);\n const run = async () => {\n try {\n const r = await loadRef.current!(query);\n if (reqId.current === id) setAsyncResults(r);\n } finally {\n if (reqId.current === id) setAsyncLoading(false);\n }\n };\n const t = setTimeout(run, query ? 150 : 0);\n return () => clearTimeout(t);\n }, [query, open, isAsync]);\n\n const clientResults = useMemo(() => {\n if (isAsync) return [];\n const src = options ?? [];\n const q = query.trim().toLowerCase();\n if (!q) return src;\n return src.filter(\n (o) => o.label.toLowerCase().includes(q) || (o.sublabel ?? \"\").toLowerCase().includes(q),\n );\n }, [isAsync, options, query]);\n\n const results = isAsync ? asyncResults : clientResults;\n const busy = Boolean(loading) || (isAsync && asyncLoading);\n\n // Accumulate every option we've seen so a selected value can render its label\n // even after the async list has moved on to other query results.\n const cacheRef = useRef(new Map<V, ComboOption<V>>());\n useEffect(() => {\n for (const o of options ?? []) cacheRef.current.set(o.value, o);\n for (const o of results) cacheRef.current.set(o.value, o);\n }, [options, results]);\n const resolve = (v: V): ComboOption<V> | null =>\n options?.find((o) => o.value === v) ?? cacheRef.current.get(v) ?? null;\n\n // Reset the highlighted row whenever the visible set changes.\n useEffect(() => {\n setActive(0);\n }, [query, open]);\n\n return {\n triggerRef,\n panelRef,\n inputRef,\n open,\n setOpen,\n query,\n setQuery,\n active,\n setActive,\n results,\n busy,\n rect,\n placement,\n cacheRef,\n resolve,\n };\n}\n\n/**\n * The portalled dropdown (search header + result rows + optional create row)\n * shared by the single- and multi-value comboboxes. Left-aligned to the trigger\n * and sized to its width; `multi` swaps the trailing check for a leading\n * checkbox. `onChoose` decides whether to close (single) or stay open (multi).\n */\nexport function ComboboxPanel<V extends string | number>({\n core,\n searchPlaceholder,\n emptyLabel,\n multi,\n isSelected,\n onChoose,\n showCreate,\n onCreate,\n createContent,\n sheetTitle,\n closeLabel,\n}: {\n core: ComboboxCore<V>;\n searchPlaceholder?: string;\n emptyLabel: string;\n multi?: boolean;\n isSelected: (v: V) => boolean;\n onChoose: (o: ComboOption<V>) => void;\n showCreate: boolean;\n onCreate: () => void;\n createContent: ReactNode;\n /** What the sheet calls itself on a phone — the field's label. Ignored on\n * desktop, where the panel sits under the field that already says it. */\n sheetTitle?: ReactNode;\n closeLabel?: string;\n}) {\n const { rect, placement, panelRef, inputRef, query, setQuery, results, busy, active, setActive } =\n core;\n // The phone gets a full-screen sheet instead of an anchored panel (live #200).\n // Same core, same results, same handlers — only the container differs, so the\n // two presentations cannot drift in what they offer.\n const isPhone = useMediaQuery(PHONE_QUERY, false);\n const rowCount = results.length + (showCreate ? 1 : 0);\n\n const onKeyDown = (e: ReactKeyboardEvent) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => Math.min(i + 1, rowCount - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n if (active < results.length) onChoose(results[active]);\n else if (showCreate) onCreate();\n }\n };\n\n if (!core.open || typeof document === \"undefined\") return null;\n\n const list = (\n <ul\n role=\"listbox\"\n aria-multiselectable={multi}\n className={cn(\"min-h-0 flex-1 overflow-y-auto py-1\", isPhone && \"flex-none\")}\n >\n {busy && results.length === 0 && (\n <li className=\"px-3 py-2 text-sm text-slate-500 dark:text-slate-400\">…</li>\n )}\n {!busy && results.length === 0 && !showCreate && (\n <li className=\"px-3 py-2 text-sm text-slate-500 dark:text-slate-400\">{emptyLabel}</li>\n )}\n {results.map((o, i) => {\n const selected = isSelected(o.value);\n // One heading per group rather than a grey suffix on every row (#136), and\n // the thing the native <select>'s <optgroup> used to give the account\n // picker for free (live #200). `results` keeps its source order, so a\n // boundary is simply \"different from the row above\".\n const startsGroup = o.group != null && o.group !== results[i - 1]?.group;\n return (\n // Keyed by group AND value: an option may deliberately appear twice —\n // Keksdose repeats recently-used categories in a \"Recent\" group at the\n // top (live #203) — and a bare value key would collide.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {startsGroup && (\n <li\n role=\"presentation\"\n className={cn(\n \"font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400\",\n isPhone ? \"px-4 pb-1 pt-3 text-xs\" : \"px-3 pb-0.5 pt-2 text-[11px] first:pt-1\",\n )}\n >\n {o.group}\n </li>\n )}\n <li role=\"option\" aria-selected={selected}>\n <button\n type=\"button\"\n onMouseDown={(e) => {\n e.preventDefault();\n onChoose(o);\n }}\n onMouseEnter={() => setActive(i)}\n className={cn(\n isPhone\n ? SHEET_ROW_CLASS\n : \"flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm\",\n i === active && !isPhone\n ? \"bg-slate-100 dark:bg-slate-800\"\n : !isPhone && \"hover:bg-slate-50 dark:hover:bg-slate-800\",\n )}\n >\n {multi && (\n <span\n className={cn(\n \"flex size-4 shrink-0 items-center justify-center rounded border\",\n selected\n ? \"border-slate-900 bg-slate-900 text-white dark:border-slate-100 dark:bg-slate-100 dark:text-slate-900\"\n : \"border-slate-300 bg-white dark:border-slate-600 dark:bg-slate-900\",\n )}\n >\n {selected && <Check className=\"size-3\" />}\n </span>\n )}\n {o.icon && <span className=\"shrink-0\">{o.icon}</span>}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-slate-900 dark:text-slate-100\">{o.label}</span>\n {o.sublabel && (\n <span className=\"block truncate text-xs text-slate-400 dark:text-slate-500\">\n {o.sublabel}\n </span>\n )}\n </span>\n {!multi && selected && <Check className=\"size-4 shrink-0 text-sky-600 dark:text-sky-400\" />}\n </button>\n </li>\n </Fragment>\n );\n })}\n {showCreate && (\n <li role=\"option\" aria-selected={active === results.length}>\n <button\n type=\"button\"\n onMouseDown={(e) => {\n e.preventDefault();\n onCreate();\n }}\n onMouseEnter={() => setActive(results.length)}\n className={cn(\n isPhone\n ? SHEET_ROW_CLASS\n : \"flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm\",\n \"text-slate-700 dark:text-slate-200\",\n active === results.length && !isPhone\n ? \"bg-slate-100 dark:bg-slate-800\"\n : !isPhone && \"hover:bg-slate-50 dark:hover:bg-slate-800\",\n )}\n >\n <Plus className=\"size-4 shrink-0\" />\n <span className=\"truncate\">{createContent}</span>\n </button>\n </li>\n )}\n </ul>\n );\n\n if (isPhone) {\n return (\n <PickerSheet\n open\n onClose={() => core.setOpen(false)}\n title={sheetTitle}\n query={query}\n onQueryChange={setQuery}\n searchPlaceholder={searchPlaceholder}\n inputRef={inputRef}\n closeLabel={closeLabel}\n >\n <div onKeyDown={onKeyDown}>{list}</div>\n </PickerSheet>\n );\n }\n\n if (!rect) return null;\n\n return createPortal(\n <div\n ref={panelRef}\n onKeyDown={onKeyDown}\n // `placement` keeps the panel inside the region actually on screen: on a phone\n // the search box below pulls up the keyboard, and a panel pinned under a\n // low trigger would otherwise sit entirely behind it (feedback #135).\n style={{\n position: \"fixed\",\n top: placement.top,\n left: rect.left,\n width: rect.width,\n minWidth: 220,\n maxHeight: placement.maxHeight,\n }}\n className=\"z-50 flex flex-col rounded-md border border-slate-200 bg-white shadow-lg dark:border-slate-700 dark:bg-slate-900\"\n >\n <DropdownSearchHeader\n query={query}\n onQueryChange={setQuery}\n inputRef={inputRef}\n placeholder={searchPlaceholder}\n />\n {/* One list, two containers: the desktop panel and the phone sheet render\n the same rows through the same handlers (live #200). */}\n {list}\n </div>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
|