@eifi1/ui-kit 0.6.1 → 0.7.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/README.md +69 -33
- package/dist/chart.d.ts +1 -0
- package/dist/components/account-settings.js +3 -2
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +5 -0
- package/dist/components/amount-input.js +6 -4
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +33 -6
- package/dist/components/autocomplete.js +62 -16
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/calculator.d.ts +5 -0
- package/dist/components/calculator.js +2 -1
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.js +2 -1
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/chart.d.ts +33 -2
- package/dist/components/chart.js +20 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +34 -14
- package/dist/components/chip.js +63 -28
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +18 -1
- package/dist/components/choice-card.js +21 -8
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.d.ts +61 -2
- package/dist/components/combobox-core.js +68 -17
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +11 -7
- package/dist/components/combobox.js +51 -28
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.js +3 -3
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +30 -6
- package/dist/components/danger-confirm.js +51 -32
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +6 -3
- package/dist/components/data-table-pagination.js +6 -5
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +332 -248
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +17 -7
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +28 -3
- package/dist/components/dialog-frame.js +47 -11
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.d.ts +22 -2
- package/dist/components/disclosure.js +41 -8
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.d.ts +7 -3
- package/dist/components/dropdown.js +6 -2
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +17 -8
- package/dist/components/entity-combobox.js +7 -5
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/file-button.d.ts +38 -5
- package/dist/components/file-button.js +23 -2
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +11 -5
- package/dist/components/file-dropzone.js +14 -19
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/grouped-picker.js +2 -2
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +8 -3
- package/dist/components/hover-menu.js +88 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-picker.d.ts +10 -1
- package/dist/components/icon-picker.js +5 -1
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/measured-grid.d.ts +165 -0
- package/dist/components/measured-grid.js +559 -0
- package/dist/components/measured-grid.js.map +1 -0
- package/dist/components/mini-calendar.js +7 -8
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/month-picker.js +13 -9
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +2 -2
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +9 -1
- package/dist/components/multi-select.js +22 -8
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +5 -0
- package/dist/components/number-input.d.ts +5 -0
- package/dist/components/number-input.js +3 -3
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +5 -0
- package/dist/components/numpad-sheet.js +3 -3
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/picker-sheet.js +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.js +11 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/series-chart-ticks.d.ts +13 -0
- package/dist/components/series-chart-ticks.js +29 -0
- package/dist/components/series-chart-ticks.js.map +1 -0
- package/dist/components/series-chart.js +33 -27
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +2 -0
- package/dist/components/swatch-picker.d.ts +10 -1
- package/dist/components/swatch-picker.js +2 -0
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +11 -2
- package/dist/components/swipeable-row.js +5 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/tile-radio.js +5 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +2 -0
- package/dist/components/toggle-group.d.ts +13 -3
- package/dist/components/toggle-group.js +21 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +1 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +10 -4
- package/dist/components/tooltip.js +23 -6
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/treemap.d.ts +6 -1
- package/dist/components/treemap.js +16 -6
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +36 -8
- package/dist/components/ui.js +27 -19
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-anchor-dir.d.ts +15 -0
- package/dist/components/use-anchor-dir.js +14 -0
- package/dist/components/use-anchor-dir.js.map +1 -0
- package/dist/components/use-mobile-reveal.d.ts +8 -1
- package/dist/components/use-mobile-reveal.js +3 -2
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +4 -4
- package/dist/components/use-table-state.js +12 -5
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-DM1auUfH.d.ts → data-table-filters-noy0Abvi.d.ts} +40 -7
- package/dist/data-table.d.ts +1 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +22 -3
- package/dist/feedback/feedback-attachment.js +15 -7
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +3 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-windowed-rows.d.ts +29 -0
- package/dist/hooks/use-windowed-rows.js +31 -0
- package/dist/hooks/use-windowed-rows.js.map +1 -0
- package/dist/i18n/defaults.d.ts +5 -0
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +7 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH.d.ts +45 -0
- package/dist/i18n/locales/de-CH.js +21 -0
- package/dist/i18n/locales/de-CH.js.map +1 -0
- package/dist/i18n/locales/de.d.ts +45 -0
- package/dist/i18n/locales/de.js +344 -0
- package/dist/i18n/locales/de.js.map +1 -0
- package/dist/i18n/locales/es.d.ts +45 -0
- package/dist/i18n/locales/es.js +339 -0
- package/dist/i18n/locales/es.js.map +1 -0
- package/dist/i18n/locales/fr.d.ts +45 -0
- package/dist/i18n/locales/fr.js +344 -0
- package/dist/i18n/locales/fr.js.map +1 -0
- package/dist/i18n/locales/hu.d.ts +45 -0
- package/dist/i18n/locales/hu.js +343 -0
- package/dist/i18n/locales/hu.js.map +1 -0
- package/dist/i18n/locales/it.d.ts +45 -0
- package/dist/i18n/locales/it.js +342 -0
- package/dist/i18n/locales/it.js.map +1 -0
- package/dist/i18n/locales/zh.d.ts +45 -0
- package/dist/i18n/locales/zh.js +338 -0
- package/dist/i18n/locales/zh.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/direction.d.ts +24 -0
- package/dist/lib/direction.js +19 -0
- package/dist/lib/direction.js.map +1 -0
- package/dist/rhf/form.d.ts +2 -0
- package/dist/rhf/form.js.map +1 -1
- package/dist/rhf.d.ts +2 -0
- package/dist/search/command-palette.d.ts +5 -0
- package/dist/search/command-palette.js +80 -50
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +13 -1
- package/dist/shell/app-shell.js +134 -85
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +1 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +7 -3
- package/dist/shell/topbar-controls.js +38 -31
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +7 -1
- package/dist/theme/chart-palette.js +2 -1
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/tour/tour.d.ts +11 -5
- package/dist/tour/tour.js +124 -58
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/use-wizard.js +30 -13
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.js +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +12 -2
- package/src/components/account-settings.tsx +6 -2
- package/src/components/amount-input.tsx +10 -8
- package/src/components/autocomplete.tsx +110 -24
- package/src/components/calculator.tsx +5 -1
- package/src/components/chart-zoom.tsx +8 -1
- package/src/components/chart.tsx +68 -7
- package/src/components/chip.tsx +131 -49
- package/src/components/choice-card.tsx +77 -36
- package/src/components/combobox-core.tsx +147 -20
- package/src/components/combobox.tsx +100 -44
- package/src/components/currency-select.tsx +6 -6
- package/src/components/danger-confirm.tsx +90 -36
- package/src/components/data-table-pagination.tsx +32 -19
- package/src/components/data-table.tsx +198 -36
- package/src/components/date-picker.tsx +24 -7
- package/src/components/dialog-frame.tsx +97 -22
- package/src/components/disclosure.tsx +75 -13
- package/src/components/dropdown.tsx +17 -5
- package/src/components/entity-combobox.tsx +26 -13
- package/src/components/file-button.tsx +74 -5
- package/src/components/file-dropzone.tsx +44 -35
- package/src/components/grouped-picker.tsx +2 -2
- package/src/components/hover-menu.tsx +139 -10
- package/src/components/icon-picker.tsx +14 -1
- package/src/components/measured-grid.tsx +852 -0
- package/src/components/mini-calendar.tsx +11 -8
- package/src/components/month-picker.tsx +18 -11
- package/src/components/multi-entity-combobox.tsx +2 -2
- package/src/components/multi-select.tsx +34 -7
- package/src/components/number-input.tsx +4 -4
- package/src/components/numpad-sheet.tsx +5 -3
- package/src/components/picker-sheet.tsx +1 -1
- package/src/components/popover.tsx +18 -3
- package/src/components/series-chart-ticks.ts +52 -0
- package/src/components/series-chart.tsx +39 -31
- package/src/components/swatch-picker.tsx +11 -0
- package/src/components/swipeable-row.tsx +21 -6
- package/src/components/tile-radio.tsx +5 -7
- package/src/components/toggle-group.tsx +39 -3
- package/src/components/toggle-legend.tsx +5 -2
- package/src/components/tooltip.tsx +40 -12
- package/src/components/treemap.tsx +28 -5
- package/src/components/ui.tsx +79 -33
- package/src/components/use-anchor-dir.ts +20 -0
- package/src/components/use-mobile-reveal.ts +10 -2
- package/src/components/use-table-state.ts +44 -7
- package/src/data-table.ts +1 -0
- package/src/feedback/feedback-attachment.tsx +36 -9
- package/src/feedback/feedback-dialog.tsx +3 -2
- package/src/hooks/use-windowed-rows.ts +69 -0
- package/src/i18n/defaults.ts +4 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/de-CH.ts +30 -0
- package/src/i18n/locales/de.ts +365 -0
- package/src/i18n/locales/es.ts +358 -0
- package/src/i18n/locales/fr.ts +371 -0
- package/src/i18n/locales/hu.ts +359 -0
- package/src/i18n/locales/it.ts +360 -0
- package/src/i18n/locales/zh.ts +348 -0
- package/src/index.ts +10 -1
- package/src/lib/direction.ts +37 -0
- package/src/rhf/form.tsx +21 -0
- package/src/search/command-palette.tsx +77 -19
- package/src/shell/app-shell.tsx +201 -130
- package/src/shell/topbar-action-menu.tsx +1 -1
- package/src/shell/topbar-controls.tsx +19 -8
- package/src/theme/chart-palette.ts +9 -2
- package/src/tour/tour.tsx +119 -18
- package/src/wizard/use-wizard.ts +55 -17
- package/src/wizard/wizard-summary.tsx +1 -1
|
@@ -13,14 +13,43 @@ interface ChartContainerProps extends ComponentProps<"div"> {
|
|
|
13
13
|
config: ChartConfig;
|
|
14
14
|
children: ComponentProps<typeof ResponsiveContainer>["children"];
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* The shell every chart in the kit is drawn inside.
|
|
18
|
+
*
|
|
19
|
+
* **The plot is not mirrored in RTL.** An x axis is a number line or a timeline, and
|
|
20
|
+
* both run left-to-right in Hebrew and Arabic charts as much as in English ones; a
|
|
21
|
+
* time axis flipped to run right-to-left is the surprise, not the courtesy. So no
|
|
22
|
+
* `reversed` on any axis, and the SVG is pinned to `direction: ltr` — without that it
|
|
23
|
+
* inherits the page's `rtl`, which flips what `text-anchor: end` means, and the tick
|
|
24
|
+
* labels of a left-hand y axis are drawn INTO the plot instead of beside it. Text
|
|
25
|
+
* inside the SVG still shapes by its own script; only the anchoring is physical.
|
|
26
|
+
* The HTML around the plot — legend, tooltip, the zoom reset button — follows the
|
|
27
|
+
* page's direction like any other markup. A consumer who does want a mirrored
|
|
28
|
+
* category axis passes `reversed` on its own `XAxis`.
|
|
29
|
+
*/
|
|
16
30
|
declare function ChartContainer({ id, className, children, config, ...props }: ChartContainerProps): react.JSX.Element;
|
|
17
|
-
|
|
31
|
+
/**
|
|
32
|
+
* recharts' `Tooltip`, with `filterNull` defaulting to **false**.
|
|
33
|
+
*
|
|
34
|
+
* recharts drops every payload entry whose value is `null`/`undefined` before the
|
|
35
|
+
* content sees it, so the missing-value path {@link ChartTooltipContent} documents —
|
|
36
|
+
* the em dash, `formatValue(undefined)` — never ran under the default: a series with a
|
|
37
|
+
* gap at the cursor simply vanished from the tooltip, which reads as "this series does
|
|
38
|
+
* not exist here" rather than "it has no value here". Kept, the entry prints "—".
|
|
39
|
+
*
|
|
40
|
+
* The other half of that filter — dropping series drawn with `hide` — is redone by
|
|
41
|
+
* `ChartTooltipContent` itself (honouring `includeHidden`), so a legend toggle still
|
|
42
|
+
* takes a series out of the tooltip. Pass `filterNull` to get recharts' behaviour back.
|
|
43
|
+
*/
|
|
44
|
+
declare function ChartTooltip(props: ComponentProps<typeof Tooltip>): react.JSX.Element;
|
|
18
45
|
declare const ChartLegend: react.MemoExoticComponent<(outsideProps: recharts.LegendProps) => React.ReactPortal | null>;
|
|
19
46
|
interface TooltipPayloadItem {
|
|
20
47
|
dataKey?: string | number;
|
|
21
48
|
name?: string | number;
|
|
22
49
|
value?: number | string;
|
|
23
50
|
color?: string;
|
|
51
|
+
/** A series drawn with `hide` — dropped here, see {@link ChartTooltip}. */
|
|
52
|
+
hide?: boolean;
|
|
24
53
|
payload?: Record<string, unknown>;
|
|
25
54
|
}
|
|
26
55
|
interface ChartTooltipContentProps {
|
|
@@ -54,8 +83,10 @@ interface ChartTooltipContentProps {
|
|
|
54
83
|
* spill past the right edge of this (usually horizontally-scrolling) container
|
|
55
84
|
* — e.g. a wide chart whose rightmost bars sat off-screen (feedback #77). */
|
|
56
85
|
boundaryRef?: RefObject<HTMLElement | null>;
|
|
86
|
+
/** Injected by recharts: keep series drawn with `hide` in the tooltip. */
|
|
87
|
+
includeHidden?: boolean;
|
|
57
88
|
}
|
|
58
|
-
declare function ChartTooltipContent({ active, payload, label, indicator, hideLabel, labelFormatter, valueFormatter, formatValue, coordinate, boundaryRef, }: ChartTooltipContentProps): react.JSX.Element | null;
|
|
89
|
+
declare function ChartTooltipContent({ active, payload, label, indicator, hideLabel, labelFormatter, valueFormatter, formatValue, coordinate, boundaryRef, includeHidden, }: ChartTooltipContentProps): react.JSX.Element | null;
|
|
59
90
|
interface LegendPayloadItem {
|
|
60
91
|
value?: string;
|
|
61
92
|
dataKey?: string | number;
|
package/dist/components/chart.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
Tooltip as RechartsTooltip
|
|
8
8
|
} from "recharts";
|
|
9
9
|
import { cn } from "../lib/cn.js";
|
|
10
|
+
import { isRtl } from "../lib/direction.js";
|
|
10
11
|
const ChartContext = createContext(null);
|
|
11
12
|
function useChart() {
|
|
12
13
|
const ctx = useContext(ChartContext);
|
|
@@ -25,6 +26,8 @@ function ChartContainer({ id, className, children, config, ...props }) {
|
|
|
25
26
|
// a horizontal drag is handed to recharts for tooltip scrubbing,
|
|
26
27
|
// instead of the page scrolling underneath the gesture (feedback #292).
|
|
27
28
|
"flex w-full touch-pan-y justify-center text-xs",
|
|
29
|
+
// physical anchoring for the unmirrored plot (see the doc comment above)
|
|
30
|
+
"[&_.recharts-surface]:[direction:ltr]",
|
|
28
31
|
// muted axis labels + gridlines; one class each, since the tokens
|
|
29
32
|
// already carry the light/dark values
|
|
30
33
|
"[&_.recharts-cartesian-axis-tick_text]:fill-[var(--text-muted)]",
|
|
@@ -47,6 +50,10 @@ function ChartContainer({ id, className, children, config, ...props }) {
|
|
|
47
50
|
}
|
|
48
51
|
const CSS_IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
49
52
|
const CSS_COLOR = /^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|(?:rgba?|hsla?)\([0-9A-Za-z.,%/ -]*\)|var\(--[A-Za-z0-9_-]+\))$/;
|
|
53
|
+
function paintedColor(key, entry) {
|
|
54
|
+
if (!entry?.color) return void 0;
|
|
55
|
+
return CSS_IDENT.test(key) && CSS_COLOR.test(entry.color) ? entry.color : void 0;
|
|
56
|
+
}
|
|
50
57
|
function ChartStyle({ id, config }) {
|
|
51
58
|
const entries = Object.entries(config).filter(([key, c]) => {
|
|
52
59
|
if (!c.color) return false;
|
|
@@ -66,7 +73,9 @@ ${entries.map(([key, c]) => ` --color-${key}: ${c.color};`).join("\n")}
|
|
|
66
73
|
}`;
|
|
67
74
|
return /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: css } });
|
|
68
75
|
}
|
|
69
|
-
|
|
76
|
+
function ChartTooltip(props) {
|
|
77
|
+
return /* @__PURE__ */ jsx(RechartsTooltip, { filterNull: false, ...props });
|
|
78
|
+
}
|
|
70
79
|
const ChartLegend = RechartsLegend;
|
|
71
80
|
const NO_VALUE = "\u2014";
|
|
72
81
|
function tooltipValue(value) {
|
|
@@ -84,15 +93,18 @@ function ChartTooltipContent({
|
|
|
84
93
|
valueFormatter,
|
|
85
94
|
formatValue,
|
|
86
95
|
coordinate,
|
|
87
|
-
boundaryRef
|
|
96
|
+
boundaryRef,
|
|
97
|
+
includeHidden = false
|
|
88
98
|
}) {
|
|
89
99
|
const config = useChart();
|
|
90
100
|
const tipRef = useRef(null);
|
|
91
|
-
|
|
101
|
+
const items = includeHidden ? payload : payload?.filter((item) => item.hide !== true);
|
|
102
|
+
if (!active || !items?.length) return null;
|
|
92
103
|
let flip = false;
|
|
93
104
|
const boundary = boundaryRef?.current;
|
|
94
105
|
if (boundary && coordinate?.x != null) {
|
|
95
|
-
const
|
|
106
|
+
const offset = isRtl(boundary) ? boundary.scrollWidth - boundary.clientWidth + boundary.scrollLeft : boundary.scrollLeft;
|
|
107
|
+
const visibleX = coordinate.x - offset;
|
|
96
108
|
const tipWidth = tipRef.current?.offsetWidth ?? 160;
|
|
97
109
|
flip = visibleX + tipWidth + 12 > boundary.clientWidth;
|
|
98
110
|
}
|
|
@@ -104,13 +116,13 @@ function ChartTooltipContent({
|
|
|
104
116
|
className: "min-w-[9rem] rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] px-2.5 py-2 text-xs shadow-xl",
|
|
105
117
|
children: [
|
|
106
118
|
!hideLabel && label != null && /* @__PURE__ */ jsx("div", { className: "mb-1.5 font-medium text-[var(--text-primary)]", children: labelFormatter ? labelFormatter(label) : label }),
|
|
107
|
-
/* @__PURE__ */ jsx("div", { className: "grid gap-1.5", children:
|
|
119
|
+
/* @__PURE__ */ jsx("div", { className: "grid gap-1.5", children: items.map((item, i) => {
|
|
108
120
|
const dk = item.dataKey != null ? String(item.dataKey) : void 0;
|
|
109
121
|
const nm = item.name != null ? String(item.name) : void 0;
|
|
110
122
|
const cfgKey = dk && config[dk] ? dk : nm && config[nm] ? nm : dk ?? nm ?? String(i);
|
|
111
123
|
const series = config[cfgKey];
|
|
112
124
|
const fill = typeof item.payload?.fill === "string" ? item.payload.fill : void 0;
|
|
113
|
-
const color = series
|
|
125
|
+
const color = paintedColor(cfgKey, series) ?? fill ?? item.color ?? "#64748b";
|
|
114
126
|
const name = series?.label ?? nm ?? cfgKey;
|
|
115
127
|
const raw = tooltipValue(item.value);
|
|
116
128
|
let shown;
|
|
@@ -129,7 +141,7 @@ function ChartTooltipContent({
|
|
|
129
141
|
}
|
|
130
142
|
),
|
|
131
143
|
/* @__PURE__ */ jsx("span", { className: "text-[var(--text-muted)]", children: name }),
|
|
132
|
-
/* @__PURE__ */ jsx("span", { "data-private": true, className: "
|
|
144
|
+
/* @__PURE__ */ jsx("span", { "data-private": true, className: "ms-auto font-mono font-medium tabular-nums text-[var(--text-primary)]", children: shown })
|
|
133
145
|
] }, `${cfgKey}-${i}`);
|
|
134
146
|
}) })
|
|
135
147
|
]
|
|
@@ -150,7 +162,7 @@ function ChartLegendContent({
|
|
|
150
162
|
const vv = item.value != null ? String(item.value) : void 0;
|
|
151
163
|
const cfgKey = dk && config[dk] ? dk : vv && config[vv] ? vv : dk ?? vv ?? String(i);
|
|
152
164
|
const series = config[cfgKey];
|
|
153
|
-
const color = series
|
|
165
|
+
const color = paintedColor(cfgKey, series) ?? item.color ?? "#64748b";
|
|
154
166
|
const label = series?.label ?? vv ?? cfgKey;
|
|
155
167
|
const off = hidden?.has(cfgKey) ?? false;
|
|
156
168
|
const dimmed = off || activeKey != null && activeKey !== cfgKey;
|
|
@@ -1 +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 onto this kit's own tokens (--text-muted, --border, --bg-surface\n// …), which follow the active palette preset and flip with the theme by\n// themselves — hence no `dark:` variants anywhere below.\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 // The id is pasted into an UNQUOTED `[data-chart=…]` selector below, so it has to be\n // a CSS identifier: one stray space or `#` and the browser discards the whole rule,\n // taking every series colour with it — silently, because an invalid rule is not an\n // error anywhere. `useId()` gives `:r0:`, and a consumer-supplied id gives whatever\n // they typed, so both are filtered rather than just the colons.\n const chartId = `chart-${(id ?? uid).replace(/[^A-Za-z0-9_-]/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; one class each, since the tokens\n // already carry the light/dark values\n \"[&_.recharts-cartesian-axis-tick_text]:fill-[var(--text-muted)]\",\n \"[&_.recharts-cartesian-grid_line]:stroke-[var(--border)]/70\",\n // hairline hover cursor instead of the heavy default\n \"[&_.recharts-curve.recharts-tooltip-cursor]:stroke-[var(--border-strong)]\",\n \"[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-[var(--bg-hover)]\",\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/**\n * A config key, which becomes the tail of a custom-property NAME.\n *\n * `dangerouslySetInnerHTML` means what it says: nothing below is escaped by React, and\n * a `ChartConfig` is not necessarily written by hand — kastlan builds one from category\n * rows in its database, so these keys are user data. A key containing `}` closes this\n * rule, and whatever follows it in that key becomes a NEW rule — one that applies to\n * the whole document rather than to this one chart.\n */\nconst CSS_IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\n/**\n * A config colour, which becomes a declaration VALUE.\n *\n * Every form a palette actually produces: both hex spellings, the functional notations\n * — with the argument list restricted to the characters those functions can legally\n * contain, so no `;`, no `}` and no nested `url(` — and `var(--token)`, which is what\n * this kit's own `paletteFor`/`CHART_COLORS` return. `var()`'s optional FALLBACK is\n * deliberately not accepted: its second argument is an arbitrary value, i.e. the same\n * hole in a shape that looks safe.\n */\nconst CSS_COLOR =\n /^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|(?:rgba?|hsla?)\\([0-9A-Za-z.,%/ -]*\\)|var\\(--[A-Za-z0-9_-]+\\))$/;\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(([key, c]) => {\n if (!c.color) return false;\n // Skipping the entry costs that one series its `var(--color-…)` and leaves the rest\n // of the chart painted; emitting it unchecked costs the page. The warning is so the\n // consumer finds out from their console rather than from a colourless bar.\n if (!CSS_IDENT.test(key)) {\n console.warn(`[ui-kit] chart config key is not a CSS identifier, skipped: ${key}`);\n return false;\n }\n if (!CSS_COLOR.test(c.color)) {\n console.warn(`[ui-kit] chart config colour for \"${key}\" is not a colour, skipped: ${c.color}`);\n return false;\n }\n return true;\n });\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\n/**\n * What a series has no value for at this point: a gap in a history, a projection that\n * has not happened yet, a field that did not exist at an earlier capture.\n *\n * An em dash rather than an empty cell, so the row still lines up with its siblings and\n * the reader can see that the series WAS asked and had nothing, rather than wondering\n * whether it was dropped. It is a mark, not a word, so there is nothing here to\n * translate and no label prop to resolve; a caller who wants words passes\n * `formatValue`, which is handed the `undefined` and decides.\n */\nconst NO_VALUE = \"—\";\n\n/**\n * The number this payload item carries, or `undefined` when it carries none.\n *\n * This was `item.value ?? 0`, so every one of the absences above printed a confident\n * \"0\" that appears nowhere in the data — a fabricated figure in a tooltip, which is the\n * one place a reader goes to check an exact number. keksdose found it and wrapped the\n * component rather than trust it. `\"\"` and a non-numeric string are the same absence\n * spelled differently (recharts hands a string value straight through from the data),\n * and are answered the same way.\n */\nfunction tooltipValue(value: number | string | undefined): number | undefined {\n if (value == null || value === \"\") return undefined;\n const n = typeof value === \"string\" ? Number(value) : value;\n return Number.isFinite(n) ? n : undefined;\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 /** Formats one series' value for display. NOT called when the series has no value at\n * this point — {@link ChartTooltipContentProps.formatValue} is the one that is. */\n valueFormatter?: (value: number) => ReactNode;\n /** Formats one series' value INCLUDING its absence: handed `undefined` for a gap, a\n * projection that has not happened yet, or a field that did not exist at an earlier\n * capture, and whatever it returns is rendered as-is. Only the caller knows whether\n * that absence reads as a dash, a \"not yet\" or a genuine zero, so this is how it\n * says. Supplying it wins over `valueFormatter` for every value.\n *\n * It sits NEXT to `valueFormatter` rather than widening it because that prop's\n * parameter is contextually typed at every call site there is — `(v) => money(v)` —\n * so widening it to `number | undefined` is a compile error in all three consuming\n * apps, which is a breaking change this module does not get to make by itself. */\n formatValue?: (value: number | undefined) => ReactNode;\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 formatValue,\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-[var(--border)] bg-[var(--bg-surface)] px-2.5 py-2 text-xs shadow-xl\"\n >\n {!hideLabel && label != null && (\n <div className=\"mb-1.5 font-medium text-[var(--text-primary)]\">\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 // The data point's own `fill` before recharts' `item.color`: a treemap tile,\n // or a bar with a per-cell colour, IS the colour the reader is pointing at,\n // while `item.color` is the series-wide one — every tile's swatch came out\n // `--chart-1` whatever colour the tile was.\n const color = series?.color ?? fill ?? item.color ?? \"#64748b\";\n const name = series?.label ?? nm ?? cfgKey;\n const raw = tooltipValue(item.value);\n let shown: ReactNode;\n if (formatValue) shown = formatValue(raw);\n else if (raw == null) shown = NO_VALUE;\n else shown = valueFormatter ? valueFormatter(raw) : raw;\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-[var(--text-muted)]\">{name}</span>\n <span data-private className=\"ml-auto font-mono font-medium tabular-nums text-[var(--text-primary)]\">\n {shown}\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 /**\n * TOGGLE mode (recommended): the series currently switched off. Each entry is a\n * toggle button — pressed while its series is shown — and a hidden one is drawn\n * faded with its label struck through. Several can be off at once; pair with\n * `toggleHidden(hidden, key)` in `onItemClick` and `hide` on the chart's series.\n */\n hiddenKeys?: ReadonlySet<string> | readonly string[];\n /**\n * ISOLATE mode: the one series shown in full, every other entry dimmed. Kept for\n * the charts built on it; a legend where each entry switches its own series is\n * what readers expect from a legend, so prefer `hiddenKeys`.\n */\n activeKey?: string | null;\n}\n\nexport function ChartLegendContent({\n payload,\n onItemClick,\n hiddenKeys,\n activeKey,\n}: ChartLegendContentProps) {\n const hidden = hiddenKeys ? new Set(hiddenKeys) : null;\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 off = hidden?.has(cfgKey) ?? false;\n const dimmed = off || (activeKey != null && activeKey !== cfgKey);\n const className = cn(\n \"flex items-center gap-1.5 text-[var(--text-secondary)] transition-opacity\",\n onItemClick ? \"cursor-pointer hover:opacity-80\" : \"cursor-default\",\n dimmed && \"opacity-40\",\n );\n const swatch = (\n <span\n aria-hidden\n className=\"h-2.5 w-2.5 shrink-0 rounded-[3px]\"\n style={{ backgroundColor: color } as CSSProperties}\n />\n );\n // A legend with no `onItemClick` is a KEY, not a control — and that is every\n // legend in kastlan, where this rendered N focusable buttons per chart that did\n // nothing when activated. A keyboard user paid the tab stops for all of them,\n // and a screen reader announced each one as a button, promising an action there\n // was none of. The interactive branch is unchanged.\n return onItemClick ? (\n <button\n key={`${cfgKey}-${i}`}\n type=\"button\"\n onClick={() => onItemClick(cfgKey)}\n // Toggles report their state: in toggle mode pressed = the series is shown;\n // in isolate mode pressed = the isolated one. With neither supplied the\n // legend knows nothing about state, and a pressed=\"false\" it cannot vouch\n // for would lie — so it says nothing.\n aria-pressed={\n hidden ? !off : activeKey === undefined ? undefined : activeKey === cfgKey\n }\n className={className}\n >\n {swatch}\n <span className={cn(off && \"line-through\")}>{label}</span>\n </button>\n ) : (\n <span key={`${cfgKey}-${i}`} className={className}>\n {swatch}\n {label}\n </span>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
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 onto this kit's own tokens (--text-muted, --border, --bg-surface\n// …), which follow the active palette preset and flip with the theme by\n// themselves — hence no `dark:` variants anywhere below.\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\";\nimport { isRtl } from \"../lib/direction\";\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\n/**\n * The shell every chart in the kit is drawn inside.\n *\n * **The plot is not mirrored in RTL.** An x axis is a number line or a timeline, and\n * both run left-to-right in Hebrew and Arabic charts as much as in English ones; a\n * time axis flipped to run right-to-left is the surprise, not the courtesy. So no\n * `reversed` on any axis, and the SVG is pinned to `direction: ltr` — without that it\n * inherits the page's `rtl`, which flips what `text-anchor: end` means, and the tick\n * labels of a left-hand y axis are drawn INTO the plot instead of beside it. Text\n * inside the SVG still shapes by its own script; only the anchoring is physical.\n * The HTML around the plot — legend, tooltip, the zoom reset button — follows the\n * page's direction like any other markup. A consumer who does want a mirrored\n * category axis passes `reversed` on its own `XAxis`.\n */\nexport function ChartContainer({ id, className, children, config, ...props }: ChartContainerProps) {\n const uid = useId();\n // The id is pasted into an UNQUOTED `[data-chart=…]` selector below, so it has to be\n // a CSS identifier: one stray space or `#` and the browser discards the whole rule,\n // taking every series colour with it — silently, because an invalid rule is not an\n // error anywhere. `useId()` gives `:r0:`, and a consumer-supplied id gives whatever\n // they typed, so both are filtered rather than just the colons.\n const chartId = `chart-${(id ?? uid).replace(/[^A-Za-z0-9_-]/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 // physical anchoring for the unmirrored plot (see the doc comment above)\n \"[&_.recharts-surface]:[direction:ltr]\",\n // muted axis labels + gridlines; one class each, since the tokens\n // already carry the light/dark values\n \"[&_.recharts-cartesian-axis-tick_text]:fill-[var(--text-muted)]\",\n \"[&_.recharts-cartesian-grid_line]:stroke-[var(--border)]/70\",\n // hairline hover cursor instead of the heavy default\n \"[&_.recharts-curve.recharts-tooltip-cursor]:stroke-[var(--border-strong)]\",\n \"[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-[var(--bg-hover)]\",\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/**\n * A config key, which becomes the tail of a custom-property NAME.\n *\n * `dangerouslySetInnerHTML` means what it says: nothing below is escaped by React, and\n * a `ChartConfig` is not necessarily written by hand — kastlan builds one from category\n * rows in its database, so these keys are user data. A key containing `}` closes this\n * rule, and whatever follows it in that key becomes a NEW rule — one that applies to\n * the whole document rather than to this one chart.\n */\nconst CSS_IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\n/**\n * A config colour, which becomes a declaration VALUE.\n *\n * Every form a palette actually produces: both hex spellings, the functional notations\n * — with the argument list restricted to the characters those functions can legally\n * contain, so no `;`, no `}` and no nested `url(` — and `var(--token)`, which is what\n * this kit's own `paletteFor`/`CHART_COLORS` return. `var()`'s optional FALLBACK is\n * deliberately not accepted: its second argument is an arbitrary value, i.e. the same\n * hole in a shape that looks safe.\n */\nconst CSS_COLOR =\n /^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|(?:rgba?|hsla?)\\([0-9A-Za-z.,%/ -]*\\)|var\\(--[A-Za-z0-9_-]+\\))$/;\n\n/**\n * The colour an entry is painted with, or `undefined` when the style block refuses it.\n *\n * The one gate both sides go through: {@link ChartStyle} emits exactly these, and the\n * legend and tooltip swatches show exactly these. They used to read `config[key].color`\n * directly, so an entry the style block had skipped still showed its configured colour\n * in the key — a swatch promising a colour the series was never painted in.\n */\nfunction paintedColor(key: string, entry: ChartSeriesConfig | undefined): string | undefined {\n if (!entry?.color) return undefined;\n return CSS_IDENT.test(key) && CSS_COLOR.test(entry.color) ? entry.color : undefined;\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(([key, c]) => {\n if (!c.color) return false;\n // Skipping the entry costs that one series its `var(--color-…)` and leaves the rest\n // of the chart painted; emitting it unchecked costs the page. The warning is so the\n // consumer finds out from their console rather than from a colourless bar.\n if (!CSS_IDENT.test(key)) {\n console.warn(`[ui-kit] chart config key is not a CSS identifier, skipped: ${key}`);\n return false;\n }\n if (!CSS_COLOR.test(c.color)) {\n console.warn(`[ui-kit] chart config colour for \"${key}\" is not a colour, skipped: ${c.color}`);\n return false;\n }\n return true;\n });\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\n/**\n * recharts' `Tooltip`, with `filterNull` defaulting to **false**.\n *\n * recharts drops every payload entry whose value is `null`/`undefined` before the\n * content sees it, so the missing-value path {@link ChartTooltipContent} documents —\n * the em dash, `formatValue(undefined)` — never ran under the default: a series with a\n * gap at the cursor simply vanished from the tooltip, which reads as \"this series does\n * not exist here\" rather than \"it has no value here\". Kept, the entry prints \"—\".\n *\n * The other half of that filter — dropping series drawn with `hide` — is redone by\n * `ChartTooltipContent` itself (honouring `includeHidden`), so a legend toggle still\n * takes a series out of the tooltip. Pass `filterNull` to get recharts' behaviour back.\n */\nexport function ChartTooltip(props: ComponentProps<typeof RechartsTooltip>) {\n return <RechartsTooltip filterNull={false} {...props} />;\n}\nexport const ChartLegend = RechartsLegend;\n\ninterface TooltipPayloadItem {\n dataKey?: string | number;\n name?: string | number;\n value?: number | string;\n color?: string;\n /** A series drawn with `hide` — dropped here, see {@link ChartTooltip}. */\n hide?: boolean;\n payload?: Record<string, unknown>;\n}\n\n/**\n * What a series has no value for at this point: a gap in a history, a projection that\n * has not happened yet, a field that did not exist at an earlier capture.\n *\n * An em dash rather than an empty cell, so the row still lines up with its siblings and\n * the reader can see that the series WAS asked and had nothing, rather than wondering\n * whether it was dropped. It is a mark, not a word, so there is nothing here to\n * translate and no label prop to resolve; a caller who wants words passes\n * `formatValue`, which is handed the `undefined` and decides.\n */\nconst NO_VALUE = \"—\";\n\n/**\n * The number this payload item carries, or `undefined` when it carries none.\n *\n * This was `item.value ?? 0`, so every one of the absences above printed a confident\n * \"0\" that appears nowhere in the data — a fabricated figure in a tooltip, which is the\n * one place a reader goes to check an exact number. keksdose found it and wrapped the\n * component rather than trust it. `\"\"` and a non-numeric string are the same absence\n * spelled differently (recharts hands a string value straight through from the data),\n * and are answered the same way.\n */\nfunction tooltipValue(value: number | string | undefined): number | undefined {\n if (value == null || value === \"\") return undefined;\n const n = typeof value === \"string\" ? Number(value) : value;\n return Number.isFinite(n) ? n : undefined;\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 /** Formats one series' value for display. NOT called when the series has no value at\n * this point — {@link ChartTooltipContentProps.formatValue} is the one that is. */\n valueFormatter?: (value: number) => ReactNode;\n /** Formats one series' value INCLUDING its absence: handed `undefined` for a gap, a\n * projection that has not happened yet, or a field that did not exist at an earlier\n * capture, and whatever it returns is rendered as-is. Only the caller knows whether\n * that absence reads as a dash, a \"not yet\" or a genuine zero, so this is how it\n * says. Supplying it wins over `valueFormatter` for every value.\n *\n * It sits NEXT to `valueFormatter` rather than widening it because that prop's\n * parameter is contextually typed at every call site there is — `(v) => money(v)` —\n * so widening it to `number | undefined` is a compile error in all three consuming\n * apps, which is a breaking change this module does not get to make by itself. */\n formatValue?: (value: number | undefined) => ReactNode;\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 /** Injected by recharts: keep series drawn with `hide` in the tooltip. */\n includeHidden?: boolean;\n}\n\nexport function ChartTooltipContent({\n active,\n payload,\n label,\n indicator = \"dot\",\n hideLabel = false,\n labelFormatter,\n valueFormatter,\n formatValue,\n coordinate,\n boundaryRef,\n includeHidden = false,\n}: ChartTooltipContentProps) {\n const config = useChart();\n const tipRef = useRef<HTMLDivElement>(null);\n // `ChartTooltip` keeps null values (so they can print \"—\"), which also keeps the\n // series a legend toggle switched off with `hide`; those go here instead.\n const items = includeHidden ? payload : payload?.filter((item) => item.hide !== true);\n if (!active || !items?.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 //\n // Both the cursor and the flip are PHYSICAL (the plot is never mirrored, see\n // `ChartContainer`), but an RTL scroller counts `scrollLeft` from its right edge —\n // 0 at the start, negative going left — so there the content's left edge sits\n // `scrollWidth - clientWidth + scrollLeft` px to the left of the visible one.\n let flip = false;\n const boundary = boundaryRef?.current;\n if (boundary && coordinate?.x != null) {\n const offset = isRtl(boundary)\n ? boundary.scrollWidth - boundary.clientWidth + boundary.scrollLeft\n : boundary.scrollLeft;\n const visibleX = coordinate.x - offset;\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-[var(--border)] bg-[var(--bg-surface)] px-2.5 py-2 text-xs shadow-xl\"\n >\n {!hideLabel && label != null && (\n <div className=\"mb-1.5 font-medium text-[var(--text-primary)]\">\n {labelFormatter ? labelFormatter(label) : label}\n </div>\n )}\n <div className=\"grid gap-1.5\">\n {items.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 // The data point's own `fill` before recharts' `item.color`: a treemap tile,\n // or a bar with a per-cell colour, IS the colour the reader is pointing at,\n // while `item.color` is the series-wide one — every tile's swatch came out\n // `--chart-1` whatever colour the tile was.\n const color = paintedColor(cfgKey, series) ?? fill ?? item.color ?? \"#64748b\";\n const name = series?.label ?? nm ?? cfgKey;\n const raw = tooltipValue(item.value);\n let shown: ReactNode;\n if (formatValue) shown = formatValue(raw);\n else if (raw == null) shown = NO_VALUE;\n else shown = valueFormatter ? valueFormatter(raw) : raw;\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-[var(--text-muted)]\">{name}</span>\n <span data-private className=\"ms-auto font-mono font-medium tabular-nums text-[var(--text-primary)]\">\n {shown}\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 /**\n * TOGGLE mode (recommended): the series currently switched off. Each entry is a\n * toggle button — pressed while its series is shown — and a hidden one is drawn\n * faded with its label struck through. Several can be off at once; pair with\n * `toggleHidden(hidden, key)` in `onItemClick` and `hide` on the chart's series.\n */\n hiddenKeys?: ReadonlySet<string> | readonly string[];\n /**\n * ISOLATE mode: the one series shown in full, every other entry dimmed. Kept for\n * the charts built on it; a legend where each entry switches its own series is\n * what readers expect from a legend, so prefer `hiddenKeys`.\n */\n activeKey?: string | null;\n}\n\nexport function ChartLegendContent({\n payload,\n onItemClick,\n hiddenKeys,\n activeKey,\n}: ChartLegendContentProps) {\n const hidden = hiddenKeys ? new Set(hiddenKeys) : null;\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 = paintedColor(cfgKey, series) ?? item.color ?? \"#64748b\";\n const label = series?.label ?? vv ?? cfgKey;\n const off = hidden?.has(cfgKey) ?? false;\n const dimmed = off || (activeKey != null && activeKey !== cfgKey);\n const className = cn(\n \"flex items-center gap-1.5 text-[var(--text-secondary)] transition-opacity\",\n onItemClick ? \"cursor-pointer hover:opacity-80\" : \"cursor-default\",\n dimmed && \"opacity-40\",\n );\n const swatch = (\n <span\n aria-hidden\n className=\"h-2.5 w-2.5 shrink-0 rounded-[3px]\"\n style={{ backgroundColor: color } as CSSProperties}\n />\n );\n // A legend with no `onItemClick` is a KEY, not a control — and that is every\n // legend in kastlan, where this rendered N focusable buttons per chart that did\n // nothing when activated. A keyboard user paid the tab stops for all of them,\n // and a screen reader announced each one as a button, promising an action there\n // was none of. The interactive branch is unchanged.\n return onItemClick ? (\n <button\n key={`${cfgKey}-${i}`}\n type=\"button\"\n onClick={() => onItemClick(cfgKey)}\n // Toggles report their state: in toggle mode pressed = the series is shown;\n // in isolate mode pressed = the isolated one. With neither supplied the\n // legend knows nothing about state, and a pressed=\"false\" it cannot vouch\n // for would lie — so it says nothing.\n aria-pressed={\n hidden ? !off : activeKey === undefined ? undefined : activeKey === cfgKey\n }\n className={className}\n >\n {swatch}\n <span className={cn(off && \"line-through\")}>{label}</span>\n </button>\n ) : (\n <span key={`${cfgKey}-${i}`} className={className}>\n {swatch}\n {label}\n </span>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode,
|
|
2
|
+
import { ReactNode, KeyboardEvent, MouseEvent, ComponentPropsWithoutRef } from 'react';
|
|
3
3
|
import { LucideIcon } from 'lucide-react';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -19,7 +19,7 @@ import { LucideIcon } from 'lucide-react';
|
|
|
19
19
|
type ChipTone = "neutral" | "brand" | "danger" | "warning" | "success" | "info" | "income" | "expense";
|
|
20
20
|
/** `lg` is the 44px touch target (`min-h-11`) a phone surface wants; `md` is ~28px. */
|
|
21
21
|
type ChipSize = "sm" | "md" | "lg";
|
|
22
|
-
interface
|
|
22
|
+
interface ChipBaseProps {
|
|
23
23
|
children: ReactNode;
|
|
24
24
|
tone?: ChipTone;
|
|
25
25
|
size?: ChipSize;
|
|
@@ -28,16 +28,19 @@ interface ChipProps {
|
|
|
28
28
|
* Marks the chip as the current one — `aria-current` on a link, `aria-pressed` on a
|
|
29
29
|
* toggle.
|
|
30
30
|
*
|
|
31
|
-
* On
|
|
32
|
-
* state. A label that flips with it ("Skip this month" / "Ask again this
|
|
33
|
-
* announced together with "pressed", and then says the opposite of what
|
|
31
|
+
* On an ON/OFF toggle, keep the LABEL the same in both states and let `selected`
|
|
32
|
+
* carry the state. A label that flips with it ("Skip this month" / "Ask again this
|
|
33
|
+
* month") is announced together with "pressed", and then says the opposite of what
|
|
34
|
+
* it does.
|
|
35
|
+
*
|
|
36
|
+
* A control that switches between two NAMED states (outflow ⇄ inflow) is not a
|
|
37
|
+
* toggle but an ACTION button: leave `selected` out, let the visible label and the
|
|
38
|
+
* tone follow the state ("− Outflow" / "+ Inflow"), and name the action in
|
|
39
|
+
* `aria-label` ("Direction: outflow — tap for inflow"). Without `selected` the chip
|
|
40
|
+
* reports no pressed state at all. A fixed label there contradicts the figure beside
|
|
41
|
+
* it — "+ Outflow" in green next to an inflow (keksdose #417).
|
|
34
42
|
*/
|
|
35
43
|
selected?: boolean;
|
|
36
|
-
/** Renders the chip as a link. Mutually exclusive with `onClick`. */
|
|
37
|
-
href?: string;
|
|
38
|
-
/** Renders the chip as a toggle button. Mutually exclusive with `href`. Receives the
|
|
39
|
-
* click, so a chip inside a clickable row can `stopPropagation()`. */
|
|
40
|
-
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
41
44
|
/** Renders a dismiss affordance. Works alongside `href`/`onClick` — see the note below. */
|
|
42
45
|
onRemove?: () => void;
|
|
43
46
|
/** Accessible name for the dismiss button. Default: `common.remove` from the
|
|
@@ -65,12 +68,29 @@ interface ChipProps {
|
|
|
65
68
|
/** A native tooltip. For anything a user must read, prefer the kit's `Tooltip`. */
|
|
66
69
|
title?: string;
|
|
67
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* A chip is a link OR a button, never both — so the types say so. Before 0.7.0 both were
|
|
73
|
+
* accepted and `onClick` was silently dropped whenever `href` was set; a link that must
|
|
74
|
+
* also run code wants a router `Link`-style `onClick` on the anchor, which is a different
|
|
75
|
+
* component's job.
|
|
76
|
+
*/
|
|
77
|
+
type ChipProps = ChipBaseProps & ({
|
|
78
|
+
/** Renders the chip as a link. Mutually exclusive with `onClick`. */
|
|
79
|
+
href?: string;
|
|
80
|
+
onClick?: never;
|
|
81
|
+
} | {
|
|
82
|
+
href?: never;
|
|
83
|
+
/** Renders the chip as a toggle button. Mutually exclusive with `href`. Receives
|
|
84
|
+
* the click, so a chip inside a clickable row can `stopPropagation()`. */
|
|
85
|
+
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
86
|
+
});
|
|
68
87
|
/**
|
|
69
88
|
* Note on `onRemove` with `href`/`onClick`: a button cannot be nested inside a button or
|
|
70
89
|
* a link, so when both are present the chip renders a wrapper holding TWO siblings — the
|
|
71
|
-
* interactive body and the dismiss control.
|
|
72
|
-
* the
|
|
73
|
-
*
|
|
90
|
+
* interactive body and the dismiss control. The WRAPPER is the visual pill (border, tone,
|
|
91
|
+
* `className`) and the two sit inside it, so the × reads as part of the chip; the body's
|
|
92
|
+
* focus ring is drawn on the body alone, and `ref`, `id` and the ARIA props still reach
|
|
93
|
+
* the body — the interactive element.
|
|
74
94
|
*/
|
|
75
95
|
declare const Chip: react.ForwardRefExoticComponent<ChipProps & react.RefAttributes<HTMLElement>>;
|
|
76
96
|
interface ChipInputLabels {
|
|
@@ -127,7 +147,7 @@ interface ChipInputProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange
|
|
|
127
147
|
*
|
|
128
148
|
* Enter, or any separator commit the typed text
|
|
129
149
|
* Backspace on empty text move focus to the last chip (does NOT delete it)
|
|
130
|
-
* ← → while on a chip move between chips
|
|
150
|
+
* ← → while on a chip move between chips, in reading order (mirrored in RTL)
|
|
131
151
|
* Backspace / Delete remove the focused chip, focus its neighbour
|
|
132
152
|
* Escape clear what is typed but keep the committed chips
|
|
133
153
|
* paste splits on the separators, so a pasted CSV becomes chips
|
package/dist/components/chip.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { forwardRef, useId, useRef, useState } from "react";
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { horizontalStep } from "../lib/direction.js";
|
|
6
7
|
import { FIELD_INVALID } from "./ui.js";
|
|
7
8
|
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
8
9
|
const TONE = {
|
|
@@ -44,17 +45,37 @@ const TONE = {
|
|
|
44
45
|
}
|
|
45
46
|
};
|
|
46
47
|
const SIZE = {
|
|
47
|
-
sm: {
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
sm: {
|
|
49
|
+
body: "gap-1 px-2 py-0.5 text-xs",
|
|
50
|
+
split: "gap-1 ps-2 pe-1 py-0.5 text-xs",
|
|
51
|
+
tail: "pe-1.5",
|
|
52
|
+
icon: "size-3",
|
|
53
|
+
remove: "size-3"
|
|
54
|
+
},
|
|
55
|
+
md: {
|
|
56
|
+
body: "gap-1.5 px-2.5 py-1 text-sm",
|
|
57
|
+
split: "gap-1.5 ps-2.5 pe-1.5 py-1 text-sm",
|
|
58
|
+
tail: "pe-2",
|
|
59
|
+
icon: "size-3.5",
|
|
60
|
+
remove: "size-3.5"
|
|
61
|
+
},
|
|
62
|
+
lg: {
|
|
63
|
+
body: "min-h-11 gap-2 px-4 py-2 text-sm",
|
|
64
|
+
split: "min-h-11 gap-2 ps-4 pe-2 py-2 text-sm",
|
|
65
|
+
tail: "min-h-11 pe-3.5",
|
|
66
|
+
icon: "size-4",
|
|
67
|
+
remove: "size-4"
|
|
68
|
+
}
|
|
50
69
|
};
|
|
51
|
-
const
|
|
70
|
+
const CHIP_PILL = "inline-flex max-w-full items-center rounded-full border transition-colors";
|
|
71
|
+
const CHIP_RING = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-surface)]";
|
|
72
|
+
const CHIP_BASE = `${CHIP_PILL} ${CHIP_RING}`;
|
|
52
73
|
const Chip = forwardRef(function Chip2({
|
|
53
74
|
children,
|
|
54
75
|
tone = "neutral",
|
|
55
76
|
size = "md",
|
|
56
77
|
icon: Icon,
|
|
57
|
-
selected
|
|
78
|
+
selected,
|
|
58
79
|
href,
|
|
59
80
|
onClick,
|
|
60
81
|
onRemove,
|
|
@@ -71,12 +92,15 @@ const Chip = forwardRef(function Chip2({
|
|
|
71
92
|
const s = SIZE[size];
|
|
72
93
|
const palette = TONE[tone];
|
|
73
94
|
const interactive = !!href || !!onClick;
|
|
95
|
+
const surface = cn(
|
|
96
|
+
selected ? palette.selected : palette.idle,
|
|
97
|
+
disabled && "cursor-default opacity-50"
|
|
98
|
+
);
|
|
74
99
|
const look = cn(
|
|
75
100
|
CHIP_BASE,
|
|
76
101
|
s.body,
|
|
77
|
-
|
|
102
|
+
surface,
|
|
78
103
|
interactive && !disabled && "cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110",
|
|
79
|
-
disabled && "cursor-default opacity-50",
|
|
80
104
|
className
|
|
81
105
|
);
|
|
82
106
|
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -95,32 +119,38 @@ const Chip = forwardRef(function Chip2({
|
|
|
95
119
|
},
|
|
96
120
|
disabled,
|
|
97
121
|
className: cn(
|
|
98
|
-
|
|
122
|
+
// Logical margins: the × sits at the END of the pill, which is the left in RTL.
|
|
123
|
+
"-me-0.5 ms-0.5 shrink-0 rounded-full p-0.5 transition-colors",
|
|
99
124
|
"hover:bg-[var(--bg-active)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
|
|
100
125
|
disabled && "pointer-events-none"
|
|
101
126
|
),
|
|
102
127
|
children: /* @__PURE__ */ jsx(X, { className: s.remove, "aria-hidden": true })
|
|
103
128
|
}
|
|
104
129
|
) : null;
|
|
130
|
+
const pill = (inner2) => /* @__PURE__ */ jsxs("span", { className: cn(CHIP_PILL, s.tail, surface, className), children: [
|
|
131
|
+
inner2,
|
|
132
|
+
remove
|
|
133
|
+
] });
|
|
134
|
+
const inner = cn(
|
|
135
|
+
"inline-flex min-w-0 items-center self-stretch rounded-full transition-colors",
|
|
136
|
+
CHIP_RING,
|
|
137
|
+
s.split,
|
|
138
|
+
!disabled && "cursor-pointer hover:bg-current/10",
|
|
139
|
+
disabled && "cursor-default"
|
|
140
|
+
);
|
|
105
141
|
if (href && !disabled) {
|
|
106
|
-
const link = /* @__PURE__ */
|
|
142
|
+
const link = /* @__PURE__ */ jsx(
|
|
107
143
|
"a",
|
|
108
144
|
{
|
|
109
145
|
ref,
|
|
110
146
|
href,
|
|
111
147
|
"aria-current": selected ? "true" : void 0,
|
|
112
|
-
className:
|
|
148
|
+
className: remove ? inner : look,
|
|
113
149
|
...rest,
|
|
114
|
-
children:
|
|
115
|
-
body,
|
|
116
|
-
!remove && null
|
|
117
|
-
]
|
|
150
|
+
children: body
|
|
118
151
|
}
|
|
119
152
|
);
|
|
120
|
-
return remove ?
|
|
121
|
-
link,
|
|
122
|
-
remove
|
|
123
|
-
] }) : link;
|
|
153
|
+
return remove ? pill(link) : link;
|
|
124
154
|
}
|
|
125
155
|
if (onClick) {
|
|
126
156
|
const button = /* @__PURE__ */ jsx(
|
|
@@ -131,15 +161,12 @@ const Chip = forwardRef(function Chip2({
|
|
|
131
161
|
onClick,
|
|
132
162
|
disabled,
|
|
133
163
|
"aria-pressed": selected,
|
|
134
|
-
className:
|
|
164
|
+
className: remove ? inner : look,
|
|
135
165
|
...rest,
|
|
136
166
|
children: body
|
|
137
167
|
}
|
|
138
168
|
);
|
|
139
|
-
return remove ?
|
|
140
|
-
button,
|
|
141
|
-
remove
|
|
142
|
-
] }) : button;
|
|
169
|
+
return remove ? pill(button) : button;
|
|
143
170
|
}
|
|
144
171
|
return /* @__PURE__ */ jsxs(
|
|
145
172
|
"span",
|
|
@@ -192,13 +219,16 @@ function ChipInput({
|
|
|
192
219
|
const locale = useKitLocale();
|
|
193
220
|
const [draft, setDraft] = useState("");
|
|
194
221
|
const [message, setMessage] = useState("");
|
|
222
|
+
const [rejected, setRejected] = useState(null);
|
|
195
223
|
const [focusedChip, setFocusedChip] = useState(null);
|
|
196
224
|
const inputRef = useRef(null);
|
|
197
225
|
const chipRefs = useRef([]);
|
|
198
226
|
const id = useId();
|
|
199
227
|
const listId = `${id}-list`;
|
|
200
228
|
const errorId = `${id}-error`;
|
|
229
|
+
const rejectedId = `${id}-rejected`;
|
|
201
230
|
const isInvalid = invalid || !!error;
|
|
231
|
+
const describedBy = [error ? errorId : null, rejected ? rejectedId : null].filter(Boolean).join(" ");
|
|
202
232
|
const focusChip = (index) => {
|
|
203
233
|
setFocusedChip(index);
|
|
204
234
|
if (index === null) inputRef.current?.focus();
|
|
@@ -230,6 +260,7 @@ function ChipInput({
|
|
|
230
260
|
if (added.length) onChange(next);
|
|
231
261
|
const addedList = new Intl.ListFormat(locale, { type: "conjunction", style: "long" });
|
|
232
262
|
setMessage(rejection ?? (added.length ? text.added(addedList.format(added)) : ""));
|
|
263
|
+
setRejected(rejection);
|
|
233
264
|
return { added, rejection };
|
|
234
265
|
};
|
|
235
266
|
const commit = (raw) => commitMany([raw]).added.length > 0;
|
|
@@ -237,6 +268,7 @@ function ChipInput({
|
|
|
237
268
|
const removed = value[index];
|
|
238
269
|
onChange(value.filter((_, i) => i !== index));
|
|
239
270
|
setMessage(text.removed(removed));
|
|
271
|
+
setRejected(null);
|
|
240
272
|
const next = index >= value.length - 1 ? null : index;
|
|
241
273
|
requestAnimationFrame(() => focusChip(value.length - 1 === 0 ? null : next));
|
|
242
274
|
};
|
|
@@ -269,12 +301,13 @@ function ChipInput({
|
|
|
269
301
|
removeAt(index);
|
|
270
302
|
return;
|
|
271
303
|
}
|
|
272
|
-
|
|
304
|
+
const step = horizontalStep(e.key, e.currentTarget);
|
|
305
|
+
if (step === -1 && index > 0) {
|
|
273
306
|
e.preventDefault();
|
|
274
307
|
focusChip(index - 1);
|
|
275
308
|
return;
|
|
276
309
|
}
|
|
277
|
-
if (
|
|
310
|
+
if (step === 1) {
|
|
278
311
|
e.preventDefault();
|
|
279
312
|
focusChip(index === value.length - 1 ? null : index + 1);
|
|
280
313
|
return;
|
|
@@ -332,6 +365,7 @@ function ChipInput({
|
|
|
332
365
|
onChange: (e) => {
|
|
333
366
|
setDraft(e.target.value);
|
|
334
367
|
setMessage("");
|
|
368
|
+
setRejected(null);
|
|
335
369
|
},
|
|
336
370
|
onKeyDown: onInputKeyDown,
|
|
337
371
|
onBlur: () => {
|
|
@@ -348,8 +382,8 @@ function ChipInput({
|
|
|
348
382
|
placeholder: value.length ? void 0 : placeholder,
|
|
349
383
|
disabled,
|
|
350
384
|
"aria-label": label ? void 0 : ariaLabelAttr ?? ariaLabel,
|
|
351
|
-
"aria-describedby":
|
|
352
|
-
"aria-invalid": isInvalid || void 0,
|
|
385
|
+
"aria-describedby": describedBy || void 0,
|
|
386
|
+
"aria-invalid": isInvalid || !!rejected || void 0,
|
|
353
387
|
className: "min-w-[6rem] flex-1 bg-transparent py-0.5 text-sm text-[var(--text-primary)] outline-none placeholder:text-[var(--text-placeholder)] disabled:cursor-default"
|
|
354
388
|
}
|
|
355
389
|
),
|
|
@@ -357,7 +391,8 @@ function ChipInput({
|
|
|
357
391
|
]
|
|
358
392
|
}
|
|
359
393
|
),
|
|
360
|
-
error && /* @__PURE__ */ jsx("p", { id: errorId, className: "text-xs text-[var(--danger)]", children: error })
|
|
394
|
+
error && /* @__PURE__ */ jsx("p", { id: errorId, className: "text-xs text-[var(--danger)]", children: error }),
|
|
395
|
+
rejected && /* @__PURE__ */ jsx("p", { id: rejectedId, className: "text-xs text-[var(--danger)]", "data-chip-input-rejected": "", children: rejected })
|
|
361
396
|
] })
|
|
362
397
|
);
|
|
363
398
|
}
|