orbital-ds 0.9.0 → 0.10.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/AGENTS.md +18 -1
- package/components.manifest.json +388 -1
- package/contract.json +5 -0
- package/dist/hooks/use-ds-messages.d.ts +59 -1
- package/dist/hooks/use-ds-messages.js +6 -0
- package/dist/lib/echarts-presets.d.ts +153 -0
- package/dist/lib/echarts-presets.js +311 -0
- package/dist/lib/echarts-server.d.ts +15 -0
- package/dist/lib/echarts-server.js +15 -0
- package/dist/lib/echarts-setup.d.ts +4 -0
- package/dist/lib/echarts-setup.js +60 -0
- package/dist/lib/echarts-theme.d.ts +36 -0
- package/dist/lib/echarts-theme.js +142 -0
- package/dist/lib/i18n.d.ts +57 -1
- package/dist/lib/i18n.js +169 -3
- package/dist/styles.css +43 -5
- package/dist/themes/friendly.css +3 -1
- package/dist/themes/friendly.scoped.css +3 -1
- package/dist/themes/friendly.tokens.json +45 -0
- package/dist/themes/qratia.tokens.json +49 -0
- package/dist/themes/tactile.css +3 -1
- package/dist/themes/tactile.scoped.css +3 -1
- package/dist/themes/tactile.tokens.json +42 -0
- package/dist/themes/twitter.css +3 -1
- package/dist/themes/twitter.scoped.css +3 -1
- package/dist/themes/twitter.tokens.json +45 -0
- package/dist/themes/zyro.css +3 -1
- package/dist/themes/zyro.scoped.css +3 -1
- package/dist/themes/zyro.tokens.json +46 -0
- package/dist/ui/calendar.js +4 -0
- package/dist/ui/code-block.d.ts +14 -0
- package/dist/ui/code-block.js +106 -0
- package/dist/ui/data-table.d.ts +185 -0
- package/dist/ui/data-table.js +714 -0
- package/dist/ui/date-picker.d.ts +65 -0
- package/dist/ui/date-picker.js +362 -0
- package/dist/ui/dropzone.d.ts +42 -0
- package/dist/ui/dropzone.js +220 -0
- package/dist/ui/echart.d.ts +31 -0
- package/dist/ui/echart.js +88 -0
- package/dist/ui/eyebrow.d.ts +7 -0
- package/dist/ui/eyebrow.js +16 -0
- package/dist/ui/filter-bar.d.ts +36 -0
- package/dist/ui/filter-bar.js +193 -0
- package/dist/ui/heatmap-grid.d.ts +39 -0
- package/dist/ui/heatmap-grid.js +195 -0
- package/dist/ui/i18n-provider.js +3 -3
- package/dist/ui/legend.d.ts +30 -0
- package/dist/ui/legend.js +124 -0
- package/dist/ui/notifications.d.ts +69 -0
- package/dist/ui/notifications.js +335 -0
- package/dist/ui/page-header.d.ts +19 -0
- package/dist/ui/page-header.js +132 -0
- package/dist/ui/panel.d.ts +12 -0
- package/dist/ui/panel.js +41 -0
- package/dist/ui/proportion-bar.d.ts +27 -0
- package/dist/ui/proportion-bar.js +92 -0
- package/dist/ui/sparkline.d.ts +26 -0
- package/dist/ui/sparkline.js +97 -0
- package/dist/ui/stat.d.ts +61 -0
- package/dist/ui/stat.js +238 -0
- package/dist/ui/status-badge.d.ts +38 -0
- package/dist/ui/status-badge.js +153 -0
- package/dist/ui/stepper.d.ts +20 -0
- package/dist/ui/stepper.js +152 -0
- package/dist/ui/timeline.d.ts +29 -0
- package/dist/ui/timeline.js +157 -0
- package/docs/GRAFICOS.md +125 -0
- package/docs/adr/0001-rtl-clases-logicas.md +78 -0
- package/package.json +171 -160
- package/scripts/doctor.mjs +0 -0
- package/scripts/i18n.mjs +0 -0
- package/scripts/init.mjs +1 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as echarts from 'echarts/core';
|
|
3
|
+
import { EChartsCoreOption } from 'echarts/core';
|
|
4
|
+
export { EChartsCoreOption as DsEChartOption } from 'echarts/core';
|
|
5
|
+
|
|
6
|
+
interface EChartHandle {
|
|
7
|
+
/** Instancia de ECharts (para eventos o acciones avanzadas). */
|
|
8
|
+
getInstance(): ReturnType<typeof echarts.init> | null;
|
|
9
|
+
/** SVG del gráfico tal como se ve (solo con renderer SVG). */
|
|
10
|
+
toSvg(): string | null;
|
|
11
|
+
/** Descarga el gráfico como SVG o PNG. */
|
|
12
|
+
download(type?: "svg" | "png", fileName?: string): void;
|
|
13
|
+
}
|
|
14
|
+
interface EChartProps extends Omit<React.ComponentProps<"div">, "ref"> {
|
|
15
|
+
option: EChartsCoreOption;
|
|
16
|
+
/** Alto del gráfico (cualquier medida CSS). El ancho es el del contenedor. */
|
|
17
|
+
height?: string | number;
|
|
18
|
+
renderer?: "svg" | "canvas";
|
|
19
|
+
/** Reemplaza las series en vez de fusionarlas al cambiar `option`. */
|
|
20
|
+
notMerge?: boolean;
|
|
21
|
+
/** Muestra los botones de descargar SVG y PNG. */
|
|
22
|
+
exportable?: boolean;
|
|
23
|
+
fileName?: string;
|
|
24
|
+
/** Sin datos: muestra el texto «Sin datos» en lugar del gráfico. */
|
|
25
|
+
empty?: boolean;
|
|
26
|
+
/** Eventos de ECharts: { click: (p) => …, legendselectchanged: … } */
|
|
27
|
+
onEvents?: Record<string, (params: unknown) => void>;
|
|
28
|
+
}
|
|
29
|
+
declare const EChart: React.ForwardRefExoticComponent<EChartProps & React.RefAttributes<EChartHandle>>;
|
|
30
|
+
|
|
31
|
+
export { EChart, type EChartHandle, type EChartProps };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { DownloadIcon } from "lucide-react";
|
|
5
|
+
import { useDsMessages } from "../hooks/use-ds-messages";
|
|
6
|
+
import { echarts } from "../lib/echarts-setup";
|
|
7
|
+
import { dsEchartsTheme } from "../lib/echarts-theme";
|
|
8
|
+
import { cn } from "../lib/utils";
|
|
9
|
+
import { Button } from "./button";
|
|
10
|
+
const EChart = React.forwardRef(function EChart2({ option, height = 320, renderer = "svg", notMerge = false, exportable = false, fileName = "grafico", empty = false, onEvents, className, ...props }, ref) {
|
|
11
|
+
const t = useDsMessages();
|
|
12
|
+
const box = React.useRef(null);
|
|
13
|
+
const chart = React.useRef(null);
|
|
14
|
+
const optionRef = React.useRef(option);
|
|
15
|
+
optionRef.current = option;
|
|
16
|
+
const [themeKey, setThemeKey] = React.useState(0);
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
const observer = new MutationObserver(() => setThemeKey((k) => k + 1));
|
|
19
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "data-theme", "data-variant", "style"] });
|
|
20
|
+
const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
21
|
+
const onMotion = () => setThemeKey((k) => k + 1);
|
|
22
|
+
mql.addEventListener("change", onMotion);
|
|
23
|
+
return () => {
|
|
24
|
+
observer.disconnect();
|
|
25
|
+
mql.removeEventListener("change", onMotion);
|
|
26
|
+
};
|
|
27
|
+
}, []);
|
|
28
|
+
React.useEffect(() => {
|
|
29
|
+
if (!box.current || empty) return;
|
|
30
|
+
const instance = echarts.init(box.current, dsEchartsTheme(box.current), { renderer });
|
|
31
|
+
chart.current = instance;
|
|
32
|
+
instance.setOption(optionRef.current);
|
|
33
|
+
for (const [name, handler] of Object.entries(onEvents ?? {})) instance.on(name, handler);
|
|
34
|
+
const resize = new ResizeObserver(() => instance.resize());
|
|
35
|
+
resize.observe(box.current);
|
|
36
|
+
return () => {
|
|
37
|
+
resize.disconnect();
|
|
38
|
+
instance.dispose();
|
|
39
|
+
chart.current = null;
|
|
40
|
+
};
|
|
41
|
+
}, [themeKey, renderer, empty]);
|
|
42
|
+
React.useEffect(() => {
|
|
43
|
+
chart.current?.setOption(option, { notMerge });
|
|
44
|
+
}, [option, notMerge]);
|
|
45
|
+
const toSvg = React.useCallback(() => {
|
|
46
|
+
const c = chart.current;
|
|
47
|
+
return renderer === "svg" && c?.renderToSVGString ? c.renderToSVGString() : null;
|
|
48
|
+
}, [renderer]);
|
|
49
|
+
const download = React.useCallback(
|
|
50
|
+
(type = "svg", name = fileName) => {
|
|
51
|
+
const c = chart.current;
|
|
52
|
+
if (!c) return;
|
|
53
|
+
let url;
|
|
54
|
+
if (type === "svg" && renderer === "svg") {
|
|
55
|
+
const svg = toSvg();
|
|
56
|
+
if (!svg) return;
|
|
57
|
+
url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml" }));
|
|
58
|
+
} else {
|
|
59
|
+
url = c.getDataURL({ type: type === "svg" ? "svg" : "png", pixelRatio: 2, backgroundColor: "transparent" });
|
|
60
|
+
}
|
|
61
|
+
const a = document.createElement("a");
|
|
62
|
+
a.href = url;
|
|
63
|
+
a.download = `${name}.${type}`;
|
|
64
|
+
a.click();
|
|
65
|
+
if (url.startsWith("blob:")) URL.revokeObjectURL(url);
|
|
66
|
+
},
|
|
67
|
+
[fileName, renderer, toSvg]
|
|
68
|
+
);
|
|
69
|
+
React.useImperativeHandle(ref, () => ({ getInstance: () => chart.current, toSvg, download }), [toSvg, download]);
|
|
70
|
+
return /* @__PURE__ */ jsxs("div", { "data-slot": "echart", "data-renderer": renderer, className: cn("group/echart relative min-w-0", className), ...props, children: [
|
|
71
|
+
exportable && !empty && /* @__PURE__ */ jsxs("div", { "data-slot": "echart-actions", className: "absolute end-0 top-0 z-(--ds-z-raised) flex gap-1", children: [
|
|
72
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-xs", "aria-label": t.chartExportSvg, title: t.chartExportSvg, onClick: () => download("svg"), children: /* @__PURE__ */ jsx(DownloadIcon, {}) }),
|
|
73
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "xs", "aria-label": t.chartExportPng, title: t.chartExportPng, onClick: () => download("png"), children: "PNG" })
|
|
74
|
+
] }),
|
|
75
|
+
empty ? /* @__PURE__ */ jsx(
|
|
76
|
+
"div",
|
|
77
|
+
{
|
|
78
|
+
"data-slot": "echart-empty",
|
|
79
|
+
className: "flex items-center justify-center rounded-card border border-dashed border-border text-sm text-muted-foreground",
|
|
80
|
+
style: { height },
|
|
81
|
+
children: t.chartNoData
|
|
82
|
+
}
|
|
83
|
+
) : /* @__PURE__ */ jsx("div", { ref: box, "data-slot": "echart-canvas", className: "w-full", style: { height } })
|
|
84
|
+
] });
|
|
85
|
+
});
|
|
86
|
+
export {
|
|
87
|
+
EChart
|
|
88
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useRender } from '@base-ui/react/use-render';
|
|
3
|
+
|
|
4
|
+
declare const eyebrowClassName = "text-xs leading-tight font-(--ds-label-weight) tracking-(--ds-label-tracking) text-muted-foreground uppercase";
|
|
5
|
+
declare function Eyebrow({ className, render, ...props }: useRender.ComponentProps<"p">): React.ReactElement<unknown, string | React.JSXElementConstructor<any>>;
|
|
6
|
+
|
|
7
|
+
export { Eyebrow, eyebrowClassName };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
const eyebrowClassName = "text-xs leading-tight font-(--ds-label-weight) tracking-(--ds-label-tracking) text-muted-foreground uppercase";
|
|
5
|
+
function Eyebrow({ className, render, ...props }) {
|
|
6
|
+
return useRender({
|
|
7
|
+
defaultTagName: "p",
|
|
8
|
+
props: mergeProps({ className: cn(eyebrowClassName, className) }, props),
|
|
9
|
+
render,
|
|
10
|
+
state: { slot: "eyebrow" }
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
Eyebrow,
|
|
15
|
+
eyebrowClassName
|
|
16
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
declare function FilterBar({ className, children, actions, onClear, canClear, ...props }: React.ComponentProps<"div"> & {
|
|
4
|
+
/** Acciones de la vista (exportar, guardar vista…), al final de la fila. */
|
|
5
|
+
actions?: React.ReactNode;
|
|
6
|
+
/** Si se pasa, muestra «Quitar filtros». */
|
|
7
|
+
onClear?: () => void;
|
|
8
|
+
/** Hay algo que quitar: si es false, «Quitar filtros» se oculta. */
|
|
9
|
+
canClear?: boolean;
|
|
10
|
+
}): React.JSX.Element;
|
|
11
|
+
type FilterOption = {
|
|
12
|
+
value: string;
|
|
13
|
+
label: React.ReactNode;
|
|
14
|
+
icon?: React.ReactNode;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
};
|
|
17
|
+
declare function FilterMenu({ className, label, icon, value, onValueChange, options, allOption, disabled, disabledReason, size, align, id, }: {
|
|
18
|
+
className?: string;
|
|
19
|
+
/** Nombre del filtro («Campaña»): va siempre en el disparador. */
|
|
20
|
+
label: React.ReactNode;
|
|
21
|
+
icon?: React.ReactNode;
|
|
22
|
+
/** Valor elegido; `null`/`undefined` = sin filtro («Todos»). */
|
|
23
|
+
value?: string | null;
|
|
24
|
+
onValueChange?: (value: string | null) => void;
|
|
25
|
+
options: FilterOption[];
|
|
26
|
+
/** Añade la opción «Todos» (o con este texto) que quita el filtro. */
|
|
27
|
+
allOption?: boolean | string;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
/** Desactiva el filtro y explica el motivo (Tooltip + texto accesible). */
|
|
30
|
+
disabledReason?: React.ReactNode;
|
|
31
|
+
size?: "sm" | "default";
|
|
32
|
+
align?: "start" | "center" | "end";
|
|
33
|
+
id?: string;
|
|
34
|
+
}): React.JSX.Element;
|
|
35
|
+
|
|
36
|
+
export { FilterBar, FilterMenu, type FilterOption };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronDownIcon, XIcon } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { useDsMessages } from "../hooks/use-ds-messages";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
import { Button } from "./button";
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuRadioGroup,
|
|
12
|
+
DropdownMenuRadioItem,
|
|
13
|
+
DropdownMenuSeparator,
|
|
14
|
+
DropdownMenuTrigger
|
|
15
|
+
} from "./dropdown-menu";
|
|
16
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip";
|
|
17
|
+
function FilterBar({
|
|
18
|
+
className,
|
|
19
|
+
children,
|
|
20
|
+
actions,
|
|
21
|
+
onClear,
|
|
22
|
+
canClear = true,
|
|
23
|
+
...props
|
|
24
|
+
}) {
|
|
25
|
+
const t = useDsMessages();
|
|
26
|
+
return /* @__PURE__ */ jsxs(
|
|
27
|
+
"div",
|
|
28
|
+
{
|
|
29
|
+
"data-slot": "filter-bar",
|
|
30
|
+
className: cn(
|
|
31
|
+
"flex w-full flex-wrap items-center gap-(--ds-filter-gap)",
|
|
32
|
+
className
|
|
33
|
+
),
|
|
34
|
+
...props,
|
|
35
|
+
children: [
|
|
36
|
+
/* @__PURE__ */ jsxs(
|
|
37
|
+
"div",
|
|
38
|
+
{
|
|
39
|
+
"data-slot": "filter-bar-filters",
|
|
40
|
+
className: "flex min-w-0 flex-wrap items-center gap-(--ds-filter-gap)",
|
|
41
|
+
children: [
|
|
42
|
+
children,
|
|
43
|
+
onClear && canClear ? /* @__PURE__ */ jsxs(
|
|
44
|
+
Button,
|
|
45
|
+
{
|
|
46
|
+
"data-slot": "filter-bar-clear",
|
|
47
|
+
variant: "ghost",
|
|
48
|
+
size: "sm",
|
|
49
|
+
onClick: onClear,
|
|
50
|
+
children: [
|
|
51
|
+
/* @__PURE__ */ jsx(XIcon, { "data-icon": "inline-start" }),
|
|
52
|
+
t.filterClear
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
) : null
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
),
|
|
59
|
+
actions ? /* @__PURE__ */ jsx(
|
|
60
|
+
"div",
|
|
61
|
+
{
|
|
62
|
+
"data-slot": "filter-bar-actions",
|
|
63
|
+
className: "ms-auto flex flex-wrap items-center gap-(--ds-filter-gap)",
|
|
64
|
+
children: actions
|
|
65
|
+
}
|
|
66
|
+
) : null
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const ALL = "__all__";
|
|
72
|
+
function FilterMenu({
|
|
73
|
+
className,
|
|
74
|
+
label,
|
|
75
|
+
icon,
|
|
76
|
+
value,
|
|
77
|
+
onValueChange,
|
|
78
|
+
options,
|
|
79
|
+
allOption = false,
|
|
80
|
+
disabled = false,
|
|
81
|
+
disabledReason,
|
|
82
|
+
size = "sm",
|
|
83
|
+
align = "start",
|
|
84
|
+
id
|
|
85
|
+
}) {
|
|
86
|
+
const t = useDsMessages();
|
|
87
|
+
const reasonId = React.useId();
|
|
88
|
+
const selected = options.find((o) => o.value === value);
|
|
89
|
+
const allLabel = typeof allOption === "string" ? allOption : t.filterAll;
|
|
90
|
+
const isDisabled = disabled || !!disabledReason;
|
|
91
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
92
|
+
icon,
|
|
93
|
+
/* @__PURE__ */ jsx("span", { "data-slot": "filter-menu-label", className: "text-muted-foreground", children: label }),
|
|
94
|
+
selected || allOption ? /* @__PURE__ */ jsx(
|
|
95
|
+
"span",
|
|
96
|
+
{
|
|
97
|
+
"data-slot": "filter-menu-value",
|
|
98
|
+
className: "max-w-40 truncate font-(--ds-label-weight) text-foreground",
|
|
99
|
+
children: selected ? selected.label : allLabel
|
|
100
|
+
}
|
|
101
|
+
) : null,
|
|
102
|
+
/* @__PURE__ */ jsx(ChevronDownIcon, { "data-icon": "inline-end", className: "text-muted-foreground" })
|
|
103
|
+
] });
|
|
104
|
+
const triggerClass = cn(
|
|
105
|
+
"gap-1.5 aria-disabled:cursor-not-allowed aria-disabled:opacity-(--ds-disabled-opacity) data-active:border-primary",
|
|
106
|
+
className
|
|
107
|
+
);
|
|
108
|
+
if (disabledReason) {
|
|
109
|
+
return /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
110
|
+
/* @__PURE__ */ jsxs(
|
|
111
|
+
TooltipTrigger,
|
|
112
|
+
{
|
|
113
|
+
render: /* @__PURE__ */ jsx(
|
|
114
|
+
Button,
|
|
115
|
+
{
|
|
116
|
+
id,
|
|
117
|
+
variant: "outline",
|
|
118
|
+
size,
|
|
119
|
+
"data-slot": "filter-menu-trigger",
|
|
120
|
+
"data-disabled": "",
|
|
121
|
+
"aria-disabled": "true",
|
|
122
|
+
"aria-describedby": reasonId,
|
|
123
|
+
className: triggerClass,
|
|
124
|
+
onClick: (e) => e.preventDefault()
|
|
125
|
+
}
|
|
126
|
+
),
|
|
127
|
+
children: [
|
|
128
|
+
content,
|
|
129
|
+
/* @__PURE__ */ jsx("span", { id: reasonId, className: "sr-only", children: disabledReason })
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
),
|
|
133
|
+
/* @__PURE__ */ jsx(TooltipContent, { "aria-hidden": "true", children: disabledReason })
|
|
134
|
+
] });
|
|
135
|
+
}
|
|
136
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
137
|
+
/* @__PURE__ */ jsx(
|
|
138
|
+
DropdownMenuTrigger,
|
|
139
|
+
{
|
|
140
|
+
disabled: isDisabled,
|
|
141
|
+
render: /* @__PURE__ */ jsx(
|
|
142
|
+
Button,
|
|
143
|
+
{
|
|
144
|
+
id,
|
|
145
|
+
variant: "outline",
|
|
146
|
+
size,
|
|
147
|
+
"data-slot": "filter-menu-trigger",
|
|
148
|
+
"data-active": selected ? "" : void 0,
|
|
149
|
+
className: triggerClass
|
|
150
|
+
}
|
|
151
|
+
),
|
|
152
|
+
children: content
|
|
153
|
+
}
|
|
154
|
+
),
|
|
155
|
+
/* @__PURE__ */ jsx(
|
|
156
|
+
DropdownMenuContent,
|
|
157
|
+
{
|
|
158
|
+
"data-slot": "filter-menu-content",
|
|
159
|
+
align,
|
|
160
|
+
className: "w-auto min-w-48",
|
|
161
|
+
children: /* @__PURE__ */ jsxs(
|
|
162
|
+
DropdownMenuRadioGroup,
|
|
163
|
+
{
|
|
164
|
+
value: value ?? ALL,
|
|
165
|
+
onValueChange: (v) => onValueChange?.(v === ALL ? null : v),
|
|
166
|
+
children: [
|
|
167
|
+
allOption ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
168
|
+
/* @__PURE__ */ jsx(DropdownMenuRadioItem, { value: ALL, children: allLabel }),
|
|
169
|
+
/* @__PURE__ */ jsx(DropdownMenuSeparator, {})
|
|
170
|
+
] }) : null,
|
|
171
|
+
options.map((o) => /* @__PURE__ */ jsxs(
|
|
172
|
+
DropdownMenuRadioItem,
|
|
173
|
+
{
|
|
174
|
+
value: o.value,
|
|
175
|
+
disabled: o.disabled,
|
|
176
|
+
children: [
|
|
177
|
+
o.icon,
|
|
178
|
+
o.label
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
o.value
|
|
182
|
+
))
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
] });
|
|
189
|
+
}
|
|
190
|
+
export {
|
|
191
|
+
FilterBar,
|
|
192
|
+
FilterMenu
|
|
193
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { DsDataTone } from './legend.js';
|
|
3
|
+
|
|
4
|
+
interface HeatmapCell {
|
|
5
|
+
row: number;
|
|
6
|
+
column: number;
|
|
7
|
+
rowLabel: string;
|
|
8
|
+
columnLabel: string;
|
|
9
|
+
value: number | null;
|
|
10
|
+
/** Valor ya formateado (o «Sin dato»). */
|
|
11
|
+
formatted: string;
|
|
12
|
+
}
|
|
13
|
+
declare function HeatmapGrid({ className, rows, columns, values, min: minProp, max: maxProp, tone, caption, formatValue, cellLabel, renderColumnHeader, renderCard, onCellClick, onCellFocus, showScale, style, ...props }: Omit<React.ComponentProps<"div">, "children"> & {
|
|
14
|
+
/** Etiquetas de las filas (p. ej. días). */
|
|
15
|
+
rows: string[];
|
|
16
|
+
/** Etiquetas de las columnas (p. ej. horas). */
|
|
17
|
+
columns: string[];
|
|
18
|
+
/** Matriz [fila][columna]; `null` o ausente = sin dato. */
|
|
19
|
+
values: Array<Array<number | null | undefined>>;
|
|
20
|
+
min?: number;
|
|
21
|
+
max?: number;
|
|
22
|
+
/** Tono del extremo alto en lugar de --ds-heatmap-high. */
|
|
23
|
+
tone?: DsDataTone;
|
|
24
|
+
/** Título de la tabla (visible). Si no, pasa `aria-label`. */
|
|
25
|
+
caption?: React.ReactNode;
|
|
26
|
+
formatValue?: (value: number) => string;
|
|
27
|
+
/** Texto accesible propio de cada celda. */
|
|
28
|
+
cellLabel?: (cell: HeatmapCell) => string;
|
|
29
|
+
/** Contenido de la cabecera de columna (p. ej. mostrar solo cada 3 horas). */
|
|
30
|
+
renderColumnHeader?: (label: string, index: number) => React.ReactNode;
|
|
31
|
+
/** Tarjeta flotante de una celda (con ratón y foco). */
|
|
32
|
+
renderCard?: (cell: HeatmapCell) => React.ReactNode;
|
|
33
|
+
onCellClick?: (cell: HeatmapCell) => void;
|
|
34
|
+
onCellFocus?: (cell: HeatmapCell) => void;
|
|
35
|
+
/** Escala bajo/alto bajo la tabla. */
|
|
36
|
+
showScale?: boolean;
|
|
37
|
+
}): React.JSX.Element;
|
|
38
|
+
|
|
39
|
+
export { type HeatmapCell, HeatmapGrid };
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useDsLocale, useDsMessages } from "../hooks/use-ds-messages";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
import { dataToneVar } from "./legend";
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
|
|
8
|
+
const HATCH = "var(--ds-hatch)";
|
|
9
|
+
function HeatmapGrid({
|
|
10
|
+
className,
|
|
11
|
+
rows,
|
|
12
|
+
columns,
|
|
13
|
+
values,
|
|
14
|
+
min: minProp,
|
|
15
|
+
max: maxProp,
|
|
16
|
+
tone,
|
|
17
|
+
caption,
|
|
18
|
+
formatValue,
|
|
19
|
+
cellLabel,
|
|
20
|
+
renderColumnHeader,
|
|
21
|
+
renderCard,
|
|
22
|
+
onCellClick,
|
|
23
|
+
onCellFocus,
|
|
24
|
+
showScale = true,
|
|
25
|
+
style,
|
|
26
|
+
...props
|
|
27
|
+
}) {
|
|
28
|
+
const t = useDsMessages();
|
|
29
|
+
const locale = useDsLocale();
|
|
30
|
+
const number = React.useMemo(() => new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }), [locale]);
|
|
31
|
+
const format = React.useCallback((v) => formatValue ? formatValue(v) : number.format(v), [formatValue, number]);
|
|
32
|
+
const { min, max, hasEmpty } = React.useMemo(() => {
|
|
33
|
+
let lo = Infinity;
|
|
34
|
+
let hi = -Infinity;
|
|
35
|
+
let empty = false;
|
|
36
|
+
for (let r = 0; r < rows.length; r++)
|
|
37
|
+
for (let c = 0; c < columns.length; c++) {
|
|
38
|
+
const v = values[r]?.[c];
|
|
39
|
+
if (v == null || !Number.isFinite(v)) empty = true;
|
|
40
|
+
else {
|
|
41
|
+
lo = Math.min(lo, v);
|
|
42
|
+
hi = Math.max(hi, v);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { min: minProp ?? (lo === Infinity ? 0 : lo), max: maxProp ?? (hi === -Infinity ? 1 : hi), hasEmpty: empty };
|
|
46
|
+
}, [values, rows.length, columns.length, minProp, maxProp]);
|
|
47
|
+
const cellAt = React.useCallback(
|
|
48
|
+
(r, c) => {
|
|
49
|
+
const raw = values[r]?.[c];
|
|
50
|
+
const value = raw == null || !Number.isFinite(raw) ? null : raw;
|
|
51
|
+
return { row: r, column: c, rowLabel: rows[r], columnLabel: columns[c], value, formatted: value == null ? t.statNoData : format(value) };
|
|
52
|
+
},
|
|
53
|
+
[values, rows, columns, format, t.statNoData]
|
|
54
|
+
);
|
|
55
|
+
const [active, setActive] = React.useState({ r: 0, c: 0 });
|
|
56
|
+
const cells = React.useRef([]);
|
|
57
|
+
const nCols = columns.length;
|
|
58
|
+
const moveTo = (r, c) => {
|
|
59
|
+
const nr = Math.min(Math.max(r, 0), rows.length - 1);
|
|
60
|
+
const nc = Math.min(Math.max(c, 0), nCols - 1);
|
|
61
|
+
setActive({ r: nr, c: nc });
|
|
62
|
+
cells.current[nr * nCols + nc]?.focus();
|
|
63
|
+
};
|
|
64
|
+
const onKeyDown = (e, r, c) => {
|
|
65
|
+
const rtl = getComputedStyle(e.currentTarget).direction === "rtl";
|
|
66
|
+
const fwd = rtl ? -1 : 1;
|
|
67
|
+
switch (e.key) {
|
|
68
|
+
case "ArrowRight":
|
|
69
|
+
moveTo(r, c + fwd);
|
|
70
|
+
break;
|
|
71
|
+
case "ArrowLeft":
|
|
72
|
+
moveTo(r, c - fwd);
|
|
73
|
+
break;
|
|
74
|
+
case "ArrowDown":
|
|
75
|
+
moveTo(r + 1, c);
|
|
76
|
+
break;
|
|
77
|
+
case "ArrowUp":
|
|
78
|
+
moveTo(r - 1, c);
|
|
79
|
+
break;
|
|
80
|
+
case "Home":
|
|
81
|
+
moveTo(e.ctrlKey ? 0 : r, 0);
|
|
82
|
+
break;
|
|
83
|
+
case "End":
|
|
84
|
+
moveTo(e.ctrlKey ? rows.length - 1 : r, nCols - 1);
|
|
85
|
+
break;
|
|
86
|
+
case "Enter":
|
|
87
|
+
case " ":
|
|
88
|
+
onCellClick?.(cellAt(r, c));
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
};
|
|
95
|
+
const span = max - min || 1;
|
|
96
|
+
const highStyle = tone ? { "--ds-heatmap-high": dataToneVar[tone] } : void 0;
|
|
97
|
+
const renderCell = (r, c) => {
|
|
98
|
+
const cell = cellAt(r, c);
|
|
99
|
+
const ratio = cell.value == null ? 0 : Math.min(Math.max((cell.value - min) / span, 0), 1);
|
|
100
|
+
const level = cell.value == null ? void 0 : Math.round(ratio * 4);
|
|
101
|
+
const isActive = active.r === r && active.c === c;
|
|
102
|
+
const tdProps = {
|
|
103
|
+
role: "gridcell",
|
|
104
|
+
tabIndex: isActive ? 0 : -1,
|
|
105
|
+
"aria-label": cellLabel ? cellLabel(cell) : `${cell.rowLabel}, ${cell.columnLabel}: ${cell.formatted}`,
|
|
106
|
+
"data-slot": "heatmap-grid-cell",
|
|
107
|
+
"data-state": cell.value == null ? "empty" : "filled",
|
|
108
|
+
"data-level": level,
|
|
109
|
+
className: cn(
|
|
110
|
+
"size-(--ds-heatmap-cell) min-w-(--ds-heatmap-cell) rounded-xs p-0 outline-hidden transition-shadow duration-(--ds-motion-fast) motion-reduce:transition-none",
|
|
111
|
+
"focus-visible:relative focus-visible:z-(--ds-z-raised) focus-visible:ring-(length:--ds-ring-width) focus-visible:ring-(--ds-ring-color)",
|
|
112
|
+
cell.value == null ? "bg-muted" : "",
|
|
113
|
+
onCellClick && "cursor-pointer hover:ring-(length:--ds-rule-w) hover:ring-ring"
|
|
114
|
+
),
|
|
115
|
+
style: cell.value == null ? { backgroundImage: HATCH } : { backgroundColor: `color-mix(in oklch, var(--ds-heatmap-high) ${Math.round(ratio * 100)}%, var(--ds-heatmap-low))` },
|
|
116
|
+
onClick: onCellClick ? () => onCellClick(cell) : void 0,
|
|
117
|
+
onFocus: () => {
|
|
118
|
+
if (!isActive) setActive({ r, c });
|
|
119
|
+
onCellFocus?.(cell);
|
|
120
|
+
},
|
|
121
|
+
onKeyDown: (e) => onKeyDown(e, r, c)
|
|
122
|
+
};
|
|
123
|
+
const ref = (el) => {
|
|
124
|
+
cells.current[r * nCols + c] = el;
|
|
125
|
+
};
|
|
126
|
+
if (!renderCard) return /* @__PURE__ */ jsx("td", { ref, ...tdProps }, c);
|
|
127
|
+
return /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
128
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { render: /* @__PURE__ */ jsx("td", { ref, ...tdProps }) }),
|
|
129
|
+
/* @__PURE__ */ jsx(TooltipContent, { "data-slot": "heatmap-grid-card", className: "max-w-xs flex-col items-start", children: renderCard(cell) })
|
|
130
|
+
] }, c);
|
|
131
|
+
};
|
|
132
|
+
const table = /* @__PURE__ */ jsxs("table", { role: "grid", "data-slot": "heatmap-grid-table", className: "border-separate border-spacing-0.5 text-xs", children: [
|
|
133
|
+
caption && /* @__PURE__ */ jsx("caption", { "data-slot": "heatmap-grid-caption", className: "pb-2 text-start text-sm font-medium text-foreground", children: caption }),
|
|
134
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
135
|
+
/* @__PURE__ */ jsx("td", { "aria-hidden": true }),
|
|
136
|
+
columns.map((col, c) => /* @__PURE__ */ jsx(
|
|
137
|
+
"th",
|
|
138
|
+
{
|
|
139
|
+
scope: "col",
|
|
140
|
+
"data-slot": "heatmap-grid-column-header",
|
|
141
|
+
"aria-label": col,
|
|
142
|
+
className: "pb-1 text-center font-normal text-muted-foreground tabular-nums",
|
|
143
|
+
children: renderColumnHeader ? renderColumnHeader(col, c) : col
|
|
144
|
+
},
|
|
145
|
+
c
|
|
146
|
+
))
|
|
147
|
+
] }) }),
|
|
148
|
+
/* @__PURE__ */ jsx("tbody", { children: rows.map((row, r) => /* @__PURE__ */ jsxs("tr", { "data-slot": "heatmap-grid-row", children: [
|
|
149
|
+
/* @__PURE__ */ jsx("th", { scope: "row", "data-slot": "heatmap-grid-row-header", className: "pe-2 text-start font-normal whitespace-nowrap text-muted-foreground", children: row }),
|
|
150
|
+
columns.map((_, c) => renderCell(r, c))
|
|
151
|
+
] }, r)) })
|
|
152
|
+
] });
|
|
153
|
+
return /* @__PURE__ */ jsxs(
|
|
154
|
+
"div",
|
|
155
|
+
{
|
|
156
|
+
"data-slot": "heatmap-grid",
|
|
157
|
+
"data-tone": tone,
|
|
158
|
+
className: cn("flex min-w-0 flex-col gap-3", className),
|
|
159
|
+
style: { ...highStyle, ...style },
|
|
160
|
+
...props,
|
|
161
|
+
children: [
|
|
162
|
+
/* @__PURE__ */ jsx("div", { "data-slot": "heatmap-grid-scroll", className: "min-w-0 overflow-x-auto", children: renderCard ? /* @__PURE__ */ jsx(TooltipProvider, { children: table }) : table }),
|
|
163
|
+
showScale && /* @__PURE__ */ jsxs("div", { "data-slot": "heatmap-grid-scale", className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [
|
|
164
|
+
/* @__PURE__ */ jsx("span", { children: t.heatmapLow }),
|
|
165
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums", children: format(min) }),
|
|
166
|
+
/* @__PURE__ */ jsx(
|
|
167
|
+
"span",
|
|
168
|
+
{
|
|
169
|
+
"aria-hidden": true,
|
|
170
|
+
"data-slot": "heatmap-grid-scale-bar",
|
|
171
|
+
className: "h-(--ds-legend-swatch) w-24 rounded-xs bg-linear-to-r from-(--ds-heatmap-low) to-(--ds-heatmap-high) ring-(length:--ds-rule-w) ring-border rtl:bg-linear-to-l"
|
|
172
|
+
}
|
|
173
|
+
),
|
|
174
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums", children: format(max) }),
|
|
175
|
+
/* @__PURE__ */ jsx("span", { children: t.heatmapHigh }),
|
|
176
|
+
hasEmpty && /* @__PURE__ */ jsxs("span", { className: "ms-2 inline-flex items-center gap-1.5", children: [
|
|
177
|
+
/* @__PURE__ */ jsx(
|
|
178
|
+
"span",
|
|
179
|
+
{
|
|
180
|
+
"aria-hidden": true,
|
|
181
|
+
"data-slot": "heatmap-grid-scale-empty",
|
|
182
|
+
className: "size-(--ds-legend-swatch) rounded-xs bg-muted",
|
|
183
|
+
style: { backgroundImage: HATCH }
|
|
184
|
+
}
|
|
185
|
+
),
|
|
186
|
+
t.statNoData
|
|
187
|
+
] })
|
|
188
|
+
] })
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
export {
|
|
194
|
+
HeatmapGrid
|
|
195
|
+
};
|
package/dist/ui/i18n-provider.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { DirectionProvider } from "@base-ui/react/direction-provider";
|
|
5
|
-
import { DsMessagesContext } from "../hooks/use-ds-messages";
|
|
6
|
-
import { dsDirection, dsMessagesFor } from "../lib/i18n";
|
|
5
|
+
import { DsLocaleContext, DsMessagesContext } from "../hooks/use-ds-messages";
|
|
6
|
+
import { DS_DEFAULT_LOCALE, dsDirection, dsMessagesFor } from "../lib/i18n";
|
|
7
7
|
function DsI18nProvider({
|
|
8
8
|
locale,
|
|
9
9
|
messages,
|
|
@@ -11,7 +11,7 @@ function DsI18nProvider({
|
|
|
11
11
|
children
|
|
12
12
|
}) {
|
|
13
13
|
const value = React.useMemo(() => dsMessagesFor(locale, messages), [locale, messages]);
|
|
14
|
-
return /* @__PURE__ */ jsx(DirectionProvider, { direction: dir ?? dsDirection(locale), children: /* @__PURE__ */ jsx(DsMessagesContext.Provider, { value, children }) });
|
|
14
|
+
return /* @__PURE__ */ jsx(DirectionProvider, { direction: dir ?? dsDirection(locale), children: /* @__PURE__ */ jsx(DsLocaleContext.Provider, { value: locale || DS_DEFAULT_LOCALE, children: /* @__PURE__ */ jsx(DsMessagesContext.Provider, { value, children }) }) });
|
|
15
15
|
}
|
|
16
16
|
export {
|
|
17
17
|
DsI18nProvider
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/** Tonos de color comunes a las piezas de datos (series del gráfico y estados). */
|
|
4
|
+
type DsDataTone = "primary" | "info" | "success" | "warning" | "danger" | "neutral" | "chart-1" | "chart-2" | "chart-3" | "chart-4" | "chart-5";
|
|
5
|
+
/** Relleno de fondo de cada tono (clases estáticas para que Tailwind las genere). */
|
|
6
|
+
declare const dataToneBg: Record<DsDataTone, string>;
|
|
7
|
+
/** Variable CSS del color de cada tono (para mezclas con color-mix). */
|
|
8
|
+
declare const dataToneVar: Record<DsDataTone, string>;
|
|
9
|
+
declare function Legend({ className, orientation, ...props }: React.ComponentProps<"ul"> & {
|
|
10
|
+
orientation?: "horizontal" | "vertical";
|
|
11
|
+
}): React.JSX.Element;
|
|
12
|
+
declare function LegendSwatch({ className, tone, color, shape, style, ...props }: React.ComponentProps<"span"> & {
|
|
13
|
+
tone?: DsDataTone;
|
|
14
|
+
/** Color propio (cualquier valor CSS, mejor una variable). Gana a `tone`. */
|
|
15
|
+
color?: string;
|
|
16
|
+
shape?: "square" | "circle" | "line";
|
|
17
|
+
}): React.JSX.Element;
|
|
18
|
+
declare function LegendItem({ className, tone, color, shape, value, active, onToggle, children, ...props }: Omit<React.ComponentProps<"li">, "onToggle" | "color"> & {
|
|
19
|
+
tone?: DsDataTone;
|
|
20
|
+
color?: string;
|
|
21
|
+
shape?: "square" | "circle" | "line";
|
|
22
|
+
/** Valor opcional, tras la etiqueta (ya formateado por la app). */
|
|
23
|
+
value?: React.ReactNode;
|
|
24
|
+
/** Serie visible (en una leyenda interactiva). */
|
|
25
|
+
active?: boolean;
|
|
26
|
+
/** Hace el elemento interactivo: recibe el nuevo estado. */
|
|
27
|
+
onToggle?: (active: boolean) => void;
|
|
28
|
+
}): React.JSX.Element;
|
|
29
|
+
|
|
30
|
+
export { type DsDataTone, Legend, LegendItem, LegendSwatch, dataToneBg, dataToneVar };
|