doom-design-system 0.4.9 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Chart/Chart.js +28 -17
- package/dist/components/Chart/renderers.d.ts +32 -5
- package/dist/components/Chart/renderers.js +169 -55
- package/dist/components/Image/Image.js +1 -1
- package/dist/components/Layout/Layout.d.ts +13 -8
- package/dist/components/Layout/Layout.js +6 -4
- package/dist/components/Table/Table.js +2 -1
- package/dist/styles/globals.css +21 -5
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -9,13 +9,19 @@ import { Text } from "../Text/Text.js";
|
|
|
9
9
|
import styles from "./Chart.module.css";
|
|
10
10
|
import { createScales, drawAxes, drawBars, drawGrid, drawLineArea, setupGradient, } from "./renderers.js";
|
|
11
11
|
export function Chart({ data, type = "line", variant = "default", x, y, d3Config, className, style, renderTooltip, flat, title, render, withFrame = true, onValueChange, }) {
|
|
12
|
+
const TOOLTIP_OFFSET = 20;
|
|
13
|
+
const HIDE_DELAY_MS = 16;
|
|
14
|
+
const MOBILE_BREAKPOINT = 480;
|
|
12
15
|
const svgRef = useRef(null);
|
|
13
16
|
const containerRef = useRef(null);
|
|
17
|
+
const wrapperRef = useRef(null);
|
|
14
18
|
const tooltipRef = useRef(null);
|
|
15
19
|
const tooltipPosRef = useRef({ x: 0, y: 0 });
|
|
16
20
|
const hideTimeoutRef = useRef(null);
|
|
17
21
|
const gradientId = React.useId().replace(/:/g, "");
|
|
18
22
|
const [activeData, setActiveData] = useState(null);
|
|
23
|
+
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
|
24
|
+
const isMobile = dimensions.width > 0 && dimensions.width < MOBILE_BREAKPOINT;
|
|
19
25
|
useEffect(() => {
|
|
20
26
|
onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(activeData !== null && activeData !== void 0 ? activeData : null);
|
|
21
27
|
}, [activeData, onValueChange]);
|
|
@@ -23,19 +29,19 @@ export function Chart({ data, type = "line", variant = "default", x, y, d3Config
|
|
|
23
29
|
top: 20,
|
|
24
30
|
right: 20,
|
|
25
31
|
bottom: (d3Config === null || d3Config === void 0 ? void 0 : d3Config.xAxisLabel) ? 60 : 50,
|
|
26
|
-
left: (d3Config === null || d3Config === void 0 ? void 0 : d3Config.yAxisLabel) ?
|
|
32
|
+
left: (d3Config === null || d3Config === void 0 ? void 0 : d3Config.yAxisLabel) ? 85 : 55,
|
|
27
33
|
}, curve: undefined, showAxes: true, grid: false, withGradient: type === "area", showDots: false }, d3Config)), [d3Config, type]);
|
|
28
|
-
const wrapperRef = useRef(null);
|
|
29
|
-
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
|
30
|
-
const isMobile = dimensions.width > 0 && dimensions.width < 480;
|
|
31
34
|
React.useLayoutEffect(() => {
|
|
32
35
|
if (activeData && tooltipRef.current && wrapperRef.current) {
|
|
33
36
|
const { x, y } = tooltipPosRef.current;
|
|
34
37
|
const rect = wrapperRef.current.getBoundingClientRect();
|
|
38
|
+
const tooltipWidth = tooltipRef.current.offsetWidth;
|
|
35
39
|
const absX = rect.left + x;
|
|
36
|
-
const isRightEdge = absX > window.innerWidth -
|
|
37
|
-
const
|
|
38
|
-
|
|
40
|
+
const isRightEdge = absX + tooltipWidth + TOOLTIP_OFFSET > window.innerWidth - 10;
|
|
41
|
+
const xOffset = isRightEdge
|
|
42
|
+
? -TOOLTIP_OFFSET - tooltipWidth
|
|
43
|
+
: TOOLTIP_OFFSET;
|
|
44
|
+
tooltipRef.current.style.transform = `translate3d(${x + xOffset}px, calc(${y}px - 50%), 0)`;
|
|
39
45
|
}
|
|
40
46
|
}, [activeData]);
|
|
41
47
|
useEffect(() => {
|
|
@@ -46,8 +52,9 @@ export function Chart({ data, type = "line", variant = "default", x, y, d3Config
|
|
|
46
52
|
};
|
|
47
53
|
}, []);
|
|
48
54
|
useEffect(() => {
|
|
49
|
-
if (!wrapperRef.current)
|
|
55
|
+
if (!wrapperRef.current) {
|
|
50
56
|
return;
|
|
57
|
+
}
|
|
51
58
|
const resizeObserver = new ResizeObserver((entries) => {
|
|
52
59
|
const entry = entries[0];
|
|
53
60
|
if (entry) {
|
|
@@ -72,9 +79,10 @@ export function Chart({ data, type = "line", variant = "default", x, y, d3Config
|
|
|
72
79
|
? (d3Config === null || d3Config === void 0 ? void 0 : d3Config.xAxisLabel)
|
|
73
80
|
? 50
|
|
74
81
|
: 30
|
|
75
|
-
: config.margin.bottom, left: isMobile ? ((d3Config === null || d3Config === void 0 ? void 0 : d3Config.yAxisLabel) ?
|
|
76
|
-
if (width <= 0 || height <= 0)
|
|
82
|
+
: config.margin.bottom, left: isMobile ? ((d3Config === null || d3Config === void 0 ? void 0 : d3Config.yAxisLabel) ? 65 : 30) : config.margin.left });
|
|
83
|
+
if (width <= 0 || height <= 0) {
|
|
77
84
|
return;
|
|
85
|
+
}
|
|
78
86
|
const svg = select(svgRef.current);
|
|
79
87
|
svg.selectAll("*").remove();
|
|
80
88
|
const g = svg
|
|
@@ -86,8 +94,9 @@ export function Chart({ data, type = "line", variant = "default", x, y, d3Config
|
|
|
86
94
|
setupGradient(svg, gradientId);
|
|
87
95
|
}
|
|
88
96
|
const { xScale, yScale, innerWidth, innerHeight } = createScales(data, width, height, margin, x, y, type);
|
|
89
|
-
if (innerWidth <= 0 || innerHeight <= 0)
|
|
97
|
+
if (innerWidth <= 0 || innerHeight <= 0) {
|
|
90
98
|
return;
|
|
99
|
+
}
|
|
91
100
|
if (config.grid) {
|
|
92
101
|
drawGrid(g, yScale, innerWidth, styles.grid);
|
|
93
102
|
}
|
|
@@ -102,10 +111,13 @@ export function Chart({ data, type = "line", variant = "default", x, y, d3Config
|
|
|
102
111
|
tooltipPosRef.current = { x: tx, y: ty };
|
|
103
112
|
if (tooltipRef.current && wrapperRef.current) {
|
|
104
113
|
const rect = wrapperRef.current.getBoundingClientRect();
|
|
114
|
+
const tooltipWidth = tooltipRef.current.offsetWidth;
|
|
105
115
|
const absX = rect.left + tx;
|
|
106
|
-
const isRightEdge = absX > window.innerWidth -
|
|
107
|
-
const
|
|
108
|
-
|
|
116
|
+
const isRightEdge = absX + tooltipWidth + TOOLTIP_OFFSET > window.innerWidth - 10;
|
|
117
|
+
const xOffset = isRightEdge
|
|
118
|
+
? -TOOLTIP_OFFSET - tooltipWidth
|
|
119
|
+
: TOOLTIP_OFFSET;
|
|
120
|
+
tooltipRef.current.style.transform = `translate3d(${tx + xOffset}px, calc(${ty}px - 50%), 0)`;
|
|
109
121
|
}
|
|
110
122
|
setActiveData((prev) => (prev === data ? prev : data));
|
|
111
123
|
};
|
|
@@ -113,13 +125,12 @@ export function Chart({ data, type = "line", variant = "default", x, y, d3Config
|
|
|
113
125
|
const [px, py] = select(svgRef.current).select("g").empty()
|
|
114
126
|
? [0, 0]
|
|
115
127
|
: pointer(event, g.node());
|
|
116
|
-
const tx = px + margin.left
|
|
128
|
+
const tx = px + margin.left;
|
|
117
129
|
const ty = py + margin.top;
|
|
118
130
|
updateTooltip(tx, ty, data);
|
|
119
131
|
};
|
|
120
132
|
const hideTooltip = () => {
|
|
121
|
-
|
|
122
|
-
hideTimeoutRef.current = setTimeout(() => setActiveData(null), 16);
|
|
133
|
+
hideTimeoutRef.current = setTimeout(() => setActiveData(null), HIDE_DELAY_MS);
|
|
123
134
|
};
|
|
124
135
|
const ctx = {
|
|
125
136
|
g,
|
|
@@ -1,23 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart Renderers
|
|
3
|
+
*
|
|
4
|
+
* D3-based rendering functions for charts. Each renderer handles
|
|
5
|
+
* drawing a specific chart type (line, area, bar) and managing
|
|
6
|
+
* user interactions (hover, touch).
|
|
7
|
+
*/
|
|
1
8
|
import * as d3Scale from "d3-scale";
|
|
2
9
|
import { ChartConfig, D3Selection, DrawContext, SVGSelection } from "./types";
|
|
10
|
+
/**
|
|
11
|
+
* Creates X and Y scales based on data and chart dimensions.
|
|
12
|
+
* Automatically selects appropriate scale type (linear, point, band).
|
|
13
|
+
*/
|
|
3
14
|
export declare function createScales<T>(data: T[], width: number, height: number, margin: {
|
|
4
15
|
top: number;
|
|
5
16
|
right: number;
|
|
6
17
|
bottom: number;
|
|
7
18
|
left: number;
|
|
8
|
-
}, x: (d: T) =>
|
|
9
|
-
xScale:
|
|
19
|
+
}, x: (d: T) => string | number, y: (d: T) => number, type?: "line" | "area" | "bar"): {
|
|
20
|
+
xScale: d3Scale.ScaleLinear<number, number, never> | d3Scale.ScalePoint<string> | d3Scale.ScaleBand<string>;
|
|
10
21
|
yScale: d3Scale.ScaleLinear<number, number, never>;
|
|
11
22
|
innerWidth: number;
|
|
12
23
|
innerHeight: number;
|
|
13
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Sets up an SVG gradient for area chart fills.
|
|
27
|
+
*/
|
|
14
28
|
export declare function setupGradient(svg: SVGSelection, gradientId: string): void;
|
|
15
|
-
|
|
16
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Draws horizontal grid lines behind the chart.
|
|
31
|
+
*/
|
|
32
|
+
export declare function drawGrid(g: D3Selection, yScale: d3Scale.ScaleLinear<number, number>, innerWidth: number, className: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Draws X and Y axes with labels.
|
|
35
|
+
* Automatically adjusts tick count to prevent label overlap.
|
|
36
|
+
*/
|
|
37
|
+
export declare function drawAxes(g: D3Selection, xScale: d3Scale.ScaleLinear<number, number> | d3Scale.ScalePoint<string> | d3Scale.ScaleBand<string>, yScale: d3Scale.ScaleLinear<number, number>, innerWidth: number, innerHeight: number, margin: {
|
|
17
38
|
top: number;
|
|
18
39
|
right: number;
|
|
19
40
|
bottom: number;
|
|
20
41
|
left: number;
|
|
21
42
|
}, config: ChartConfig, styles: Record<string, string>, isMobile: boolean): void;
|
|
43
|
+
/**
|
|
44
|
+
* Renders line and area charts with interactive cursor tracking.
|
|
45
|
+
*/
|
|
22
46
|
export declare function drawLineArea<T>({ g, data, xScale, yScale, x, y, innerWidth, innerHeight, config, styles, gradientId, setHoverState, margin, type, }: DrawContext<T>): void;
|
|
23
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Renders bar charts with hover highlighting.
|
|
49
|
+
*/
|
|
50
|
+
export declare function drawBars<T>({ g, data, xScale, yScale, x, y, innerWidth, innerHeight, styles, setHoverState, margin, }: DrawContext<T>): void;
|
|
@@ -1,9 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart Renderers
|
|
3
|
+
*
|
|
4
|
+
* D3-based rendering functions for charts. Each renderer handles
|
|
5
|
+
* drawing a specific chart type (line, area, bar) and managing
|
|
6
|
+
* user interactions (hover, touch).
|
|
7
|
+
*/
|
|
1
8
|
import * as d3Array from "d3-array";
|
|
2
9
|
import * as d3Axis from "d3-axis";
|
|
3
10
|
import * as d3Scale from "d3-scale";
|
|
4
11
|
import * as d3Selection from "d3-selection";
|
|
5
12
|
import * as d3Shape from "d3-shape";
|
|
6
13
|
const d3 = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, d3Scale), d3Shape), d3Selection), d3Axis), d3Array);
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// UTILITIES
|
|
16
|
+
// ============================================================================
|
|
17
|
+
/**
|
|
18
|
+
* Extracts pointer coordinates from mouse or touch events.
|
|
19
|
+
* Uses the parent SVG as reference for accurate positioning across all devices.
|
|
20
|
+
*/
|
|
21
|
+
function getPointerCoords(event, gElement, margin) {
|
|
22
|
+
if (!gElement) {
|
|
23
|
+
return [0, 0];
|
|
24
|
+
}
|
|
25
|
+
const svg = gElement.ownerSVGElement;
|
|
26
|
+
if (!svg) {
|
|
27
|
+
return [0, 0];
|
|
28
|
+
}
|
|
29
|
+
const rect = svg.getBoundingClientRect();
|
|
30
|
+
let clientX;
|
|
31
|
+
let clientY;
|
|
32
|
+
if ("touches" in event && event.touches.length > 0) {
|
|
33
|
+
clientX = event.touches[0].clientX;
|
|
34
|
+
clientY = event.touches[0].clientY;
|
|
35
|
+
}
|
|
36
|
+
else if ("changedTouches" in event && event.changedTouches.length > 0) {
|
|
37
|
+
clientX = event.changedTouches[0].clientX;
|
|
38
|
+
clientY = event.changedTouches[0].clientY;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
clientX = event.clientX;
|
|
42
|
+
clientY = event.clientY;
|
|
43
|
+
}
|
|
44
|
+
return [clientX - rect.left - margin.left, clientY - rect.top - margin.top];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Creates an SVG path for a rectangle with rounded top corners.
|
|
48
|
+
* Used for bar charts to create a softer, modern look.
|
|
49
|
+
*/
|
|
50
|
+
function createRoundedTopBarPath(xPos, yPos, width, height, radius) {
|
|
51
|
+
const r = Math.min(radius, width / 2, height);
|
|
52
|
+
return `
|
|
53
|
+
M ${xPos},${yPos + height}
|
|
54
|
+
L ${xPos},${yPos + r}
|
|
55
|
+
A ${r},${r} 0 0 1 ${xPos + r},${yPos}
|
|
56
|
+
L ${xPos + width - r},${yPos}
|
|
57
|
+
A ${r},${r} 0 0 1 ${xPos + width},${yPos + r}
|
|
58
|
+
L ${xPos + width},${yPos + height}
|
|
59
|
+
Z
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// SCALE & AXIS SETUP
|
|
64
|
+
// ============================================================================
|
|
65
|
+
/**
|
|
66
|
+
* Creates X and Y scales based on data and chart dimensions.
|
|
67
|
+
* Automatically selects appropriate scale type (linear, point, band).
|
|
68
|
+
*/
|
|
7
69
|
export function createScales(data, width, height, margin, x, y, type) {
|
|
8
70
|
const innerWidth = width - margin.left - margin.right;
|
|
9
71
|
const innerHeight = height - margin.top - margin.bottom;
|
|
@@ -37,6 +99,9 @@ export function createScales(data, width, height, margin, x, y, type) {
|
|
|
37
99
|
.range([innerHeight, 0]);
|
|
38
100
|
return { xScale, yScale, innerWidth, innerHeight };
|
|
39
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Sets up an SVG gradient for area chart fills.
|
|
104
|
+
*/
|
|
40
105
|
export function setupGradient(svg, gradientId) {
|
|
41
106
|
const defs = svg.append("defs");
|
|
42
107
|
const gradient = defs
|
|
@@ -57,6 +122,9 @@ export function setupGradient(svg, gradientId) {
|
|
|
57
122
|
.attr("stop-color", "var(--primary)")
|
|
58
123
|
.attr("stop-opacity", 0);
|
|
59
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Draws horizontal grid lines behind the chart.
|
|
127
|
+
*/
|
|
60
128
|
export function drawGrid(g, yScale, innerWidth, className) {
|
|
61
129
|
g.append("g")
|
|
62
130
|
.attr("class", className)
|
|
@@ -65,12 +133,15 @@ export function drawGrid(g, yScale, innerWidth, className) {
|
|
|
65
133
|
.tickSize(-innerWidth)
|
|
66
134
|
.tickFormat(() => ""));
|
|
67
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Draws X and Y axes with labels.
|
|
138
|
+
* Automatically adjusts tick count to prevent label overlap.
|
|
139
|
+
*/
|
|
68
140
|
export function drawAxes(g, xScale, yScale, innerWidth, innerHeight, margin, config, styles, isMobile) {
|
|
69
|
-
const
|
|
70
|
-
const maxTicks = Math.floor(innerWidth /
|
|
141
|
+
const TICK_WIDTH = 40; // Approximate width of a tick label
|
|
142
|
+
const maxTicks = Math.floor(innerWidth / TICK_WIDTH);
|
|
71
143
|
const axisBottom = d3.axisBottom(xScale);
|
|
72
|
-
|
|
73
|
-
if (xScale.domain && Array.isArray(xScale.domain())) {
|
|
144
|
+
if ("domain" in xScale && Array.isArray(xScale.domain())) {
|
|
74
145
|
const domain = xScale.domain();
|
|
75
146
|
if (domain.length > maxTicks) {
|
|
76
147
|
const step = Math.ceil(domain.length / maxTicks);
|
|
@@ -80,8 +151,7 @@ export function drawAxes(g, xScale, yScale, innerWidth, innerHeight, margin, con
|
|
|
80
151
|
else {
|
|
81
152
|
axisBottom.ticks(Math.min(5, maxTicks));
|
|
82
153
|
}
|
|
83
|
-
|
|
84
|
-
.append("g")
|
|
154
|
+
g.append("g")
|
|
85
155
|
.attr("transform", `translate(0,${innerHeight})`)
|
|
86
156
|
.call(axisBottom);
|
|
87
157
|
const yAxis = g.append("g").call(d3
|
|
@@ -107,6 +177,12 @@ export function drawAxes(g, xScale, yScale, innerWidth, innerHeight, margin, con
|
|
|
107
177
|
.text(config.yAxisLabel);
|
|
108
178
|
}
|
|
109
179
|
}
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// CHART RENDERERS
|
|
182
|
+
// ============================================================================
|
|
183
|
+
/**
|
|
184
|
+
* Renders line and area charts with interactive cursor tracking.
|
|
185
|
+
*/
|
|
110
186
|
export function drawLineArea({ g, data, xScale, yScale, x, y, innerWidth, innerHeight, config, styles, gradientId, setHoverState, margin, type, }) {
|
|
111
187
|
const lineGenerator = d3
|
|
112
188
|
.line()
|
|
@@ -146,14 +222,14 @@ export function drawLineArea({ g, data, xScale, yScale, x, y, innerWidth, innerH
|
|
|
146
222
|
.attr("cy", (d) => yScale(y(d)))
|
|
147
223
|
.attr("r", 5);
|
|
148
224
|
}
|
|
149
|
-
// Interaction Overlay
|
|
150
225
|
const overlay = g
|
|
151
226
|
.append("rect")
|
|
152
227
|
.attr("class", "overlay")
|
|
153
228
|
.attr("width", innerWidth)
|
|
154
229
|
.attr("height", innerHeight)
|
|
155
230
|
.style("fill", "transparent")
|
|
156
|
-
.style("cursor", "crosshair")
|
|
231
|
+
.style("cursor", "crosshair")
|
|
232
|
+
.style("touch-action", "none");
|
|
157
233
|
const cursorLine = g
|
|
158
234
|
.append("line")
|
|
159
235
|
.attr("class", styles.cursorLine)
|
|
@@ -165,59 +241,65 @@ export function drawLineArea({ g, data, xScale, yScale, x, y, innerWidth, innerH
|
|
|
165
241
|
.attr("class", styles.cursorPoint)
|
|
166
242
|
.attr("r", 6)
|
|
167
243
|
.style("opacity", 0);
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
if (event.type.startsWith("touch")) {
|
|
171
|
-
event.preventDefault();
|
|
172
|
-
}
|
|
173
|
-
const [pointerX, pointerY] = d3.pointer(event);
|
|
174
|
-
let selectedData = null;
|
|
175
|
-
if (xScale.invert) {
|
|
244
|
+
const findNearestPoint = (pointerX) => {
|
|
245
|
+
if ("invert" in xScale && typeof xScale.invert === "function") {
|
|
176
246
|
const x0 = xScale.invert(pointerX);
|
|
177
247
|
const bisect = d3.bisector(x).left;
|
|
178
248
|
const i = bisect(data, x0, 1);
|
|
179
249
|
const d0 = data[i - 1];
|
|
180
250
|
const d1 = data[i];
|
|
181
251
|
if (!d0) {
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
else if (!d1) {
|
|
185
|
-
selectedData = d0;
|
|
252
|
+
return d1;
|
|
186
253
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const d1Dist = x(d1) - x0;
|
|
190
|
-
selectedData = d0Dist > d1Dist ? d1 : d0;
|
|
254
|
+
if (!d1) {
|
|
255
|
+
return d0;
|
|
191
256
|
}
|
|
257
|
+
const d0Dist = x0 - x(d0);
|
|
258
|
+
const d1Dist = x(d1) - x0;
|
|
259
|
+
return d0Dist > d1Dist ? d1 : d0;
|
|
192
260
|
}
|
|
193
|
-
else {
|
|
261
|
+
else if ("step" in xScale && typeof xScale.step === "function") {
|
|
194
262
|
const step = xScale.step();
|
|
195
263
|
const index = Math.round(pointerX / step);
|
|
196
|
-
|
|
264
|
+
return data[Math.min(Math.max(0, index), data.length - 1)];
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
};
|
|
268
|
+
const handleInteraction = (event) => {
|
|
269
|
+
var _a;
|
|
270
|
+
if (event.type.startsWith("touch") && event.cancelable) {
|
|
271
|
+
event.preventDefault();
|
|
197
272
|
}
|
|
273
|
+
const [pointerX, pointerY] = getPointerCoords(event, g.node(), margin);
|
|
274
|
+
const selectedData = findNearestPoint(pointerX);
|
|
198
275
|
if (selectedData) {
|
|
199
276
|
const cx = (_a = xScale(x(selectedData))) !== null && _a !== void 0 ? _a : 0;
|
|
200
277
|
const cy = yScale(y(selectedData));
|
|
201
278
|
cursorLine.attr("x1", cx).attr("x2", cx).style("opacity", 1);
|
|
202
279
|
cursorDot.attr("cx", cx).attr("cy", cy).style("opacity", 1);
|
|
280
|
+
const isTouch = event.type.startsWith("touch");
|
|
203
281
|
setHoverState({
|
|
204
|
-
x: pointerX + margin.left,
|
|
205
|
-
y: pointerY + margin.top,
|
|
282
|
+
x: (isTouch ? cx : pointerX) + margin.left,
|
|
283
|
+
y: (isTouch ? cy : pointerY) + margin.top, // Snap Y to point on touch, cursor on mouse
|
|
206
284
|
data: selectedData,
|
|
207
285
|
});
|
|
208
286
|
}
|
|
209
287
|
};
|
|
210
288
|
overlay
|
|
211
|
-
.on("mousemove touchmove touchstart",
|
|
212
|
-
.on("mouseleave touchend", () => {
|
|
289
|
+
.on("mousemove touchmove touchstart", handleInteraction)
|
|
290
|
+
.on("mouseleave touchend touchcancel", () => {
|
|
213
291
|
cursorLine.style("opacity", 0);
|
|
214
292
|
cursorDot.style("opacity", 0);
|
|
215
293
|
setHoverState(null);
|
|
216
294
|
});
|
|
217
295
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
296
|
+
/**
|
|
297
|
+
* Renders bar charts with hover highlighting.
|
|
298
|
+
*/
|
|
299
|
+
export function drawBars({ g, data, xScale, yScale, x, y, innerWidth, innerHeight, styles, setHoverState, margin, }) {
|
|
300
|
+
const BAR_RADIUS = 4;
|
|
301
|
+
const bars = g
|
|
302
|
+
.selectAll(".bar")
|
|
221
303
|
.data(data)
|
|
222
304
|
.enter()
|
|
223
305
|
.append("path")
|
|
@@ -225,28 +307,60 @@ export function drawBars({ g, data, xScale, yScale, x, y, innerHeight, styles, s
|
|
|
225
307
|
.attr("d", (d) => {
|
|
226
308
|
const xVal = xScale(x(d)) || 0;
|
|
227
309
|
const yVal = yScale(y(d));
|
|
228
|
-
const w =
|
|
310
|
+
const w = "bandwidth" in xScale ? xScale.bandwidth() : 10;
|
|
229
311
|
const h = innerHeight - yVal;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
312
|
+
return createRoundedTopBarPath(xVal, yVal, w, h, BAR_RADIUS);
|
|
313
|
+
});
|
|
314
|
+
const overlay = g
|
|
315
|
+
.append("rect")
|
|
316
|
+
.attr("class", "overlay")
|
|
317
|
+
.attr("width", innerWidth)
|
|
318
|
+
.attr("height", innerHeight)
|
|
319
|
+
.style("fill", "transparent")
|
|
320
|
+
.style("cursor", "crosshair")
|
|
321
|
+
.style("touch-action", "none");
|
|
322
|
+
const findNearestBar = (pointerX) => {
|
|
323
|
+
const bandwidth = "bandwidth" in xScale ? xScale.bandwidth() : 10;
|
|
324
|
+
let nearestData = null;
|
|
325
|
+
let minDist = Infinity;
|
|
326
|
+
for (const d of data) {
|
|
327
|
+
const barX = xScale(x(d)) || 0;
|
|
328
|
+
const barCenter = barX + bandwidth / 2;
|
|
329
|
+
const dist = Math.abs(pointerX - barCenter);
|
|
330
|
+
if (dist < minDist) {
|
|
331
|
+
minDist = dist;
|
|
332
|
+
nearestData = d;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return nearestData;
|
|
336
|
+
};
|
|
337
|
+
const handleInteraction = (event) => {
|
|
338
|
+
if (event.type.startsWith("touch") && event.cancelable) {
|
|
243
339
|
event.preventDefault();
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
340
|
+
}
|
|
341
|
+
const [pointerX, pointerY] = getPointerCoords(event, g.node(), margin);
|
|
342
|
+
const selectedData = findNearestBar(pointerX);
|
|
343
|
+
if (selectedData) {
|
|
344
|
+
bars.style("opacity", (d) => (d === selectedData ? 1 : 0.6));
|
|
345
|
+
let hoverX = pointerX;
|
|
346
|
+
let hoverY = pointerY;
|
|
347
|
+
if (event.type.startsWith("touch")) {
|
|
348
|
+
const xVal = xScale(x(selectedData)) || 0;
|
|
349
|
+
const bandwidth = "bandwidth" in xScale ? xScale.bandwidth() : 10;
|
|
350
|
+
hoverX = xVal + bandwidth / 2;
|
|
351
|
+
hoverY = yScale(y(selectedData));
|
|
352
|
+
}
|
|
353
|
+
setHoverState({
|
|
354
|
+
x: hoverX + margin.left,
|
|
355
|
+
y: hoverY + margin.top,
|
|
356
|
+
data: selectedData,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
overlay
|
|
361
|
+
.on("mousemove touchmove touchstart", handleInteraction)
|
|
362
|
+
.on("mouseleave touchend touchcancel", () => {
|
|
363
|
+
bars.style("opacity", 1);
|
|
364
|
+
setHoverState(null);
|
|
365
|
+
});
|
|
252
366
|
}
|
|
@@ -95,7 +95,7 @@ export function Image(_a) {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
setStatus("error");
|
|
98
|
-
setShowSkeleton(false); // Remove skeleton immediately on error
|
|
98
|
+
setShowSkeleton(false); // Remove skeleton immediately on error
|
|
99
99
|
onError === null || onError === void 0 ? void 0 : onError(e);
|
|
100
100
|
}, [fallbackSrc, status, onError]);
|
|
101
101
|
return (_jsxs("div", { className: clsx(styles.wrapper, rounded && styles.rounded, className), style: Object.assign({ aspectRatio: computedAspectRatio, width: toCssValue(width), height: toCssValue(height) }, style), children: [_jsx("div", { "aria-hidden": "true", className: clsx(styles.skeletonLayer, status === "loaded" && styles.fadeOut), children: showSkeleton && (_jsx(Skeleton, { style: {
|
|
@@ -25,7 +25,8 @@ export interface LayoutProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
25
25
|
export interface GridProps extends LayoutProps {
|
|
26
26
|
/**
|
|
27
27
|
* Defines the columns of the grid.
|
|
28
|
-
* Accepts a number (e.g., `3` for 3 equal columns) or a CSS string
|
|
28
|
+
* Accepts a number (e.g., `3` for 3 equal columns) or a CSS string
|
|
29
|
+
* (e.g., `"1fr 2fr"`).
|
|
29
30
|
* @default "1fr"
|
|
30
31
|
*/
|
|
31
32
|
columns?: string | number;
|
|
@@ -38,7 +39,8 @@ export interface GridProps extends LayoutProps {
|
|
|
38
39
|
}
|
|
39
40
|
/**
|
|
40
41
|
* A CSS Grid container for creating two-dimensional layouts.
|
|
41
|
-
* Use this when you need precise control over columns and rows, or complex
|
|
42
|
+
* Use this when you need precise control over columns and rows, or complex
|
|
43
|
+
* grid placements.
|
|
42
44
|
*/
|
|
43
45
|
export declare function Grid({ children, columns, gap, className, style, as: Component, ...props }: GridProps): import("react/jsx-runtime").JSX.Element;
|
|
44
46
|
export interface FlexProps extends LayoutProps {
|
|
@@ -64,7 +66,8 @@ export interface FlexProps extends LayoutProps {
|
|
|
64
66
|
*/
|
|
65
67
|
gap?: Spacing;
|
|
66
68
|
/**
|
|
67
|
-
* Controls whether flex items are forced onto one line or can wrap onto
|
|
69
|
+
* Controls whether flex items are forced onto one line or can wrap onto
|
|
70
|
+
* multiple lines.
|
|
68
71
|
* @default false
|
|
69
72
|
*/
|
|
70
73
|
wrap?: boolean | "wrap" | "nowrap" | "wrap-reverse";
|
|
@@ -87,20 +90,22 @@ export interface StackProps extends Omit<FlexProps, "direction"> {
|
|
|
87
90
|
* A specialized Flex container explicitly optimized for vertical stacking.
|
|
88
91
|
* Use this for lists, form fields, card content, or any group of elements
|
|
89
92
|
* that should be arranged vertically with consistent spacing.
|
|
90
|
-
* Note: While it defaults to column, `direction="row"` is supported for
|
|
93
|
+
* Note: While it defaults to column, `direction="row"` is supported for
|
|
94
|
+
* semantic overrides.
|
|
91
95
|
*/
|
|
92
96
|
export declare function Stack({ children, direction, gap, align, ...props }: StackProps): import("react/jsx-runtime").JSX.Element;
|
|
93
97
|
export interface SwitcherProps extends FlexProps {
|
|
94
98
|
/**
|
|
95
|
-
* The breakpoint threshold at which the layout switches from horizontal to
|
|
96
|
-
* e.g., "xs" means it will be horizontal on screens larger than
|
|
99
|
+
* The breakpoint threshold at which the layout switches from horizontal to
|
|
100
|
+
* vertical. e.g., "xs" means it will be horizontal on screens larger than
|
|
101
|
+
* "xs" (480px), and vertical below.
|
|
97
102
|
* @default "xs"
|
|
98
103
|
*/
|
|
99
104
|
threshold?: "xxs" | "xs" | "sm" | "md";
|
|
100
105
|
}
|
|
101
106
|
/**
|
|
102
|
-
* A responsive layout component that switches from horizontal to vertical
|
|
103
|
-
* based on a container query or breakpoint threshold.
|
|
107
|
+
* A responsive layout component that switches from horizontal to vertical
|
|
108
|
+
* layout based on a container query or breakpoint threshold.
|
|
104
109
|
* Use this for "sidebar + main content" layouts or any pattern that needs
|
|
105
110
|
* to stack on smaller screens but sit side-by-side on larger ones.
|
|
106
111
|
*/
|
|
@@ -34,7 +34,8 @@ function resolveGap(gap) {
|
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* A CSS Grid container for creating two-dimensional layouts.
|
|
37
|
-
* Use this when you need precise control over columns and rows, or complex
|
|
37
|
+
* Use this when you need precise control over columns and rows, or complex
|
|
38
|
+
* grid placements.
|
|
38
39
|
*/
|
|
39
40
|
export function Grid(_a) {
|
|
40
41
|
var { children, columns = "1fr", gap = 4, className, style, as: Component = "div" } = _a, props = __rest(_a, ["children", "columns", "gap", "className", "style", "as"]);
|
|
@@ -57,15 +58,16 @@ export function Flex(_a) {
|
|
|
57
58
|
* A specialized Flex container explicitly optimized for vertical stacking.
|
|
58
59
|
* Use this for lists, form fields, card content, or any group of elements
|
|
59
60
|
* that should be arranged vertically with consistent spacing.
|
|
60
|
-
* Note: While it defaults to column, `direction="row"` is supported for
|
|
61
|
+
* Note: While it defaults to column, `direction="row"` is supported for
|
|
62
|
+
* semantic overrides.
|
|
61
63
|
*/
|
|
62
64
|
export function Stack(_a) {
|
|
63
65
|
var { children, direction = "column", gap = 4, align = "stretch" } = _a, props = __rest(_a, ["children", "direction", "gap", "align"]);
|
|
64
66
|
return (_jsx(Flex, Object.assign({ align: align, direction: direction, gap: gap }, props, { children: children })));
|
|
65
67
|
}
|
|
66
68
|
/**
|
|
67
|
-
* A responsive layout component that switches from horizontal to vertical
|
|
68
|
-
* based on a container query or breakpoint threshold.
|
|
69
|
+
* A responsive layout component that switches from horizontal to vertical
|
|
70
|
+
* layout based on a container query or breakpoint threshold.
|
|
69
71
|
* Use this for "sidebar + main content" layouts or any pattern that needs
|
|
70
72
|
* to stack on smaller screens but sit side-by-side on larger ones.
|
|
71
73
|
*/
|
|
@@ -29,7 +29,8 @@ export function Table({ data, columns, enablePagination = true, enableFiltering
|
|
|
29
29
|
onGlobalFilterChange: setGlobalFilter,
|
|
30
30
|
onPaginationChange: setPagination,
|
|
31
31
|
getCoreRowModel: getCoreRowModel(),
|
|
32
|
-
|
|
32
|
+
// Always provide the model, enableSorting controls usage
|
|
33
|
+
getSortedRowModel: getSortedRowModel(),
|
|
33
34
|
getPaginationRowModel: enablePagination
|
|
34
35
|
? getPaginationRowModel()
|
|
35
36
|
: undefined,
|
package/dist/styles/globals.css
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/* _reset.scss */
|
|
2
|
-
*,
|
|
2
|
+
*,
|
|
3
|
+
*::before,
|
|
4
|
+
*::after {
|
|
3
5
|
box-sizing: border-box;
|
|
4
6
|
margin: 0;
|
|
5
7
|
padding: 0;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
html,
|
|
10
|
+
html,
|
|
11
|
+
body {
|
|
9
12
|
height: 100%;
|
|
10
13
|
}
|
|
11
14
|
|
|
@@ -17,16 +20,29 @@ body {
|
|
|
17
20
|
font-family: var(--font-sans, system-ui, sans-serif);
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
img,
|
|
23
|
+
img,
|
|
24
|
+
picture,
|
|
25
|
+
video,
|
|
26
|
+
canvas,
|
|
27
|
+
svg {
|
|
21
28
|
display: block;
|
|
22
29
|
max-width: 100%;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
|
-
input,
|
|
32
|
+
input,
|
|
33
|
+
button,
|
|
34
|
+
textarea,
|
|
35
|
+
select {
|
|
26
36
|
font: inherit;
|
|
27
37
|
}
|
|
28
38
|
|
|
29
|
-
p,
|
|
39
|
+
p,
|
|
40
|
+
h1,
|
|
41
|
+
h2,
|
|
42
|
+
h3,
|
|
43
|
+
h4,
|
|
44
|
+
h5,
|
|
45
|
+
h6 {
|
|
30
46
|
overflow-wrap: break-word;
|
|
31
47
|
}
|
|
32
48
|
|