@eifi1/ui-kit 0.7.0 → 0.8.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 +32 -17
- package/dist/chart.d.ts +3 -2
- package/dist/components/alert-banner.d.ts +59 -10
- package/dist/components/alert-banner.js +114 -8
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +5 -0
- package/dist/components/button-group.d.ts +26 -0
- package/dist/components/button-group.js +49 -0
- package/dist/components/button-group.js.map +1 -0
- package/dist/components/calculator.d.ts +5 -0
- package/dist/components/chart-zoom.d.ts +63 -14
- package/dist/components/chart-zoom.js +82 -16
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/chip.d.ts +67 -2
- package/dist/components/chip.js +73 -8
- package/dist/components/chip.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +120 -0
- package/dist/components/confirm-dialog.js +96 -0
- package/dist/components/confirm-dialog.js.map +1 -0
- package/dist/components/copy-button.d.ts +62 -0
- package/dist/components/copy-button.js +95 -0
- package/dist/components/copy-button.js.map +1 -0
- 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 +5 -1
- package/dist/components/data-table-pagination.js +71 -58
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-sort.d.ts +28 -5
- package/dist/components/data-table-sort.js +11 -7
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +544 -423
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +51 -5
- package/dist/components/date-picker.js +152 -40
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +60 -0
- package/dist/components/description-list.js +112 -0
- package/dist/components/description-list.js.map +1 -0
- package/dist/components/disclosure.d.ts +37 -2
- package/dist/components/disclosure.js +14 -5
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/facing-pair.d.ts +2 -0
- package/dist/components/file-button.d.ts +19 -1
- package/dist/components/file-button.js +8 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +56 -9
- package/dist/components/file-dropzone.js +100 -70
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.d.ts +107 -0
- package/dist/components/floating-panel.js +206 -0
- package/dist/components/floating-panel.js.map +1 -0
- package/dist/components/full-bleed-dialog.d.ts +12 -1
- package/dist/components/full-bleed-dialog.js +26 -5
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/modal.d.ts +6 -2
- package/dist/components/modal.js +2 -1
- package/dist/components/modal.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/numpad-sheet.d.ts +5 -0
- package/dist/components/progress-bar.d.ts +54 -0
- package/dist/components/progress-bar.js +94 -0
- package/dist/components/progress-bar.js.map +1 -0
- package/dist/components/scroll-area.d.ts +53 -0
- package/dist/components/scroll-area.js +86 -0
- package/dist/components/scroll-area.js.map +1 -0
- package/dist/components/separator.d.ts +23 -0
- package/dist/components/separator.js +24 -0
- package/dist/components/separator.js.map +1 -0
- package/dist/components/series-chart-ticks.d.ts +34 -1
- package/dist/components/series-chart-ticks.js +83 -1
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.d.ts +314 -19
- package/dist/components/series-chart.js +510 -120
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +2 -0
- package/dist/components/skeleton.d.ts +36 -0
- package/dist/components/skeleton.js +35 -0
- package/dist/components/skeleton.js.map +1 -0
- package/dist/components/stat-tile.d.ts +60 -6
- package/dist/components/stat-tile.js +60 -26
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/table.d.ts +83 -0
- package/dist/components/table.js +166 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/time-input.d.ts +2 -0
- package/dist/components/toggle-legend.d.ts +3 -2
- package/dist/components/toggle-legend.js +1 -1
- 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/tree-view.d.ts +129 -0
- package/dist/components/tree-view.js +376 -0
- package/dist/components/tree-view.js.map +1 -0
- package/dist/components/treemap.d.ts +9 -2
- package/dist/components/treemap.js +2 -1
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +92 -14
- package/dist/components/ui.js +112 -30
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/{data-table-filters-noy0Abvi.d.ts → data-table-filters-CF1PXqjQ.d.ts} +73 -3
- package/dist/data-table.d.ts +2 -2
- package/dist/data-table.js.map +1 -1
- package/dist/hooks/use-copy-to-clipboard.d.ts +28 -0
- package/dist/hooks/use-copy-to-clipboard.js +81 -0
- package/dist/hooks/use-copy-to-clipboard.js.map +1 -0
- package/dist/hooks/use-debounce.d.ts +43 -0
- package/dist/hooks/use-debounce.js +56 -0
- package/dist/hooks/use-debounce.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +72 -0
- package/dist/hooks/use-file-drop.js +57 -0
- package/dist/hooks/use-file-drop.js.map +1 -0
- package/dist/i18n/defaults.d.ts +5 -0
- package/dist/i18n/defaults.js +7 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +17 -0
- package/dist/i18n/kit-labels.js +6 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +43 -0
- package/dist/i18n/locales/de-CH-informal.js +8 -0
- package/dist/i18n/locales/de-CH-informal.js.map +1 -0
- package/dist/i18n/locales/de-CH.d.ts +5 -0
- package/dist/i18n/locales/de-CH.js +1 -14
- package/dist/i18n/locales/de-CH.js.map +1 -1
- package/dist/i18n/locales/de-informal.d.ts +64 -0
- package/dist/i18n/locales/de-informal.js +37 -0
- package/dist/i18n/locales/de-informal.js.map +1 -0
- package/dist/i18n/locales/de.d.ts +5 -0
- package/dist/i18n/locales/de.js +25 -0
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +5 -0
- package/dist/i18n/locales/es.js +25 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +5 -0
- package/dist/i18n/locales/fr.js +25 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +5 -0
- package/dist/i18n/locales/hu.js +25 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +5 -0
- package/dist/i18n/locales/it.js +25 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +5 -0
- package/dist/i18n/locales/zh.js +25 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/swiss.d.ts +13 -0
- package/dist/i18n/swiss.js +19 -0
- package/dist/i18n/swiss.js.map +1 -0
- package/dist/index.d.ts +26 -11
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/dates.d.ts +37 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/rhf/form.d.ts +2 -0
- package/dist/rhf.d.ts +2 -0
- package/dist/search/command-palette.d.ts +15 -1
- package/dist/search/command-palette.js +10 -2
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.js +2 -2
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +35 -2
- package/dist/wizard/stepper-nav.js +54 -22
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +107 -5
- package/dist/wizard/types.js +2 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +141 -21
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.d.ts +10 -2
- package/dist/wizard/wizard-summary.js +3 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +4 -1
- package/dist/wizard.js.map +1 -1
- package/package.json +1 -1
- package/src/components/alert-banner.tsx +211 -20
- package/src/components/button-group.tsx +75 -0
- package/src/components/chart-zoom.tsx +158 -20
- package/src/components/chip.tsx +160 -9
- package/src/components/confirm-dialog.tsx +242 -0
- package/src/components/copy-button.tsx +158 -0
- package/src/components/data-table-pagination.tsx +19 -6
- package/src/components/data-table-sort.ts +49 -10
- package/src/components/data-table.tsx +293 -24
- package/src/components/date-picker.tsx +262 -57
- package/src/components/description-list.tsx +174 -0
- package/src/components/disclosure.tsx +58 -5
- package/src/components/file-button.tsx +22 -2
- package/src/components/file-dropzone.tsx +198 -100
- package/src/components/floating-panel.tsx +341 -0
- package/src/components/full-bleed-dialog.tsx +48 -5
- package/src/components/modal.tsx +9 -2
- package/src/components/progress-bar.tsx +163 -0
- package/src/components/scroll-area.tsx +129 -0
- package/src/components/separator.tsx +37 -0
- package/src/components/series-chart-ticks.ts +135 -0
- package/src/components/series-chart.tsx +934 -67
- package/src/components/skeleton.tsx +65 -0
- package/src/components/stat-tile.tsx +160 -27
- package/src/components/table.tsx +263 -0
- package/src/components/toggle-legend.tsx +4 -3
- package/src/components/tooltip.tsx +40 -12
- package/src/components/tree-view.tsx +589 -0
- package/src/components/treemap.tsx +10 -2
- package/src/components/ui.tsx +268 -42
- package/src/data-table.ts +2 -0
- package/src/hooks/use-copy-to-clipboard.ts +122 -0
- package/src/hooks/use-debounce.ts +105 -0
- package/src/hooks/use-file-drop.ts +123 -0
- package/src/i18n/defaults.ts +6 -0
- package/src/i18n/kit-labels.tsx +19 -0
- package/src/i18n/locales/de-CH-informal.ts +10 -0
- package/src/i18n/locales/de-CH.ts +1 -16
- package/src/i18n/locales/de-informal.ts +61 -0
- package/src/i18n/locales/de.ts +25 -0
- package/src/i18n/locales/es.ts +25 -0
- package/src/i18n/locales/fr.ts +25 -0
- package/src/i18n/locales/hu.ts +25 -0
- package/src/i18n/locales/it.ts +25 -0
- package/src/i18n/locales/zh.ts +25 -0
- package/src/i18n/swiss.ts +25 -0
- package/src/index.ts +29 -0
- package/src/lib/dates.ts +48 -0
- package/src/search/command-palette.tsx +29 -3
- package/src/shell/app-shell.tsx +2 -2
- package/src/wizard/stepper-nav.tsx +102 -28
- package/src/wizard/types.ts +106 -4
- package/src/wizard/use-wizard.ts +211 -31
- package/src/wizard/wizard-summary.tsx +24 -12
- package/src/wizard.ts +3 -2
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type ProgressBarTone = "brand" | "neutral" | "success" | "warning" | "danger" | "info";
|
|
5
|
+
type ProgressBarSize = "sm" | "md" | "lg";
|
|
6
|
+
interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "role"> {
|
|
7
|
+
/** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way
|
|
8
|
+
* and nobody knows how much is left (an import that reports no count). */
|
|
9
|
+
value?: number;
|
|
10
|
+
min?: number;
|
|
11
|
+
max?: number;
|
|
12
|
+
/**
|
|
13
|
+
* `progress` (default): a task moving toward completion — `role="progressbar"`.
|
|
14
|
+
* `meter`: a share of a known whole that is not going anywhere (budget used, disk
|
|
15
|
+
* space, one category's slice of spending) — `role="meter"`. keksdose's share bars
|
|
16
|
+
* were progress bars to a screen reader, which reads "busy" into a figure that is
|
|
17
|
+
* simply a figure. A meter is never indeterminate; a missing value reads as `min`.
|
|
18
|
+
*/
|
|
19
|
+
variant?: "progress" | "meter";
|
|
20
|
+
/** Visible label above the bar, and its accessible name. Without it, pass
|
|
21
|
+
* `aria-label` (an indeterminate bar with neither is named `common.loading`). */
|
|
22
|
+
label?: ReactNode;
|
|
23
|
+
/** Show the value text at the end of the label row. */
|
|
24
|
+
showValue?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The value as words — both the visible `showValue` text and `aria-valuetext`.
|
|
27
|
+
* Default: the fraction as a percentage in the kit's locale ("42 %" in French).
|
|
28
|
+
* Pass one for anything that is not a percentage: `(v, max) => \`${v} of ${max} files\``.
|
|
29
|
+
*/
|
|
30
|
+
formatValue?: (value: number, max: number, min: number) => string;
|
|
31
|
+
tone?: ProgressBarTone;
|
|
32
|
+
size?: ProgressBarSize;
|
|
33
|
+
locale?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A horizontal bar that fills toward `max`.
|
|
37
|
+
*
|
|
38
|
+
* keksdose (import progress, budget share bars) and kastlan (upload and job
|
|
39
|
+
* progress) each drew their own div-in-a-div. None had a role, so a reader heard
|
|
40
|
+
* nothing at all; the share bars that did have one claimed to be progress.
|
|
41
|
+
*
|
|
42
|
+
* `aria-valuetext` is always set when there is a value: a bare `aria-valuenow="37"`
|
|
43
|
+
* is read as "37" by some readers and "37 percent" by others, and neither is right
|
|
44
|
+
* when `max` is 212 files.
|
|
45
|
+
*
|
|
46
|
+
* The indeterminate sweep stops under `prefers-reduced-motion`. It becomes a still,
|
|
47
|
+
* half-strength bar across the whole track — still visibly "something is happening",
|
|
48
|
+
* with nothing moving. The sweep is a Tailwind arbitrary `animate-[…]` rather than
|
|
49
|
+
* tokens.css's `.animate-indeterminate` class because that one is UNLAYERED, and an
|
|
50
|
+
* unlayered rule beats every layered utility, `motion-reduce:animate-none` included.
|
|
51
|
+
*/
|
|
52
|
+
declare function ProgressBar({ value, min, max, variant, label, showValue, formatValue, tone, size, locale, className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...rest }: ProgressBarProps): react.JSX.Element;
|
|
53
|
+
|
|
54
|
+
export { ProgressBar, type ProgressBarProps, type ProgressBarSize, type ProgressBarTone };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
6
|
+
const FILL = {
|
|
7
|
+
brand: "bg-[var(--brand)]",
|
|
8
|
+
neutral: "bg-[var(--text-muted)]",
|
|
9
|
+
success: "bg-[var(--success)]",
|
|
10
|
+
warning: "bg-[var(--warning)]",
|
|
11
|
+
danger: "bg-[var(--danger)]",
|
|
12
|
+
info: "bg-[var(--info)]"
|
|
13
|
+
};
|
|
14
|
+
const TRACK_HEIGHT = { sm: "h-1", md: "h-2", lg: "h-3" };
|
|
15
|
+
function ProgressBar({
|
|
16
|
+
value,
|
|
17
|
+
min = 0,
|
|
18
|
+
max = 100,
|
|
19
|
+
variant = "progress",
|
|
20
|
+
label,
|
|
21
|
+
showValue = false,
|
|
22
|
+
formatValue,
|
|
23
|
+
tone = "brand",
|
|
24
|
+
size = "md",
|
|
25
|
+
locale,
|
|
26
|
+
className,
|
|
27
|
+
"aria-label": ariaLabel,
|
|
28
|
+
"aria-labelledby": ariaLabelledBy,
|
|
29
|
+
...rest
|
|
30
|
+
}) {
|
|
31
|
+
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
32
|
+
const kitLocale = useKitLocale(locale);
|
|
33
|
+
const labelId = useId();
|
|
34
|
+
const meter = variant === "meter";
|
|
35
|
+
const current = value === void 0 || !Number.isFinite(value) ? meter ? min : void 0 : value;
|
|
36
|
+
const indeterminate = current === void 0;
|
|
37
|
+
const span = max - min;
|
|
38
|
+
const clamped = indeterminate ? void 0 : Math.min(max, Math.max(min, current));
|
|
39
|
+
const fraction = clamped === void 0 || span <= 0 ? 0 : (clamped - min) / span;
|
|
40
|
+
const valueText = clamped === void 0 ? void 0 : formatValue ? formatValue(clamped, max, min) : new Intl.NumberFormat(kitLocale, { style: "percent", maximumFractionDigits: 0 }).format(fraction);
|
|
41
|
+
const labelledBy = ariaLabelledBy ?? (label != null ? labelId : void 0);
|
|
42
|
+
const name = labelledBy ? void 0 : ariaLabel ?? (indeterminate ? common.loading : void 0);
|
|
43
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("min-w-0", className), "data-state": indeterminate ? "indeterminate" : "determinate", children: [
|
|
44
|
+
(label != null || showValue && valueText !== void 0) && /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-baseline gap-2 text-sm", children: [
|
|
45
|
+
label != null && /* @__PURE__ */ jsx("span", { id: labelId, className: "min-w-0 text-[var(--text-secondary)]", children: label }),
|
|
46
|
+
showValue && valueText !== void 0 && // Hidden: the same words are the bar's aria-valuetext, and reading them
|
|
47
|
+
// twice in a row is noise.
|
|
48
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "ms-auto shrink-0 tabular-nums text-[var(--text-muted)]", children: valueText })
|
|
49
|
+
] }),
|
|
50
|
+
/* @__PURE__ */ jsx(
|
|
51
|
+
"div",
|
|
52
|
+
{
|
|
53
|
+
...rest,
|
|
54
|
+
role: meter ? "meter" : "progressbar",
|
|
55
|
+
"aria-label": name,
|
|
56
|
+
"aria-labelledby": labelledBy,
|
|
57
|
+
"aria-valuemin": indeterminate ? void 0 : min,
|
|
58
|
+
"aria-valuemax": indeterminate ? void 0 : max,
|
|
59
|
+
"aria-valuenow": clamped,
|
|
60
|
+
"aria-valuetext": valueText,
|
|
61
|
+
"aria-busy": !meter && indeterminate ? true : void 0,
|
|
62
|
+
className: cn("relative w-full overflow-hidden rounded-full bg-[var(--bg-active)]", TRACK_HEIGHT[size]),
|
|
63
|
+
children: indeterminate ? /* @__PURE__ */ jsx(
|
|
64
|
+
"div",
|
|
65
|
+
{
|
|
66
|
+
"data-part": "fill",
|
|
67
|
+
className: cn(
|
|
68
|
+
// `left-0`, deliberately physical: the keyframes (tokens.css
|
|
69
|
+
// `indeterminate-sweep`) translate along the physical x axis, and under
|
|
70
|
+
// RTL the same keyframes are run in REVERSE — from the right edge back
|
|
71
|
+
// to the left. A logical `start-0` would park the segment at the right
|
|
72
|
+
// in RTL and the reversed sweep would then start off-screen.
|
|
73
|
+
"absolute inset-y-0 left-0 w-1/3 rounded-full",
|
|
74
|
+
"animate-[indeterminate-sweep_1.4s_ease-in-out_infinite] rtl:[animation-direction:reverse]",
|
|
75
|
+
"motion-reduce:w-full motion-reduce:animate-none motion-reduce:opacity-50",
|
|
76
|
+
FILL[tone]
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
) : /* @__PURE__ */ jsx(
|
|
80
|
+
"div",
|
|
81
|
+
{
|
|
82
|
+
"data-part": "fill",
|
|
83
|
+
className: cn("h-full rounded-full transition-[width] duration-300 motion-reduce:transition-none", FILL[tone]),
|
|
84
|
+
style: { width: `${fraction * 100}%` }
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
] });
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
ProgressBar
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=progress-bar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/progress-bar.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\n\nexport type ProgressBarTone = \"brand\" | \"neutral\" | \"success\" | \"warning\" | \"danger\" | \"info\";\nexport type ProgressBarSize = \"sm\" | \"md\" | \"lg\";\n\nconst FILL: Record<ProgressBarTone, string> = {\n brand: \"bg-[var(--brand)]\",\n neutral: \"bg-[var(--text-muted)]\",\n success: \"bg-[var(--success)]\",\n warning: \"bg-[var(--warning)]\",\n danger: \"bg-[var(--danger)]\",\n info: \"bg-[var(--info)]\",\n};\n\nconst TRACK_HEIGHT: Record<ProgressBarSize, string> = { sm: \"h-1\", md: \"h-2\", lg: \"h-3\" };\n\nexport interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"role\"> {\n /** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way\n * and nobody knows how much is left (an import that reports no count). */\n value?: number;\n min?: number;\n max?: number;\n /**\n * `progress` (default): a task moving toward completion — `role=\"progressbar\"`.\n * `meter`: a share of a known whole that is not going anywhere (budget used, disk\n * space, one category's slice of spending) — `role=\"meter\"`. keksdose's share bars\n * were progress bars to a screen reader, which reads \"busy\" into a figure that is\n * simply a figure. A meter is never indeterminate; a missing value reads as `min`.\n */\n variant?: \"progress\" | \"meter\";\n /** Visible label above the bar, and its accessible name. Without it, pass\n * `aria-label` (an indeterminate bar with neither is named `common.loading`). */\n label?: ReactNode;\n /** Show the value text at the end of the label row. */\n showValue?: boolean;\n /**\n * The value as words — both the visible `showValue` text and `aria-valuetext`.\n * Default: the fraction as a percentage in the kit's locale (\"42 %\" in French).\n * Pass one for anything that is not a percentage: `(v, max) => \\`${v} of ${max} files\\``.\n */\n formatValue?: (value: number, max: number, min: number) => string;\n tone?: ProgressBarTone;\n size?: ProgressBarSize;\n locale?: string;\n}\n\n/**\n * A horizontal bar that fills toward `max`.\n *\n * keksdose (import progress, budget share bars) and kastlan (upload and job\n * progress) each drew their own div-in-a-div. None had a role, so a reader heard\n * nothing at all; the share bars that did have one claimed to be progress.\n *\n * `aria-valuetext` is always set when there is a value: a bare `aria-valuenow=\"37\"`\n * is read as \"37\" by some readers and \"37 percent\" by others, and neither is right\n * when `max` is 212 files.\n *\n * The indeterminate sweep stops under `prefers-reduced-motion`. It becomes a still,\n * half-strength bar across the whole track — still visibly \"something is happening\",\n * with nothing moving. The sweep is a Tailwind arbitrary `animate-[…]` rather than\n * tokens.css's `.animate-indeterminate` class because that one is UNLAYERED, and an\n * unlayered rule beats every layered utility, `motion-reduce:animate-none` included.\n */\nexport function ProgressBar({\n value,\n min = 0,\n max = 100,\n variant = \"progress\",\n label,\n showValue = false,\n formatValue,\n tone = \"brand\",\n size = \"md\",\n locale,\n className,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n ...rest\n}: ProgressBarProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const kitLocale = useKitLocale(locale);\n const labelId = useId();\n\n const meter = variant === \"meter\";\n const current = value === undefined || !Number.isFinite(value) ? (meter ? min : undefined) : value;\n const indeterminate = current === undefined;\n const span = max - min;\n const clamped = indeterminate ? undefined : Math.min(max, Math.max(min, current));\n const fraction = clamped === undefined || span <= 0 ? 0 : (clamped - min) / span;\n\n const valueText =\n clamped === undefined\n ? undefined\n : formatValue\n ? formatValue(clamped, max, min)\n : new Intl.NumberFormat(kitLocale, { style: \"percent\", maximumFractionDigits: 0 }).format(fraction);\n\n const labelledBy = ariaLabelledBy ?? (label != null ? labelId : undefined);\n const name = labelledBy ? undefined : (ariaLabel ?? (indeterminate ? common.loading : undefined));\n\n return (\n <div className={cn(\"min-w-0\", className)} data-state={indeterminate ? \"indeterminate\" : \"determinate\"}>\n {(label != null || (showValue && valueText !== undefined)) && (\n <div className=\"mb-1 flex items-baseline gap-2 text-sm\">\n {label != null && (\n <span id={labelId} className=\"min-w-0 text-[var(--text-secondary)]\">\n {label}\n </span>\n )}\n {showValue && valueText !== undefined && (\n // Hidden: the same words are the bar's aria-valuetext, and reading them\n // twice in a row is noise.\n <span aria-hidden className=\"ms-auto shrink-0 tabular-nums text-[var(--text-muted)]\">\n {valueText}\n </span>\n )}\n </div>\n )}\n <div\n {...rest}\n role={meter ? \"meter\" : \"progressbar\"}\n aria-label={name}\n aria-labelledby={labelledBy}\n aria-valuemin={indeterminate ? undefined : min}\n aria-valuemax={indeterminate ? undefined : max}\n aria-valuenow={clamped}\n aria-valuetext={valueText}\n aria-busy={!meter && indeterminate ? true : undefined}\n // `relative` + `overflow-hidden`: the indeterminate segment is positioned in\n // here and must not paint past the rounded ends.\n className={cn(\"relative w-full overflow-hidden rounded-full bg-[var(--bg-active)]\", TRACK_HEIGHT[size])}\n >\n {indeterminate ? (\n <div\n data-part=\"fill\"\n className={cn(\n // `left-0`, deliberately physical: the keyframes (tokens.css\n // `indeterminate-sweep`) translate along the physical x axis, and under\n // RTL the same keyframes are run in REVERSE — from the right edge back\n // to the left. A logical `start-0` would park the segment at the right\n // in RTL and the reversed sweep would then start off-screen.\n \"absolute inset-y-0 left-0 w-1/3 rounded-full\",\n \"animate-[indeterminate-sweep_1.4s_ease-in-out_infinite] rtl:[animation-direction:reverse]\",\n \"motion-reduce:w-full motion-reduce:animate-none motion-reduce:opacity-50\",\n FILL[tone],\n )}\n />\n ) : (\n <div\n data-part=\"fill\"\n // Block flow starts at the inline start, so a plain width fills from the\n // right under `dir=\"rtl\"` with nothing extra.\n className={cn(\"h-full rounded-full transition-[width] duration-300 motion-reduce:transition-none\", FILL[tone])}\n style={{ width: `${fraction * 100}%` }}\n />\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";AA0GQ,SAEI,KAFJ;AA1GR,SAAS,aAAa;AAEtB,SAAS,UAAU;AACnB,SAAS,uBAAuB,cAAc,oBAAoB;AAKlE,MAAM,OAAwC;AAAA,EAC5C,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,MAAM;AACR;AAEA,MAAM,eAAgD,EAAE,IAAI,OAAO,IAAI,OAAO,IAAI,MAAM;AAiDjF,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,MAAM;AAAA,EACN,MAAM;AAAA,EACN,UAAU;AAAA,EACV;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,OAAO;AAAA,EACP,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,mBAAmB;AAAA,EACnB,GAAG;AACL,GAAqB;AACnB,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,YAAY,aAAa,MAAM;AACrC,QAAM,UAAU,MAAM;AAEtB,QAAM,QAAQ,YAAY;AAC1B,QAAM,UAAU,UAAU,UAAa,CAAC,OAAO,SAAS,KAAK,IAAK,QAAQ,MAAM,SAAa;AAC7F,QAAM,gBAAgB,YAAY;AAClC,QAAM,OAAO,MAAM;AACnB,QAAM,UAAU,gBAAgB,SAAY,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,OAAO,CAAC;AAChF,QAAM,WAAW,YAAY,UAAa,QAAQ,IAAI,KAAK,UAAU,OAAO;AAE5E,QAAM,YACJ,YAAY,SACR,SACA,cACE,YAAY,SAAS,KAAK,GAAG,IAC7B,IAAI,KAAK,aAAa,WAAW,EAAE,OAAO,WAAW,uBAAuB,EAAE,CAAC,EAAE,OAAO,QAAQ;AAExG,QAAM,aAAa,mBAAmB,SAAS,OAAO,UAAU;AAChE,QAAM,OAAO,aAAa,SAAa,cAAc,gBAAgB,OAAO,UAAU;AAEtF,SACE,qBAAC,SAAI,WAAW,GAAG,WAAW,SAAS,GAAG,cAAY,gBAAgB,kBAAkB,eACpF;AAAA,cAAS,QAAS,aAAa,cAAc,WAC7C,qBAAC,SAAI,WAAU,0CACZ;AAAA,eAAS,QACR,oBAAC,UAAK,IAAI,SAAS,WAAU,wCAC1B,iBACH;AAAA,MAED,aAAa,cAAc;AAAA;AAAA,MAG1B,oBAAC,UAAK,eAAW,MAAC,WAAU,0DACzB,qBACH;AAAA,OAEJ;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ,MAAM,QAAQ,UAAU;AAAA,QACxB,cAAY;AAAA,QACZ,mBAAiB;AAAA,QACjB,iBAAe,gBAAgB,SAAY;AAAA,QAC3C,iBAAe,gBAAgB,SAAY;AAAA,QAC3C,iBAAe;AAAA,QACf,kBAAgB;AAAA,QAChB,aAAW,CAAC,SAAS,gBAAgB,OAAO;AAAA,QAG5C,WAAW,GAAG,sEAAsE,aAAa,IAAI,CAAC;AAAA,QAErG,0BACC;AAAA,UAAC;AAAA;AAAA,YACC,aAAU;AAAA,YACV,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAMT;AAAA,cACA;AAAA,cACA;AAAA,cACA,KAAK,IAAI;AAAA,YACX;AAAA;AAAA,QACF,IAEA;AAAA,UAAC;AAAA;AAAA,YACC,aAAU;AAAA,YAGV,WAAW,GAAG,qFAAqF,KAAK,IAAI,CAAC;AAAA,YAC7G,OAAO,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI;AAAA;AAAA,QACvC;AAAA;AAAA,IAEJ;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, Ref, RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether `ref`'s element currently overflows its box — the one fact a scroll
|
|
6
|
+
* container needs to decide whether it is a keyboard stop.
|
|
7
|
+
*
|
|
8
|
+
* Measured on every commit (cheap: two reads, and state is set only on a change) and
|
|
9
|
+
* whenever the element or one of its direct children resizes, which covers both
|
|
10
|
+
* "the panel got narrower" and "the rows arrived". Without `ResizeObserver` (jsdom,
|
|
11
|
+
* very old engines) the per-commit measurement is what remains.
|
|
12
|
+
*
|
|
13
|
+
* Shared with {@link Table}'s overflow wrapper, which has the same obligation.
|
|
14
|
+
*/
|
|
15
|
+
declare function useScrollOverflow(ref: RefObject<HTMLElement | null>): boolean;
|
|
16
|
+
/** Thin, token-coloured scrollbars. `scrollbar-width` / `scrollbar-color` are the
|
|
17
|
+
* standard (Firefox, Chrome 121+); the `::-webkit-scrollbar` rules are for Safari,
|
|
18
|
+
* which has neither. Where the standard pair applies, Chrome ignores the rest. */
|
|
19
|
+
declare const THIN_SCROLLBAR_CLASS: string;
|
|
20
|
+
declare const AXIS: {
|
|
21
|
+
readonly vertical: "overflow-y-auto overflow-x-hidden";
|
|
22
|
+
readonly horizontal: "overflow-x-auto overflow-y-hidden";
|
|
23
|
+
readonly both: "overflow-auto";
|
|
24
|
+
};
|
|
25
|
+
interface ScrollAreaProps extends ComponentPropsWithoutRef<"div"> {
|
|
26
|
+
/** Which way the content may scroll. Default `vertical`. */
|
|
27
|
+
orientation?: keyof typeof AXIS;
|
|
28
|
+
/**
|
|
29
|
+
* The region's accessible name. With it (or `aria-labelledby`) the container is a
|
|
30
|
+
* `role="region"` a reader can find and name; without it, it is a plain box.
|
|
31
|
+
* Name every ScrollArea whose content can overflow: once it is a tab stop, a
|
|
32
|
+
* reader announces it on focus, and an unnamed one is announced as nothing.
|
|
33
|
+
*/
|
|
34
|
+
label?: string;
|
|
35
|
+
ref?: Ref<HTMLDivElement>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A styled overflow container: a thin scrollbar in the kit's colours, a focus ring,
|
|
39
|
+
* and the keyboard access a plain `overflow-auto` div does not have.
|
|
40
|
+
*
|
|
41
|
+
* kastlan used Radix's ScrollArea for this and nothing else from it. Radix draws a
|
|
42
|
+
* custom scrollbar in JavaScript; the platform's own, thinned and coloured, costs no
|
|
43
|
+
* script and keeps native momentum, overscroll and scrollbar-dragging behaviour.
|
|
44
|
+
*
|
|
45
|
+
* A container that scrolls but holds nothing focusable cannot be scrolled from the
|
|
46
|
+
* keyboard at all — Tab jumps straight past it (WCAG 2.1.1). So while the content
|
|
47
|
+
* overflows, the container itself is a tab stop (`tabIndex=0`), and arrow keys and
|
|
48
|
+
* Page Up/Down scroll it natively. While nothing overflows it is not, so a short list
|
|
49
|
+
* adds no empty stop to the tab order.
|
|
50
|
+
*/
|
|
51
|
+
declare function ScrollArea({ orientation, label, className, ref, tabIndex, "aria-labelledby": labelledBy, ...rest }: ScrollAreaProps): react.JSX.Element;
|
|
52
|
+
|
|
53
|
+
export { ScrollArea, type ScrollAreaProps, THIN_SCROLLBAR_CLASS, useScrollOverflow };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
function useScrollOverflow(ref) {
|
|
6
|
+
const [overflowing, setOverflowing] = useState(false);
|
|
7
|
+
const measure = useCallback(() => {
|
|
8
|
+
const el = ref.current;
|
|
9
|
+
if (!el) return;
|
|
10
|
+
const next = el.scrollHeight > el.clientHeight + 1 || el.scrollWidth > el.clientWidth + 1;
|
|
11
|
+
setOverflowing((prev) => prev === next ? prev : next);
|
|
12
|
+
}, [ref]);
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
measure();
|
|
15
|
+
});
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
const el = ref.current;
|
|
18
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
19
|
+
const ro = new ResizeObserver(measure);
|
|
20
|
+
ro.observe(el);
|
|
21
|
+
for (const child of Array.from(el.children)) ro.observe(child);
|
|
22
|
+
return () => ro.disconnect();
|
|
23
|
+
}, [ref, measure]);
|
|
24
|
+
return overflowing;
|
|
25
|
+
}
|
|
26
|
+
const THIN_SCROLLBAR_CLASS = cn(
|
|
27
|
+
"[scrollbar-width:thin] [scrollbar-color:var(--border-strong)_transparent]",
|
|
28
|
+
"[&::-webkit-scrollbar]:size-2 [&::-webkit-scrollbar-track]:bg-transparent",
|
|
29
|
+
"[&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-[var(--border-strong)]"
|
|
30
|
+
);
|
|
31
|
+
const AXIS = {
|
|
32
|
+
vertical: "overflow-y-auto overflow-x-hidden",
|
|
33
|
+
horizontal: "overflow-x-auto overflow-y-hidden",
|
|
34
|
+
both: "overflow-auto"
|
|
35
|
+
};
|
|
36
|
+
function ScrollArea({
|
|
37
|
+
orientation = "vertical",
|
|
38
|
+
label,
|
|
39
|
+
className,
|
|
40
|
+
ref,
|
|
41
|
+
tabIndex,
|
|
42
|
+
"aria-labelledby": labelledBy,
|
|
43
|
+
...rest
|
|
44
|
+
}) {
|
|
45
|
+
const inner = useRef(null);
|
|
46
|
+
const overflowing = useScrollOverflow(inner);
|
|
47
|
+
const setRef = useCallback(
|
|
48
|
+
(el) => {
|
|
49
|
+
inner.current = el;
|
|
50
|
+
if (typeof ref === "function") ref(el);
|
|
51
|
+
else if (ref) ref.current = el;
|
|
52
|
+
},
|
|
53
|
+
[ref]
|
|
54
|
+
);
|
|
55
|
+
const named = label !== void 0 || labelledBy !== void 0;
|
|
56
|
+
return /* @__PURE__ */ jsx(
|
|
57
|
+
"div",
|
|
58
|
+
{
|
|
59
|
+
...rest,
|
|
60
|
+
ref: setRef,
|
|
61
|
+
role: named ? "region" : void 0,
|
|
62
|
+
"aria-label": label,
|
|
63
|
+
"aria-labelledby": labelledBy,
|
|
64
|
+
tabIndex: tabIndex ?? (overflowing ? 0 : void 0),
|
|
65
|
+
"data-overflowing": overflowing || void 0,
|
|
66
|
+
"data-orientation": orientation,
|
|
67
|
+
className: cn(
|
|
68
|
+
"relative min-h-0 min-w-0",
|
|
69
|
+
AXIS[orientation],
|
|
70
|
+
THIN_SCROLLBAR_CLASS,
|
|
71
|
+
// An OUTLINE, inset: a ring is a box-shadow, which paints beneath the content —
|
|
72
|
+
// a row with its own background would cover exactly the part of the ring that
|
|
73
|
+
// says where focus is. An outline paints above it, and the negative offset
|
|
74
|
+
// keeps it inside a parent that clips.
|
|
75
|
+
"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--brand)]",
|
|
76
|
+
className
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
ScrollArea,
|
|
83
|
+
THIN_SCROLLBAR_CLASS,
|
|
84
|
+
useScrollOverflow
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=scroll-area.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/scroll-area.tsx"],"sourcesContent":["import { useCallback, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, Ref, RefObject } from \"react\";\nimport { cn } from \"../lib/cn\";\n\n/**\n * Whether `ref`'s element currently overflows its box — the one fact a scroll\n * container needs to decide whether it is a keyboard stop.\n *\n * Measured on every commit (cheap: two reads, and state is set only on a change) and\n * whenever the element or one of its direct children resizes, which covers both\n * \"the panel got narrower\" and \"the rows arrived\". Without `ResizeObserver` (jsdom,\n * very old engines) the per-commit measurement is what remains.\n *\n * Shared with {@link Table}'s overflow wrapper, which has the same obligation.\n */\nexport function useScrollOverflow(ref: RefObject<HTMLElement | null>): boolean {\n const [overflowing, setOverflowing] = useState(false);\n\n const measure = useCallback(() => {\n const el = ref.current;\n if (!el) return;\n const next = el.scrollHeight > el.clientHeight + 1 || el.scrollWidth > el.clientWidth + 1;\n setOverflowing((prev) => (prev === next ? prev : next));\n }, [ref]);\n\n // No dependency list on purpose: content changes arrive as re-renders.\n useLayoutEffect(() => {\n measure();\n });\n\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el || typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(measure);\n ro.observe(el);\n for (const child of Array.from(el.children)) ro.observe(child);\n return () => ro.disconnect();\n }, [ref, measure]);\n\n return overflowing;\n}\n\n/** Thin, token-coloured scrollbars. `scrollbar-width` / `scrollbar-color` are the\n * standard (Firefox, Chrome 121+); the `::-webkit-scrollbar` rules are for Safari,\n * which has neither. Where the standard pair applies, Chrome ignores the rest. */\nexport const THIN_SCROLLBAR_CLASS = cn(\n \"[scrollbar-width:thin] [scrollbar-color:var(--border-strong)_transparent]\",\n \"[&::-webkit-scrollbar]:size-2 [&::-webkit-scrollbar-track]:bg-transparent\",\n \"[&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-[var(--border-strong)]\",\n);\n\nconst AXIS = {\n vertical: \"overflow-y-auto overflow-x-hidden\",\n horizontal: \"overflow-x-auto overflow-y-hidden\",\n both: \"overflow-auto\",\n} as const;\n\nexport interface ScrollAreaProps extends ComponentPropsWithoutRef<\"div\"> {\n /** Which way the content may scroll. Default `vertical`. */\n orientation?: keyof typeof AXIS;\n /**\n * The region's accessible name. With it (or `aria-labelledby`) the container is a\n * `role=\"region\"` a reader can find and name; without it, it is a plain box.\n * Name every ScrollArea whose content can overflow: once it is a tab stop, a\n * reader announces it on focus, and an unnamed one is announced as nothing.\n */\n label?: string;\n ref?: Ref<HTMLDivElement>;\n}\n\n/**\n * A styled overflow container: a thin scrollbar in the kit's colours, a focus ring,\n * and the keyboard access a plain `overflow-auto` div does not have.\n *\n * kastlan used Radix's ScrollArea for this and nothing else from it. Radix draws a\n * custom scrollbar in JavaScript; the platform's own, thinned and coloured, costs no\n * script and keeps native momentum, overscroll and scrollbar-dragging behaviour.\n *\n * A container that scrolls but holds nothing focusable cannot be scrolled from the\n * keyboard at all — Tab jumps straight past it (WCAG 2.1.1). So while the content\n * overflows, the container itself is a tab stop (`tabIndex=0`), and arrow keys and\n * Page Up/Down scroll it natively. While nothing overflows it is not, so a short list\n * adds no empty stop to the tab order.\n */\nexport function ScrollArea({\n orientation = \"vertical\",\n label,\n className,\n ref,\n tabIndex,\n \"aria-labelledby\": labelledBy,\n ...rest\n}: ScrollAreaProps) {\n const inner = useRef<HTMLDivElement | null>(null);\n const overflowing = useScrollOverflow(inner);\n const setRef = useCallback(\n (el: HTMLDivElement | null) => {\n inner.current = el;\n if (typeof ref === \"function\") ref(el);\n else if (ref) ref.current = el;\n },\n [ref],\n );\n const named = label !== undefined || labelledBy !== undefined;\n\n return (\n <div\n {...rest}\n ref={setRef}\n role={named ? \"region\" : undefined}\n aria-label={label}\n aria-labelledby={labelledBy}\n tabIndex={tabIndex ?? (overflowing ? 0 : undefined)}\n data-overflowing={overflowing || undefined}\n data-orientation={orientation}\n className={cn(\n \"relative min-h-0 min-w-0\",\n AXIS[orientation],\n THIN_SCROLLBAR_CLASS,\n // An OUTLINE, inset: a ring is a box-shadow, which paints beneath the content —\n // a row with its own background would cover exactly the part of the ring that\n // says where focus is. An outline paints above it, and the negative offset\n // keeps it inside a parent that clips.\n \"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--brand)]\",\n className,\n )}\n />\n );\n}\n"],"mappings":";AA0GI;AA1GJ,SAAS,aAAa,iBAAiB,QAAQ,gBAAgB;AAE/D,SAAS,UAAU;AAaZ,SAAS,kBAAkB,KAA6C;AAC7E,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AAEpD,QAAM,UAAU,YAAY,MAAM;AAChC,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AACT,UAAM,OAAO,GAAG,eAAe,GAAG,eAAe,KAAK,GAAG,cAAc,GAAG,cAAc;AACxF,mBAAe,CAAC,SAAU,SAAS,OAAO,OAAO,IAAK;AAAA,EACxD,GAAG,CAAC,GAAG,CAAC;AAGR,kBAAgB,MAAM;AACpB,YAAQ;AAAA,EACV,CAAC;AAED,kBAAgB,MAAM;AACpB,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,MAAM,OAAO,mBAAmB,YAAa;AAClD,UAAM,KAAK,IAAI,eAAe,OAAO;AACrC,OAAG,QAAQ,EAAE;AACb,eAAW,SAAS,MAAM,KAAK,GAAG,QAAQ,EAAG,IAAG,QAAQ,KAAK;AAC7D,WAAO,MAAM,GAAG,WAAW;AAAA,EAC7B,GAAG,CAAC,KAAK,OAAO,CAAC;AAEjB,SAAO;AACT;AAKO,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,OAAO;AAAA,EACX,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AACR;AA6BO,SAAS,WAAW;AAAA,EACzB,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,mBAAmB;AAAA,EACnB,GAAG;AACL,GAAoB;AAClB,QAAM,QAAQ,OAA8B,IAAI;AAChD,QAAM,cAAc,kBAAkB,KAAK;AAC3C,QAAM,SAAS;AAAA,IACb,CAAC,OAA8B;AAC7B,YAAM,UAAU;AAChB,UAAI,OAAO,QAAQ,WAAY,KAAI,EAAE;AAAA,eAC5B,IAAK,KAAI,UAAU;AAAA,IAC9B;AAAA,IACA,CAAC,GAAG;AAAA,EACN;AACA,QAAM,QAAQ,UAAU,UAAa,eAAe;AAEpD,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,KAAK;AAAA,MACL,MAAM,QAAQ,WAAW;AAAA,MACzB,cAAY;AAAA,MACZ,mBAAiB;AAAA,MACjB,UAAU,aAAa,cAAc,IAAI;AAAA,MACzC,oBAAkB,eAAe;AAAA,MACjC,oBAAkB;AAAA,MAClB,WAAW;AAAA,QACT;AAAA,QACA,KAAK,WAAW;AAAA,QAChB;AAAA;AAAA;AAAA;AAAA;AAAA,QAKA;AAAA,QACA;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef } from 'react';
|
|
3
|
+
|
|
4
|
+
interface SeparatorProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "role"> {
|
|
5
|
+
orientation?: "horizontal" | "vertical";
|
|
6
|
+
/**
|
|
7
|
+
* `false` (default, as in Radix): a real `role="separator"`, which a reader
|
|
8
|
+
* announces — right between groups of controls, a menu's sections, a toolbar's
|
|
9
|
+
* halves. `true`: a line for the eye only (`role="none"`), for a divider that
|
|
10
|
+
* separates nothing the structure does not already separate.
|
|
11
|
+
*/
|
|
12
|
+
decorative?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A hairline between two things.
|
|
16
|
+
*
|
|
17
|
+
* kastlan's other Radix leftover. `aria-orientation` is set only when vertical:
|
|
18
|
+
* horizontal is the role's default, and stating a default is noise in every reader's
|
|
19
|
+
* verbose mode.
|
|
20
|
+
*/
|
|
21
|
+
declare function Separator({ orientation, decorative, className, ...rest }: SeparatorProps): react.JSX.Element;
|
|
22
|
+
|
|
23
|
+
export { Separator, type SeparatorProps };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
function Separator({ orientation = "horizontal", decorative = false, className, ...rest }) {
|
|
5
|
+
const vertical = orientation === "vertical";
|
|
6
|
+
return /* @__PURE__ */ jsx(
|
|
7
|
+
"div",
|
|
8
|
+
{
|
|
9
|
+
...rest,
|
|
10
|
+
role: decorative ? "none" : "separator",
|
|
11
|
+
"aria-orientation": !decorative && vertical ? "vertical" : void 0,
|
|
12
|
+
"data-orientation": orientation,
|
|
13
|
+
className: cn(
|
|
14
|
+
"shrink-0 bg-[var(--border)]",
|
|
15
|
+
vertical ? "w-px self-stretch" : "h-px w-full",
|
|
16
|
+
className
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
Separator
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=separator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/separator.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef } from \"react\";\nimport { cn } from \"../lib/cn\";\n\nexport interface SeparatorProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"role\"> {\n orientation?: \"horizontal\" | \"vertical\";\n /**\n * `false` (default, as in Radix): a real `role=\"separator\"`, which a reader\n * announces — right between groups of controls, a menu's sections, a toolbar's\n * halves. `true`: a line for the eye only (`role=\"none\"`), for a divider that\n * separates nothing the structure does not already separate.\n */\n decorative?: boolean;\n}\n\n/**\n * A hairline between two things.\n *\n * kastlan's other Radix leftover. `aria-orientation` is set only when vertical:\n * horizontal is the role's default, and stating a default is noise in every reader's\n * verbose mode.\n */\nexport function Separator({ orientation = \"horizontal\", decorative = false, className, ...rest }: SeparatorProps) {\n const vertical = orientation === \"vertical\";\n return (\n <div\n {...rest}\n role={decorative ? \"none\" : \"separator\"}\n aria-orientation={!decorative && vertical ? \"vertical\" : undefined}\n data-orientation={orientation}\n className={cn(\n \"shrink-0 bg-[var(--border)]\",\n vertical ? \"w-px self-stretch\" : \"h-px w-full\",\n className,\n )}\n />\n );\n}\n"],"mappings":";AAwBI;AAvBJ,SAAS,UAAU;AAoBZ,SAAS,UAAU,EAAE,cAAc,cAAc,aAAa,OAAO,WAAW,GAAG,KAAK,GAAmB;AAChH,QAAM,WAAW,gBAAgB;AACjC,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAM,aAAa,SAAS;AAAA,MAC5B,oBAAkB,CAAC,cAAc,WAAW,aAAa;AAAA,MACzD,oBAAkB;AAAA,MAClB,WAAW;AAAA,QACT;AAAA,QACA,WAAW,sBAAsB;AAAA,QACjC;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -9,5 +9,38 @@ declare function niceStep(span: number, target?: number): number;
|
|
|
9
9
|
* accumulating `+= 0.1` would print 0.30000000000000004 on the third tick.
|
|
10
10
|
*/
|
|
11
11
|
declare function niceTicks(domain: readonly [number, number] | undefined, target?: number): number[] | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* {@link niceTicks} on whole numbers only: the step never drops below 1, so a short
|
|
14
|
+
* index series (a domain of [-0.2, 4.2]) is ticked 0 1 2 3 4 instead of every half.
|
|
15
|
+
* `undefined` when the window holds no whole number at all, so the caller can fall
|
|
16
|
+
* back to the fractional ticks of a deep zoom rather than show none.
|
|
17
|
+
*/
|
|
18
|
+
declare function integerTicks(domain: readonly [number, number] | undefined, target?: number): number[] | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The slots of a CATEGORY axis that lie inside a window over their positions.
|
|
21
|
+
*
|
|
22
|
+
* `SeriesChart` draws a category axis on the slot INDEX (see `x.type: "category"`), so a
|
|
23
|
+
* zoom window is a range of fractional positions — [2.6, 8.3] after a drag across the
|
|
24
|
+
* third to ninth month. A tick belongs on every whole slot inside it, and on nothing in
|
|
25
|
+
* between: there is no month 2.5. Recharts' `minTickGap` thins them where they crowd.
|
|
26
|
+
*/
|
|
27
|
+
declare function categoryTicks(domain: readonly [number, number] | undefined, count: number): number[] | undefined;
|
|
28
|
+
/** What one tick of a time axis stands for, which is also how its label is written. */
|
|
29
|
+
type TimeTickUnit = "hour" | "day" | "month" | "year";
|
|
30
|
+
/**
|
|
31
|
+
* Calendar ticks inside a window of epoch milliseconds — midnights, Mondays, month and
|
|
32
|
+
* quarter starts, new years, in LOCAL time — and the unit they are in, so the label can
|
|
33
|
+
* say as much as the tick means ("Mar 26" for a month start, "14 Mar" for a day).
|
|
34
|
+
*
|
|
35
|
+
* The time counterpart of {@link niceTicks}: a 1/2/5 ladder over raw milliseconds lands
|
|
36
|
+
* on 5 × 10⁹ ms, every 58 days, at whatever hour that happens to be. keksdose's price
|
|
37
|
+
* history and cash buffer each thinned their own ticks by hand for exactly this reason.
|
|
38
|
+
*/
|
|
39
|
+
declare function timeTicksWithUnit(domain: readonly [number, number] | undefined, target?: number): {
|
|
40
|
+
ticks: number[];
|
|
41
|
+
unit: TimeTickUnit;
|
|
42
|
+
} | undefined;
|
|
43
|
+
/** {@link timeTicksWithUnit}, ticks only. */
|
|
44
|
+
declare function timeTicks(domain: readonly [number, number] | undefined, target?: number): number[] | undefined;
|
|
12
45
|
|
|
13
|
-
export { niceStep, niceTicks };
|
|
46
|
+
export { type TimeTickUnit, categoryTicks, integerTicks, niceStep, niceTicks, timeTicks, timeTicksWithUnit };
|
|
@@ -22,8 +22,90 @@ function niceTicks(domain, target = TARGET_TICKS) {
|
|
|
22
22
|
}
|
|
23
23
|
return ticks.length ? ticks : void 0;
|
|
24
24
|
}
|
|
25
|
+
function integerTicks(domain, target = TARGET_TICKS) {
|
|
26
|
+
if (!domain) return void 0;
|
|
27
|
+
const [low, high] = domain;
|
|
28
|
+
if (!Number.isFinite(low) || !Number.isFinite(high) || !(high >= low)) return void 0;
|
|
29
|
+
const step = high > low ? Math.max(1, niceStep(high - low, target)) : 1;
|
|
30
|
+
const ticks = [];
|
|
31
|
+
const first = Math.ceil(low / step - 1e-6);
|
|
32
|
+
const last = Math.floor(high / step + 1e-6);
|
|
33
|
+
for (let k = first; k <= last; k++) ticks.push(k * step + 0);
|
|
34
|
+
return ticks.length ? ticks : void 0;
|
|
35
|
+
}
|
|
36
|
+
function categoryTicks(domain, count) {
|
|
37
|
+
if (!domain || !(count > 0)) return void 0;
|
|
38
|
+
const first = Math.max(0, Math.ceil(domain[0] - 1e-6));
|
|
39
|
+
const last = Math.min(count - 1, Math.floor(domain[1] + 1e-6));
|
|
40
|
+
const ticks = [];
|
|
41
|
+
for (let slot = first; slot <= last; slot++) ticks.push(slot);
|
|
42
|
+
return ticks.length ? ticks : void 0;
|
|
43
|
+
}
|
|
44
|
+
const HOUR = 36e5;
|
|
45
|
+
const DAY = 24 * HOUR;
|
|
46
|
+
const TIME_STEPS = [
|
|
47
|
+
{ unit: "hour", n: 1, ms: HOUR },
|
|
48
|
+
{ unit: "hour", n: 3, ms: 3 * HOUR },
|
|
49
|
+
{ unit: "hour", n: 6, ms: 6 * HOUR },
|
|
50
|
+
{ unit: "hour", n: 12, ms: 12 * HOUR },
|
|
51
|
+
{ unit: "day", n: 1, ms: DAY },
|
|
52
|
+
{ unit: "day", n: 2, ms: 2 * DAY },
|
|
53
|
+
{ unit: "day", n: 7, ms: 7 * DAY },
|
|
54
|
+
{ unit: "month", n: 1, ms: 30.44 * DAY },
|
|
55
|
+
{ unit: "month", n: 2, ms: 60.88 * DAY },
|
|
56
|
+
{ unit: "month", n: 3, ms: 91.31 * DAY },
|
|
57
|
+
{ unit: "month", n: 6, ms: 182.62 * DAY }
|
|
58
|
+
];
|
|
59
|
+
function timeTicksWithUnit(domain, target = TARGET_TICKS) {
|
|
60
|
+
if (!domain) return void 0;
|
|
61
|
+
const [low, high] = domain;
|
|
62
|
+
if (!Number.isFinite(low) || !Number.isFinite(high) || !(high > low)) return void 0;
|
|
63
|
+
const rough = (high - low) / Math.max(1, target);
|
|
64
|
+
let unit = "year";
|
|
65
|
+
let n = 1;
|
|
66
|
+
let best = Infinity;
|
|
67
|
+
for (const step of TIME_STEPS) {
|
|
68
|
+
const miss = Math.abs(Math.log(step.ms / rough));
|
|
69
|
+
if (miss < best) {
|
|
70
|
+
best = miss;
|
|
71
|
+
unit = step.unit;
|
|
72
|
+
n = step.n;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const yearN = Math.max(1, Math.round(niceStep((high - low) / (365.25 * DAY), target)));
|
|
76
|
+
if (Math.abs(Math.log(yearN * 365.25 * DAY / rough)) < best) {
|
|
77
|
+
unit = "year";
|
|
78
|
+
n = yearN;
|
|
79
|
+
}
|
|
80
|
+
const start = new Date(low);
|
|
81
|
+
start.setMinutes(0, 0, 0);
|
|
82
|
+
if (unit !== "hour") start.setHours(0);
|
|
83
|
+
if (unit === "month" || unit === "year") start.setDate(1);
|
|
84
|
+
if (unit === "year") start.setMonth(0);
|
|
85
|
+
if (unit === "hour") start.setHours(start.getHours() - start.getHours() % n);
|
|
86
|
+
if (unit === "day" && n === 7) start.setDate(start.getDate() - (start.getDay() + 6) % 7);
|
|
87
|
+
if (unit === "month") start.setMonth(start.getMonth() - start.getMonth() % n);
|
|
88
|
+
if (unit === "year") start.setFullYear(start.getFullYear() - start.getFullYear() % n);
|
|
89
|
+
const ticks = [];
|
|
90
|
+
const at = new Date(start);
|
|
91
|
+
for (let guard = 0; guard < 1e3 && at.getTime() <= high; guard++) {
|
|
92
|
+
if (at.getTime() >= low) ticks.push(at.getTime());
|
|
93
|
+
if (unit === "hour") at.setHours(at.getHours() + n);
|
|
94
|
+
else if (unit === "day") at.setDate(at.getDate() + n);
|
|
95
|
+
else if (unit === "month") at.setMonth(at.getMonth() + n);
|
|
96
|
+
else at.setFullYear(at.getFullYear() + n);
|
|
97
|
+
}
|
|
98
|
+
return ticks.length ? { ticks, unit } : void 0;
|
|
99
|
+
}
|
|
100
|
+
function timeTicks(domain, target = TARGET_TICKS) {
|
|
101
|
+
return timeTicksWithUnit(domain, target)?.ticks;
|
|
102
|
+
}
|
|
25
103
|
export {
|
|
104
|
+
categoryTicks,
|
|
105
|
+
integerTicks,
|
|
26
106
|
niceStep,
|
|
27
|
-
niceTicks
|
|
107
|
+
niceTicks,
|
|
108
|
+
timeTicks,
|
|
109
|
+
timeTicksWithUnit
|
|
28
110
|
};
|
|
29
111
|
//# sourceMappingURL=series-chart-ticks.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/series-chart-ticks.ts"],"sourcesContent":["// Round tick values for `SeriesChart` — internal, not re-exported by `@eifi1/ui-kit/chart`.\n//\n// Handed an explicit numeric domain, recharts divides it into equal parts and prints\n// wherever those land: a fitted band of [-11, 811] comes out as -11, 189, 389 … and a\n// padded [89, 1011] as 89, 289, 489 — numbers nobody can put a ruler against. These\n// are the ticks a person would draw instead: every multiple of a 1, 2 or 5 × 10^n step\n// that lies INSIDE the domain. The domain itself is left alone, so the 4 % of air the\n// fit leaves and a zoom window's exact edges both survive; the frame simply does not\n// get a number of its own.\n\n/** Roughly how many ticks to aim for. The 1/2/5 ladder lands anywhere from about half\n * to about twice this, which is the price of every tick being a round number. */\nconst TARGET_TICKS = 5;\n\n/** The round step closest to `span / target`: 1, 2 or 5 times a power of ten. */\nexport function niceStep(span: number, target: number = TARGET_TICKS): number {\n const rough = span / Math.max(1, target);\n const magnitude = 10 ** Math.floor(Math.log10(rough));\n const norm = rough / magnitude;\n const factor = norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10;\n return factor * magnitude;\n}\n\n/**\n * The round ticks inside `[low, high]`, ascending — or `undefined` when there is no\n * span to divide (no domain, a zero-height one, anything non-finite), which leaves the\n * choice to recharts.\n *\n * Each tick is computed as `k × step` and then rounded to the step's own decimals:\n * accumulating `+= 0.1` would print 0.30000000000000004 on the third tick.\n */\nexport function niceTicks(\n domain: readonly [number, number] | undefined,\n target: number = TARGET_TICKS,\n): number[] | undefined {\n if (!domain) return undefined;\n const [low, high] = domain;\n if (!Number.isFinite(low) || !Number.isFinite(high) || !(high > low)) return undefined;\n const step = niceStep(high - low, target);\n if (!(step > 0) || !Number.isFinite(step)) return undefined;\n const decimals = Math.max(0, -Math.floor(Math.log10(step)));\n const ticks: number[] = [];\n // Half a step's tolerance per end would admit ticks outside the frame; a millionth\n // of one only forgives the float error in `low / step`.\n const first = Math.ceil(low / step - 1e-6);\n const last = Math.floor(high / step + 1e-6);\n for (let k = first; k <= last; k++) {\n // `+ 0` turns the -0 that `Number(\"-0.00\")` gives back into a plain 0.\n ticks.push(Number((k * step).toFixed(decimals)) + 0);\n }\n return ticks.length ? ticks : undefined;\n}\n"],"mappings":";AAYA,MAAM,eAAe;AAGd,SAAS,SAAS,MAAc,SAAiB,cAAsB;AAC5E,QAAM,QAAQ,OAAO,KAAK,IAAI,GAAG,MAAM;AACvC,QAAM,YAAY,MAAM,KAAK,MAAM,KAAK,MAAM,KAAK,CAAC;AACpD,QAAM,OAAO,QAAQ;AACrB,QAAM,SAAS,OAAO,MAAM,IAAI,OAAO,IAAI,IAAI,OAAO,IAAI,IAAI;AAC9D,SAAO,SAAS;AAClB;AAUO,SAAS,UACd,QACA,SAAiB,cACK;AACtB,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,CAAC,KAAK,IAAI,IAAI;AACpB,MAAI,CAAC,OAAO,SAAS,GAAG,KAAK,CAAC,OAAO,SAAS,IAAI,KAAK,EAAE,OAAO,KAAM,QAAO;AAC7E,QAAM,OAAO,SAAS,OAAO,KAAK,MAAM;AACxC,MAAI,EAAE,OAAO,MAAM,CAAC,OAAO,SAAS,IAAI,EAAG,QAAO;AAClD,QAAM,WAAW,KAAK,IAAI,GAAG,CAAC,KAAK,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC;AAC1D,QAAM,QAAkB,CAAC;AAGzB,QAAM,QAAQ,KAAK,KAAK,MAAM,OAAO,IAAI;AACzC,QAAM,OAAO,KAAK,MAAM,OAAO,OAAO,IAAI;AAC1C,WAAS,IAAI,OAAO,KAAK,MAAM,KAAK;AAElC,UAAM,KAAK,QAAQ,IAAI,MAAM,QAAQ,QAAQ,CAAC,IAAI,CAAC;AAAA,EACrD;AACA,SAAO,MAAM,SAAS,QAAQ;AAChC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/series-chart-ticks.ts"],"sourcesContent":["// Round tick values for `SeriesChart` — internal, not re-exported by `@eifi1/ui-kit/chart`.\n//\n// Handed an explicit numeric domain, recharts divides it into equal parts and prints\n// wherever those land: a fitted band of [-11, 811] comes out as -11, 189, 389 … and a\n// padded [89, 1011] as 89, 289, 489 — numbers nobody can put a ruler against. These\n// are the ticks a person would draw instead: every multiple of a 1, 2 or 5 × 10^n step\n// that lies INSIDE the domain. The domain itself is left alone, so the 4 % of air the\n// fit leaves and a zoom window's exact edges both survive; the frame simply does not\n// get a number of its own.\n\n/** Roughly how many ticks to aim for. The 1/2/5 ladder lands anywhere from about half\n * to about twice this, which is the price of every tick being a round number. */\nconst TARGET_TICKS = 5;\n\n/** The round step closest to `span / target`: 1, 2 or 5 times a power of ten. */\nexport function niceStep(span: number, target: number = TARGET_TICKS): number {\n const rough = span / Math.max(1, target);\n const magnitude = 10 ** Math.floor(Math.log10(rough));\n const norm = rough / magnitude;\n const factor = norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10;\n return factor * magnitude;\n}\n\n/**\n * The round ticks inside `[low, high]`, ascending — or `undefined` when there is no\n * span to divide (no domain, a zero-height one, anything non-finite), which leaves the\n * choice to recharts.\n *\n * Each tick is computed as `k × step` and then rounded to the step's own decimals:\n * accumulating `+= 0.1` would print 0.30000000000000004 on the third tick.\n */\nexport function niceTicks(\n domain: readonly [number, number] | undefined,\n target: number = TARGET_TICKS,\n): number[] | undefined {\n if (!domain) return undefined;\n const [low, high] = domain;\n if (!Number.isFinite(low) || !Number.isFinite(high) || !(high > low)) return undefined;\n const step = niceStep(high - low, target);\n if (!(step > 0) || !Number.isFinite(step)) return undefined;\n const decimals = Math.max(0, -Math.floor(Math.log10(step)));\n const ticks: number[] = [];\n // Half a step's tolerance per end would admit ticks outside the frame; a millionth\n // of one only forgives the float error in `low / step`.\n const first = Math.ceil(low / step - 1e-6);\n const last = Math.floor(high / step + 1e-6);\n for (let k = first; k <= last; k++) {\n // `+ 0` turns the -0 that `Number(\"-0.00\")` gives back into a plain 0.\n ticks.push(Number((k * step).toFixed(decimals)) + 0);\n }\n return ticks.length ? ticks : undefined;\n}\n\n/**\n * {@link niceTicks} on whole numbers only: the step never drops below 1, so a short\n * index series (a domain of [-0.2, 4.2]) is ticked 0 1 2 3 4 instead of every half.\n * `undefined` when the window holds no whole number at all, so the caller can fall\n * back to the fractional ticks of a deep zoom rather than show none.\n */\nexport function integerTicks(\n domain: readonly [number, number] | undefined,\n target: number = TARGET_TICKS,\n): number[] | undefined {\n if (!domain) return undefined;\n const [low, high] = domain;\n if (!Number.isFinite(low) || !Number.isFinite(high) || !(high >= low)) return undefined;\n const step = high > low ? Math.max(1, niceStep(high - low, target)) : 1;\n const ticks: number[] = [];\n const first = Math.ceil(low / step - 1e-6);\n const last = Math.floor(high / step + 1e-6);\n for (let k = first; k <= last; k++) ticks.push(k * step + 0);\n return ticks.length ? ticks : undefined;\n}\n\n/**\n * The slots of a CATEGORY axis that lie inside a window over their positions.\n *\n * `SeriesChart` draws a category axis on the slot INDEX (see `x.type: \"category\"`), so a\n * zoom window is a range of fractional positions — [2.6, 8.3] after a drag across the\n * third to ninth month. A tick belongs on every whole slot inside it, and on nothing in\n * between: there is no month 2.5. Recharts' `minTickGap` thins them where they crowd.\n */\nexport function categoryTicks(\n domain: readonly [number, number] | undefined,\n count: number,\n): number[] | undefined {\n if (!domain || !(count > 0)) return undefined;\n const first = Math.max(0, Math.ceil(domain[0] - 1e-6));\n const last = Math.min(count - 1, Math.floor(domain[1] + 1e-6));\n const ticks: number[] = [];\n for (let slot = first; slot <= last; slot++) ticks.push(slot);\n return ticks.length ? ticks : undefined;\n}\n\n/** What one tick of a time axis stands for, which is also how its label is written. */\nexport type TimeTickUnit = \"hour\" | \"day\" | \"month\" | \"year\";\n\nconst HOUR = 3_600_000;\nconst DAY = 24 * HOUR;\n\n/**\n * The calendar steps a time axis may tick in, with their rough length for choosing.\n * Months and years are stepped with `Date` setters, not by adding their \"length\":\n * thirty days from the 1st of February is the 3rd of March, and a tick that promises a\n * month start and prints the 3rd is the ruler lying.\n */\nconst TIME_STEPS: readonly { unit: TimeTickUnit; n: number; ms: number }[] = [\n { unit: \"hour\", n: 1, ms: HOUR },\n { unit: \"hour\", n: 3, ms: 3 * HOUR },\n { unit: \"hour\", n: 6, ms: 6 * HOUR },\n { unit: \"hour\", n: 12, ms: 12 * HOUR },\n { unit: \"day\", n: 1, ms: DAY },\n { unit: \"day\", n: 2, ms: 2 * DAY },\n { unit: \"day\", n: 7, ms: 7 * DAY },\n { unit: \"month\", n: 1, ms: 30.44 * DAY },\n { unit: \"month\", n: 2, ms: 60.88 * DAY },\n { unit: \"month\", n: 3, ms: 91.31 * DAY },\n { unit: \"month\", n: 6, ms: 182.62 * DAY },\n];\n\n/**\n * Calendar ticks inside a window of epoch milliseconds — midnights, Mondays, month and\n * quarter starts, new years, in LOCAL time — and the unit they are in, so the label can\n * say as much as the tick means (\"Mar 26\" for a month start, \"14 Mar\" for a day).\n *\n * The time counterpart of {@link niceTicks}: a 1/2/5 ladder over raw milliseconds lands\n * on 5 × 10⁹ ms, every 58 days, at whatever hour that happens to be. keksdose's price\n * history and cash buffer each thinned their own ticks by hand for exactly this reason.\n */\nexport function timeTicksWithUnit(\n domain: readonly [number, number] | undefined,\n target: number = TARGET_TICKS,\n): { ticks: number[]; unit: TimeTickUnit } | undefined {\n if (!domain) return undefined;\n const [low, high] = domain;\n if (!Number.isFinite(low) || !Number.isFinite(high) || !(high > low)) return undefined;\n const rough = (high - low) / Math.max(1, target);\n let unit: TimeTickUnit = \"year\";\n let n = 1;\n let best = Infinity;\n for (const step of TIME_STEPS) {\n const miss = Math.abs(Math.log(step.ms / rough));\n if (miss < best) {\n best = miss;\n unit = step.unit;\n n = step.n;\n }\n }\n // The year ladder competes on the same terms as the table: one year is 365.25 days.\n const yearN = Math.max(1, Math.round(niceStep((high - low) / (365.25 * DAY), target)));\n if (Math.abs(Math.log((yearN * 365.25 * DAY) / rough)) < best) {\n unit = \"year\";\n n = yearN;\n }\n\n const start = new Date(low);\n start.setMinutes(0, 0, 0);\n if (unit !== \"hour\") start.setHours(0);\n if (unit === \"month\" || unit === \"year\") start.setDate(1);\n if (unit === \"year\") start.setMonth(0);\n // Onto the step's own grid: hours of the day, Mondays, month indices, years — so a\n // zoom window starting on a Wednesday still ticks on Mondays.\n if (unit === \"hour\") start.setHours(start.getHours() - (start.getHours() % n));\n if (unit === \"day\" && n === 7) start.setDate(start.getDate() - ((start.getDay() + 6) % 7));\n if (unit === \"month\") start.setMonth(start.getMonth() - (start.getMonth() % n));\n if (unit === \"year\") start.setFullYear(start.getFullYear() - (start.getFullYear() % n));\n\n const ticks: number[] = [];\n const at = new Date(start);\n // Bounded, so a window of a million years cannot hang the render.\n for (let guard = 0; guard < 1000 && at.getTime() <= high; guard++) {\n if (at.getTime() >= low) ticks.push(at.getTime());\n if (unit === \"hour\") at.setHours(at.getHours() + n);\n else if (unit === \"day\") at.setDate(at.getDate() + n);\n else if (unit === \"month\") at.setMonth(at.getMonth() + n);\n else at.setFullYear(at.getFullYear() + n);\n }\n return ticks.length ? { ticks, unit } : undefined;\n}\n\n/** {@link timeTicksWithUnit}, ticks only. */\nexport function timeTicks(\n domain: readonly [number, number] | undefined,\n target: number = TARGET_TICKS,\n): number[] | undefined {\n return timeTicksWithUnit(domain, target)?.ticks;\n}\n"],"mappings":"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","names":[]}
|