@konce-pt/react 0.15.1 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/charts/chart-tooltip.d.ts +10 -2
- package/dist/charts/chart-tooltip.js +42 -6
- package/dist/charts/chart.js +261 -55
- package/dist/charts/llms.txt +21 -6
- package/dist/i18n/i18n-context.js +5 -5
- package/dist/internal/overlay.js +20 -1
- package/dist/popover/llms.txt +2 -0
- package/dist/popover/popover.d.ts +6 -1
- package/dist/popover/popover.js +2 -2
- package/package.json +14 -14
|
@@ -10,12 +10,20 @@ export interface KptChartTooltipState {
|
|
|
10
10
|
y: number;
|
|
11
11
|
label: string;
|
|
12
12
|
rows: KptChartTooltipRow[];
|
|
13
|
+
/** Kierunek ruchu do tego punktu: 1 w prawo, -1 w lewo, 0 bez kierunku - stąd wjeżdża etykieta. */
|
|
14
|
+
dir?: number;
|
|
15
|
+
/** Dymek już był na ekranie i jedzie dalej - etykieta animuje zmianę (pierwsze pokazanie stoi). */
|
|
16
|
+
live?: boolean;
|
|
13
17
|
}
|
|
14
18
|
export interface KptChartTooltipProps {
|
|
15
19
|
state: KptChartTooltipState | null;
|
|
16
20
|
}
|
|
17
21
|
/**
|
|
18
|
-
* Dymek wykresu - pozycjonowany
|
|
19
|
-
*
|
|
22
|
+
* Dymek wykresu - pozycjonowany względem obszaru wykresu, niezależnie od skali SVG. Element
|
|
23
|
+
* wewnętrzny biblioteki, bliźniak `kpt-chart-tooltip` z portu Angulara (te same klasy i SCSS).
|
|
24
|
+
*
|
|
25
|
+
* Nie wychodzi poza obszar wykresu: przycięcie liczy się z faktycznej szerokości dymka. Przy
|
|
26
|
+
* chowaniu zostaje z ostatnią treścią i gaśnie (`--hidden`); cyfry wartości stoją na bębnach
|
|
27
|
+
* (`reelCells` z `@konce-pt/chart`), które przy kolejnym punkcie kręcą się do nowej liczby.
|
|
20
28
|
*/
|
|
21
29
|
export declare function KptChartTooltip({ state }: Readonly<KptChartTooltipProps>): import("react").JSX.Element | null;
|
|
@@ -1,13 +1,49 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { reelCells } from '@konce-pt/chart';
|
|
2
4
|
import { cn } from "../internal/cn.js";
|
|
5
|
+
const DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
3
6
|
/**
|
|
4
|
-
* Dymek wykresu - pozycjonowany
|
|
5
|
-
*
|
|
7
|
+
* Dymek wykresu - pozycjonowany względem obszaru wykresu, niezależnie od skali SVG. Element
|
|
8
|
+
* wewnętrzny biblioteki, bliźniak `kpt-chart-tooltip` z portu Angulara (te same klasy i SCSS).
|
|
9
|
+
*
|
|
10
|
+
* Nie wychodzi poza obszar wykresu: przycięcie liczy się z faktycznej szerokości dymka. Przy
|
|
11
|
+
* chowaniu zostaje z ostatnią treścią i gaśnie (`--hidden`); cyfry wartości stoją na bębnach
|
|
12
|
+
* (`reelCells` z `@konce-pt/chart`), które przy kolejnym punkcie kręcą się do nowej liczby.
|
|
6
13
|
*/
|
|
7
14
|
export function KptChartTooltip({ state }) {
|
|
8
|
-
|
|
15
|
+
// Ostatnia treść zostaje na czas wygaszania (wzorzec "poprzednia wartość w stanie", bez efektu).
|
|
16
|
+
const [shown, setShown] = useState(state);
|
|
17
|
+
if (state && state !== shown)
|
|
18
|
+
setShown(state);
|
|
19
|
+
const bubbleRef = useRef(null);
|
|
20
|
+
const labelRef = useRef(null);
|
|
21
|
+
const lastLabel = useRef('');
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
const el = bubbleRef.current;
|
|
24
|
+
if (!shown || !el)
|
|
25
|
+
return;
|
|
26
|
+
// obszar wykresu (`.kpt-chart__canvas`, position: relative) - host dymka nie ma wymiarów
|
|
27
|
+
const box = el.offsetParent;
|
|
28
|
+
if (box) {
|
|
29
|
+
const width = box.clientWidth;
|
|
30
|
+
const half = el.offsetWidth / 2;
|
|
31
|
+
const x = (shown.x / 100) * width;
|
|
32
|
+
el.style.left = `${half * 2 >= width ? width / 2 : Math.min(width - half, Math.max(half, x))}px`;
|
|
33
|
+
}
|
|
34
|
+
// Nowa kategoria: etykieta wjeżdża z kierunku ruchu. Pierwsze pokazanie stoi. Nagłówek nigdy nie
|
|
35
|
+
// znika: start od 40% widoczności, a przy szybkim przejeździe trwający wjazd nie startuje od nowa
|
|
36
|
+
// (wtedy tylko podmienia się tekst) - inaczej przy każdej kategorii etykieta gasła.
|
|
37
|
+
const label = labelRef.current;
|
|
38
|
+
if (label && shown.label !== lastLabel.current) {
|
|
39
|
+
if (shown.live && lastLabel.current && typeof label.animate === 'function' && !label.getAnimations().length) {
|
|
40
|
+
const dir = shown.dir ?? 0;
|
|
41
|
+
label.animate({ opacity: [0.4, 1], filter: ['blur(1.5px)', 'blur(0px)'], translate: [`${dir * 6}px ${dir ? 0 : 4}px`, '0 0'] }, { duration: 220, easing: 'cubic-bezier(0.2, 0, 0, 1)' });
|
|
42
|
+
}
|
|
43
|
+
lastLabel.current = shown.label;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
if (!shown)
|
|
9
47
|
return null;
|
|
10
|
-
|
|
11
|
-
const left = Math.min(94, Math.max(6, state.x));
|
|
12
|
-
return (_jsx("div", { className: "kpt-chart-tooltip-host", children: _jsxs("div", { className: cn('kpt-chart__tooltip', state.y < 22 && 'kpt-chart__tooltip--flip'), style: { left: `${left}%`, top: `${state.y}%` }, role: "tooltip", children: [state.label ? _jsx("div", { className: "kpt-chart__tooltip-title", children: state.label }) : null, state.rows.map((row, index) => (_jsxs("div", { className: "kpt-chart__tooltip-row", children: [_jsx("span", { className: "kpt-chart__tooltip-dot", style: { backgroundColor: row.color } }), _jsx("span", { className: "kpt-chart__tooltip-name", children: row.name }), _jsx("span", { className: "kpt-chart__tooltip-val", children: row.value })] }, index)))] }) }));
|
|
48
|
+
return (_jsx("div", { className: "kpt-chart-tooltip-host", children: _jsxs("div", { ref: bubbleRef, className: cn('kpt-chart__tooltip', shown.y < 22 && 'kpt-chart__tooltip--flip', !state && 'kpt-chart__tooltip--hidden'), style: { top: `${shown.y}%` }, role: "tooltip", "aria-hidden": state ? undefined : true, children: [shown.label ? (_jsx("div", { className: "kpt-chart__tooltip-title", children: _jsx("span", { ref: labelRef, className: "kpt-chart__tooltip-label", children: shown.label }) })) : null, shown.rows.map((row, index) => (_jsxs("div", { className: "kpt-chart__tooltip-row", children: [_jsx("span", { className: "kpt-chart__tooltip-dot", style: { backgroundColor: row.color } }), _jsx("span", { className: "kpt-chart__tooltip-name", children: row.name }), _jsxs("span", { className: "kpt-chart__tooltip-val", children: [_jsx("span", { className: "kpt-chart__sr", children: row.value }), _jsx("span", { className: "kpt-chart__reels", "aria-hidden": "true", children: reelCells(row.value).map((cell) => cell.kind === 'digit' ? (_jsx("span", { className: "kpt-chart__reel", children: _jsx("span", { className: "kpt-chart__reel-strip", style: { ['--kpt-chart-digit']: cell.digit }, children: DIGITS.map((d) => (_jsx("span", { children: d }, d))) }) }, cell.key)) : (_jsx("span", { className: "kpt-chart__reel-text", children: cell.text }, cell.key))) })] })] }, index)))] }) }));
|
|
13
49
|
}
|
package/dist/charts/chart.js
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useId, useState } from 'react';
|
|
4
|
-
import { areaPath, bandScale, barLayout, chartAccessibleName, chartDataTable, extent, formatNumber, linePath, linearScale, niceScale, pieSlices, plotArea, polarSlices, polarToCartesian, polygonPath, radarGrid, radarPoints, radialArea, seriesColor, } from '@konce-pt/chart';
|
|
3
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
4
|
+
import { KptChartMotion, areaPath, bandScale, barLayout, chartAccessibleName, chartDataTable, dataScale, deltaKind, domainScale, extent, formatNumber, holdDomain, linePath, linearScale, niceScale, pieSlices, plotArea, polarSlices, polarToCartesian, polygonPath, radarGrid, radarPoints, radialArea, sameChartShape, seriesColor, significantDelta, } from '@konce-pt/chart';
|
|
5
5
|
import { cn } from "../internal/cn.js";
|
|
6
|
+
import { useKptMotion } from "../motion/motion-context.js";
|
|
6
7
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
7
8
|
import { KptChartLegend } from "./chart-legend.js";
|
|
8
9
|
import { KptChartTooltip } from "./chart-tooltip.js";
|
|
9
10
|
const VERTICAL_INSETS = { top: 12, right: 16, bottom: 28, left: 46 };
|
|
10
11
|
const HORIZONTAL_INSETS = { top: 12, right: 16, bottom: 28, left: 76 };
|
|
12
|
+
/** Szerokość pasma smugi światła (jednostki viewBoxa) - połowa po każdej stronie czoła fali. */
|
|
13
|
+
const SHEEN_HALF = 90;
|
|
14
|
+
/** Zjechanie z wykresu: dymek czeka chwilę, zanim zgaśnie (powrót kursora go zatrzymuje). */
|
|
15
|
+
const TIP_HIDE_DELAY = 60;
|
|
16
|
+
const EMPTY_FRAME = { values: null, domain: null, ghost: null, sheen: null, tip: null };
|
|
17
|
+
/** Indeks najbliższej pozycji (środka kategorii) do współrzędnej kursora. */
|
|
18
|
+
function nearestIndex(positions, at) {
|
|
19
|
+
let best = 0;
|
|
20
|
+
positions.forEach((p, i) => {
|
|
21
|
+
if (Math.abs(p - at) < Math.abs((positions[best] ?? p) - at))
|
|
22
|
+
best = i;
|
|
23
|
+
});
|
|
24
|
+
return best;
|
|
25
|
+
}
|
|
26
|
+
/** Różnica ze znakiem (plus albo minus z myślnikiem ASCII). */
|
|
27
|
+
function signed(delta) {
|
|
28
|
+
const text = formatNumber(Math.abs(Math.round(delta)));
|
|
29
|
+
return delta > 0 ? `+${text}` : `-${text}`;
|
|
30
|
+
}
|
|
31
|
+
/** Koniec słupka po stronie wartości (dla ujemnych - po drugiej stronie osi). */
|
|
32
|
+
function barEnd(bar, vertical) {
|
|
33
|
+
if (vertical)
|
|
34
|
+
return bar.value >= 0 ? bar.rect.y : bar.rect.y + bar.rect.height;
|
|
35
|
+
return bar.value >= 0 ? bar.rect.x + bar.rect.width : bar.rect.x;
|
|
36
|
+
}
|
|
11
37
|
function styleFor(align) {
|
|
12
38
|
if (align === 'center')
|
|
13
39
|
return 'center';
|
|
@@ -32,7 +58,6 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
32
58
|
const dataTable = chartDataTable(config, activeLocale);
|
|
33
59
|
const [hidden, setHidden] = useState(new Set());
|
|
34
60
|
const [highlighted, setHighlighted] = useState(null);
|
|
35
|
-
const [tooltip, setTooltip] = useState(null);
|
|
36
61
|
/** Indeks kategorii pod kursorem - prowadnica crosshair w wykresie liniowym. */
|
|
37
62
|
const [activeIndex, setActiveIndex] = useState(null);
|
|
38
63
|
const anim = {
|
|
@@ -41,6 +66,70 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
41
66
|
delay: config.animation?.delay ?? 0,
|
|
42
67
|
stagger: config.animation?.stagger ?? 60,
|
|
43
68
|
};
|
|
69
|
+
// -- Ruch sterowany z kodu (pętla klatek w `@konce-pt/chart`, wspólna z portem Angulara) --
|
|
70
|
+
const motionConfig = useKptMotion();
|
|
71
|
+
const [frame, setFrame] = useState(EMPTY_FRAME);
|
|
72
|
+
const [motion] = useState(() => new KptChartMotion((f) => setFrame(f)));
|
|
73
|
+
const [tipContent, setTipContent] = useState(null);
|
|
74
|
+
const [ripple, setRipple] = useState(null);
|
|
75
|
+
const prevConfig = useRef(null);
|
|
76
|
+
const lastTipIndex = useRef(null);
|
|
77
|
+
const hideTimer = useRef(undefined);
|
|
78
|
+
const rippleId = useRef(0);
|
|
79
|
+
/** Ruch wyłączają: config, globalny `KptMotionProvider enabled={false}` i systemowe "ogranicz ruch". */
|
|
80
|
+
const motionAllowed = () => {
|
|
81
|
+
if (!anim.enabled || motionConfig?.enabled === false)
|
|
82
|
+
return false;
|
|
83
|
+
return typeof matchMedia === 'undefined' || !matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
84
|
+
};
|
|
85
|
+
const tipMotion = () => motionAllowed() && config.tooltip?.motion !== false;
|
|
86
|
+
useEffect(() => () => motion.stop(), [motion]);
|
|
87
|
+
useEffect(() => () => clearTimeout(hideTimer.current), []);
|
|
88
|
+
// Nowe wartości w tym samym wykresie -> przejście; inny wykres -> zwykłe wejście z CSS.
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const prev = prevConfig.current;
|
|
91
|
+
prevConfig.current = config;
|
|
92
|
+
if (!prev || !sameChartShape(prev, config)) {
|
|
93
|
+
motion.reset();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (prev.series.every((s, i) => s.data.every((v, k) => v === config.series[i]?.data[k])))
|
|
97
|
+
return;
|
|
98
|
+
const allowed = (config.animation?.enabled ?? true) &&
|
|
99
|
+
motionConfig?.enabled !== false &&
|
|
100
|
+
(typeof matchMedia === 'undefined' || !matchMedia('(prefers-reduced-motion: reduce)').matches);
|
|
101
|
+
if (!allowed || config.animation?.update === false) {
|
|
102
|
+
motion.cancelData();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const from = motion.current(prev.series.map((s) => s.data));
|
|
106
|
+
const to = config.series.map((s) => [...s.data]);
|
|
107
|
+
const withAxis = config.type === 'line' || config.type === 'bar';
|
|
108
|
+
const visible = (rows) => rows.filter((_, i) => !hidden.has(`ser:${config.series[i]?.name ?? ''}`));
|
|
109
|
+
const scales = withAxis
|
|
110
|
+
? { start: dataScale(visible(from)), hold: dataScale([holdDomain(visible(from), visible(to))]), end: dataScale(visible(to)) }
|
|
111
|
+
: null;
|
|
112
|
+
motion.transition(from, to, { delta: withAxis && config.animation?.delta !== false, scales });
|
|
113
|
+
}, [config, hidden, motion, motionConfig]);
|
|
114
|
+
/** Config z wartościami z bieżącej klatki ruchu - z niego liczy się geometria. */
|
|
115
|
+
const view = frame.values
|
|
116
|
+
? { ...config, series: config.series.map((s, i) => ({ ...s, data: frame.values?.[i] ?? s.data })) }
|
|
117
|
+
: config;
|
|
118
|
+
const tooltip = tipContent && frame.tip ? { x: (frame.tip.x / width) * 100, y: (frame.tip.y / height) * 100, ...tipContent } : null;
|
|
119
|
+
const ghostState = frame.ghost;
|
|
120
|
+
const sheenId = `${descId}-sheen`;
|
|
121
|
+
const horizontal = config.type === 'bar' && config.orientation === 'horizontal';
|
|
122
|
+
const sheen = (() => {
|
|
123
|
+
if (!frame.sheen)
|
|
124
|
+
return null;
|
|
125
|
+
const plot = plotArea(width, height, horizontal ? HORIZONTAL_INSETS : VERTICAL_INSETS);
|
|
126
|
+
const start = horizontal ? plot.y : plot.x;
|
|
127
|
+
const length = horizontal ? plot.height : plot.width;
|
|
128
|
+
const mid = start - SHEEN_HALF + (length + 2 * SHEEN_HALF) * frame.sheen.progress;
|
|
129
|
+
return horizontal
|
|
130
|
+
? { x1: 0, x2: 0, y1: mid - SHEEN_HALF, y2: mid + SHEEN_HALF, opacity: frame.sheen.opacity }
|
|
131
|
+
: { x1: mid - SHEEN_HALF, x2: mid + SHEEN_HALF, y1: 0, y2: 0, opacity: frame.sheen.opacity };
|
|
132
|
+
})();
|
|
44
133
|
const legendOpts = {
|
|
45
134
|
display: config.legend?.display ?? true,
|
|
46
135
|
position: config.legend?.position ?? 'bottom',
|
|
@@ -63,14 +152,45 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
63
152
|
hidden: hidden.has(`ser:${s.name}`),
|
|
64
153
|
}));
|
|
65
154
|
const dim = (key) => highlighted !== null && highlighted !== key;
|
|
66
|
-
const
|
|
155
|
+
const row = (name, label, value, color) => {
|
|
156
|
+
const format = config.tooltip?.format;
|
|
157
|
+
return { name, value: format ? format({ series: name, label, value }) : formatNumber(value), color };
|
|
158
|
+
};
|
|
159
|
+
const openTip = (x, y, label, rows, index) => {
|
|
67
160
|
if (config.tooltip?.enabled === false)
|
|
68
161
|
return;
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
162
|
+
clearTimeout(hideTimer.current);
|
|
163
|
+
const first = tipContent === null;
|
|
164
|
+
const animate = tipMotion();
|
|
165
|
+
const dir = first || index === null || lastTipIndex.current === null ? 0 : Math.sign(index - lastTipIndex.current);
|
|
166
|
+
lastTipIndex.current = index;
|
|
167
|
+
setTipContent({ label, rows, dir, live: !first && animate });
|
|
168
|
+
motion.moveTip(x, y, first || !animate);
|
|
169
|
+
};
|
|
170
|
+
/** Wycinki i radar: dymek jednego elementu pod kursorem. */
|
|
171
|
+
const showTip = (tipX, tipY, name, label, value, color) => openTip(tipX, tipY, label, [row(name, label, value, color)], null);
|
|
172
|
+
const hideTip = () => {
|
|
173
|
+
clearTimeout(hideTimer.current);
|
|
174
|
+
hideTimer.current = setTimeout(() => {
|
|
175
|
+
setTipContent(null);
|
|
176
|
+
setActiveIndex(null);
|
|
177
|
+
setRipple(null);
|
|
178
|
+
lastTipIndex.current = null;
|
|
179
|
+
motion.hideTip();
|
|
180
|
+
}, TIP_HIDE_DELAY);
|
|
181
|
+
};
|
|
182
|
+
/** Ten sam punkt pod kursorem - dymek zostaje (i nie zgaśnie, jeśli właśnie miał). */
|
|
183
|
+
const keepTip = (idx) => {
|
|
184
|
+
if (tipContent === null || lastTipIndex.current !== idx)
|
|
185
|
+
return false;
|
|
186
|
+
clearTimeout(hideTimer.current);
|
|
187
|
+
return true;
|
|
188
|
+
};
|
|
189
|
+
/** Kursor w viewBoxie wykresu (SVG skaluje się do kontenera). */
|
|
190
|
+
const svgPoint = (event) => {
|
|
191
|
+
const matrix = event.currentTarget.ownerSVGElement?.getScreenCTM();
|
|
192
|
+
return matrix ? new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse()) : null;
|
|
72
193
|
};
|
|
73
|
-
const hideTip = () => setTooltip(null);
|
|
74
194
|
const toggle = (key) => {
|
|
75
195
|
const next = new Set(hidden);
|
|
76
196
|
if (next.has(key))
|
|
@@ -81,19 +201,21 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
81
201
|
};
|
|
82
202
|
// -- Wykres liniowy --
|
|
83
203
|
const lineVm = (() => {
|
|
84
|
-
if (
|
|
204
|
+
if (view.type !== 'line')
|
|
85
205
|
return null;
|
|
86
|
-
const active =
|
|
206
|
+
const active = view.series.map((s, i) => ({ s, i })).filter(({ s }) => !hidden.has(`ser:${s.name}`));
|
|
87
207
|
const values = active.flatMap(({ s }) => s.data);
|
|
88
208
|
const [mn, mx] = extent(values.length ? values : [0]);
|
|
89
|
-
const scale =
|
|
209
|
+
const scale = frame.domain
|
|
210
|
+
? domainScale(frame.domain.target, frame.domain.min, frame.domain.max)
|
|
211
|
+
: niceScale(Math.min(0, mn), Math.max(0, mx), 5);
|
|
90
212
|
const plot = plotArea(width, height, VERTICAL_INSETS);
|
|
91
213
|
const yScale = linearScale(scale.min, scale.max, plot.y + plot.height, plot.y);
|
|
92
|
-
const count =
|
|
214
|
+
const count = view.labels.length;
|
|
93
215
|
const xAt = (i) => count > 1 ? plot.x + (i / (count - 1)) * plot.width : plot.x + plot.width / 2;
|
|
94
216
|
const baseline = yScale(0);
|
|
95
|
-
const interpolation =
|
|
96
|
-
const showPoints =
|
|
217
|
+
const interpolation = view.interpolation ?? 'linear';
|
|
218
|
+
const showPoints = view.points ?? true;
|
|
97
219
|
const grid = scale.ticks.map((t) => ({
|
|
98
220
|
x1: plot.x,
|
|
99
221
|
y1: yScale(t),
|
|
@@ -106,49 +228,89 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
106
228
|
text: formatNumber(t),
|
|
107
229
|
anchor: 'end',
|
|
108
230
|
}));
|
|
109
|
-
const catLabels =
|
|
231
|
+
const catLabels = view.labels.map((label, i) => ({
|
|
110
232
|
x: xAt(i),
|
|
111
233
|
y: plot.y + plot.height + 18,
|
|
112
234
|
text: label,
|
|
113
235
|
anchor: 'middle',
|
|
114
236
|
}));
|
|
237
|
+
const series = active.map(({ s, i }) => ({
|
|
238
|
+
key: `ser:${s.name}`,
|
|
239
|
+
name: s.name,
|
|
240
|
+
color: seriesColor(s.color, i),
|
|
241
|
+
i,
|
|
242
|
+
points: s.data.map((v, idx) => ({ x: xAt(idx), y: yScale(v), value: v, label: view.labels[idx] ?? '' })),
|
|
243
|
+
}));
|
|
115
244
|
const crosshairCat = config.crosshair && activeIndex !== null ? catLabels[activeIndex] : undefined;
|
|
245
|
+
// Prowadnica i pigułka jadą razem z dymkiem.
|
|
246
|
+
const crossX = crosshairCat ? (frame.tip?.x ?? crosshairCat.x) : 0;
|
|
116
247
|
const pillWidth = crosshairCat ? Math.max(28, crosshairCat.text.length * 7 + 16) : 0;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
248
|
+
/** Najbliższa kategoria w całym obszarze wykresu: wiersze wszystkich serii, dymek nad najwyższym punktem. */
|
|
249
|
+
const onMove = (event) => {
|
|
250
|
+
const p = svgPoint(event);
|
|
251
|
+
if (!p || !series.length)
|
|
252
|
+
return;
|
|
253
|
+
const idx = nearestIndex(catLabels.map((l) => l.x), p.x);
|
|
254
|
+
if (keepTip(idx))
|
|
255
|
+
return;
|
|
256
|
+
const hits = series.map((s) => ({ s, pt: s.points[idx] })).filter((e) => !!e.pt);
|
|
257
|
+
const top = hits.reduce((a, b) => (b.pt.y < a.pt.y ? b : a), hits[0]);
|
|
258
|
+
if (!top)
|
|
259
|
+
return;
|
|
260
|
+
const label = config.labels[idx] ?? '';
|
|
261
|
+
const moved = tipContent !== null && lastTipIndex.current !== null;
|
|
262
|
+
setActiveIndex(idx);
|
|
263
|
+
openTip(top.pt.x, top.pt.y, label, hits.map((e) => row(e.s.name, label, e.pt.value, e.s.color)), idx);
|
|
264
|
+
if (moved && tipMotion())
|
|
265
|
+
setRipple({ id: ++rippleId.current, x: top.pt.x, y: top.pt.y, color: top.s.color });
|
|
266
|
+
};
|
|
267
|
+
return (_jsxs(_Fragment, { children: [_jsx("g", { className: "kpt-chart__grid", children: grid.map((g, i) => (_jsx("line", { className: "kpt-chart__gridline", x1: g.x1, y1: g.y1, x2: g.x2, y2: g.y2 }, i))) }), crosshairCat ? (_jsx("line", { className: "kpt-chart__crosshair", x1: crossX, y1: plot.y, x2: crossX, y2: plot.y + plot.height })) : null, _jsx("g", { className: "kpt-chart__axis", children: [...valueLabels, ...catLabels].map((l, i) => (_jsx("text", { x: l.x, y: l.y, textAnchor: l.anchor, children: l.text }, i))) }), crosshairCat ? (_jsxs("g", { className: "kpt-chart__cat-active", children: [_jsx("rect", { x: crossX - pillWidth / 2, y: crosshairCat.y - 13, width: pillWidth, height: 18, rx: 9 }), _jsx("text", { x: crossX, y: crosshairCat.y, textAnchor: "middle", children: crosshairCat.text })] })) : null, ghostState
|
|
268
|
+
? series.map((s) => {
|
|
269
|
+
const prev = ghostState.prev[s.i] ?? [];
|
|
270
|
+
const next = ghostState.next[s.i] ?? [];
|
|
271
|
+
const gpts = prev.map((v, idx) => ({ x: xAt(idx), y: yScale(v) }));
|
|
272
|
+
return (_jsxs("g", { className: "kpt-chart__ghost", "aria-hidden": "true", children: [_jsx("path", { className: "kpt-chart__ghost-line", d: linePath(gpts, interpolation) }), gpts.map((p, idx) => (_jsx("line", { className: "kpt-chart__delta", "data-kind": deltaKind(prev[idx] ?? 0, next[idx] ?? 0), x1: p.x, x2: p.x, y1: p.y, y2: s.points[idx]?.y ?? p.y }, idx)))] }, `${ghostState.id}:${s.key}`));
|
|
273
|
+
})
|
|
274
|
+
: null, series.map((s) => (_jsxs("g", { className: cn('kpt-chart__series', dim(s.key) && 'kpt-chart__series--dim'), children: [view.area ? (_jsx("path", { className: "kpt-chart__area", d: areaPath(s.points, baseline, interpolation), style: { fill: s.color } })) : null, _jsx("path", { className: "kpt-chart__line", d: linePath(s.points, interpolation), style: { stroke: s.color }, pathLength: 1 }), showPoints
|
|
275
|
+
? s.points.map((p, idx) => (_jsx("circle", { className: cn('kpt-chart__point', activeIndex === idx && 'kpt-chart__point--active'), cx: p.x, cy: p.y, r: 4, style: { fill: s.color, ['--kpt-chart-i']: idx } }, idx)))
|
|
276
|
+
: null] }, s.key))), sheen
|
|
277
|
+
? series.map((s) => (_jsx("path", { className: "kpt-chart__sheen-line", d: linePath(s.points, interpolation), stroke: `url(#${sheenId})`, style: { opacity: sheen.opacity }, "aria-hidden": "true" }, s.key)))
|
|
278
|
+
: null, ghostState
|
|
279
|
+
? series.flatMap((s) => {
|
|
280
|
+
const prev = ghostState.prev[s.i] ?? [];
|
|
281
|
+
const next = ghostState.next[s.i] ?? [];
|
|
282
|
+
return next.flatMap((v, idx) => {
|
|
283
|
+
const before = prev[idx] ?? 0;
|
|
284
|
+
if (!significantDelta(before, v))
|
|
285
|
+
return [];
|
|
286
|
+
const kind = deltaKind(before, v);
|
|
287
|
+
const y = s.points[idx]?.y ?? 0;
|
|
288
|
+
return [
|
|
289
|
+
_jsx("text", { className: "kpt-chart__chip", "data-kind": kind, x: xAt(idx), y: kind === 'up' ? y - 12 : y + 20, textAnchor: "middle", style: { ['--kpt-chart-i']: idx }, "aria-hidden": "true", children: signed(v - before) }, `${ghostState.id}:${s.key}:${idx}`),
|
|
290
|
+
];
|
|
291
|
+
});
|
|
292
|
+
})
|
|
293
|
+
: null, ripple ? (_jsx("circle", { className: "kpt-chart__ripple", cx: ripple.x, cy: ripple.y, r: 5, style: { stroke: ripple.color }, "aria-hidden": "true" }, ripple.id)) : null, _jsx("rect", { className: "kpt-chart__hover", x: plot.x - 12, y: plot.y - 12, width: plot.width + 24, height: plot.height + 24, onPointerMove: onMove, onPointerLeave: hideTip })] }));
|
|
134
294
|
})();
|
|
135
295
|
// -- Wykres słupkowy --
|
|
136
296
|
const barVm = (() => {
|
|
137
|
-
if (
|
|
297
|
+
if (view.type !== 'bar')
|
|
138
298
|
return null;
|
|
139
|
-
const orientation =
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
const
|
|
299
|
+
const orientation = view.orientation ?? 'vertical';
|
|
300
|
+
const vertical = orientation === 'vertical';
|
|
301
|
+
const active = view.series.map((s, i) => ({ s, i })).filter(({ s }) => !hidden.has(`ser:${s.name}`));
|
|
302
|
+
const plot = plotArea(width, height, vertical ? VERTICAL_INSETS : HORIZONTAL_INSETS);
|
|
303
|
+
const radius = view.borderRadius ?? 4;
|
|
304
|
+
const layout = barLayout(active.map(({ s }) => s.data), view.labels.length, plot, orientation, radius, frame.domain ? domainScale(frame.domain.target, frame.domain.min, frame.domain.max) : undefined);
|
|
143
305
|
const colors = active.map(({ s, i }) => seriesColor(s.color, i));
|
|
144
306
|
const names = active.map(({ s }) => s.name);
|
|
145
|
-
const cat =
|
|
146
|
-
? bandScale(
|
|
147
|
-
: bandScale(
|
|
148
|
-
const grid =
|
|
307
|
+
const cat = vertical
|
|
308
|
+
? bandScale(view.labels.length, plot.x, plot.x + plot.width, 0.2)
|
|
309
|
+
: bandScale(view.labels.length, plot.y, plot.y + plot.height, 0.2);
|
|
310
|
+
const grid = vertical
|
|
149
311
|
? layout.ticks.map((t) => ({ x1: plot.x, y1: t.pos, x2: plot.x + plot.width, y2: t.pos }))
|
|
150
312
|
: layout.ticks.map((t) => ({ x1: t.pos, y1: plot.y, x2: t.pos, y2: plot.y + plot.height }));
|
|
151
|
-
const valueLabels =
|
|
313
|
+
const valueLabels = vertical
|
|
152
314
|
? layout.ticks.map((t) => ({ x: plot.x - 8, y: t.pos + 3, text: formatNumber(t.value), anchor: 'end' }))
|
|
153
315
|
: layout.ticks.map((t) => ({
|
|
154
316
|
x: t.pos,
|
|
@@ -156,22 +318,66 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
156
318
|
text: formatNumber(t.value),
|
|
157
319
|
anchor: 'middle',
|
|
158
320
|
}));
|
|
159
|
-
const catLabels =
|
|
160
|
-
?
|
|
161
|
-
:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
321
|
+
const catLabels = vertical
|
|
322
|
+
? view.labels.map((l, i) => ({ x: cat.center(i), y: plot.y + plot.height + 18, text: l, anchor: 'middle' }))
|
|
323
|
+
: view.labels.map((l, i) => ({ x: plot.x - 8, y: cat.center(i) + 3, text: l, anchor: 'end' }));
|
|
324
|
+
const bars = layout.bars.map((b, order) => ({
|
|
325
|
+
b,
|
|
326
|
+
order,
|
|
327
|
+
key: `ser:${names[b.seriesIndex]}`,
|
|
328
|
+
color: colors[b.seriesIndex] ?? '',
|
|
329
|
+
name: names[b.seriesIndex] ?? '',
|
|
330
|
+
tipX: vertical ? b.rect.x + b.rect.width / 2 : b.rect.x + b.rect.width,
|
|
331
|
+
tipY: vertical ? b.rect.y : b.rect.y + b.rect.height / 2,
|
|
332
|
+
}));
|
|
333
|
+
// Słupki-duchy w tej samej skali i odcinki zmiany od końca ducha do końca bieżącego słupka.
|
|
334
|
+
const ghostBars = ghostState
|
|
335
|
+
? (() => {
|
|
336
|
+
const prevRows = active.map(({ i }) => ghostState.prev[i] ?? []);
|
|
337
|
+
const nextRows = active.map(({ i }) => ghostState.next[i] ?? []);
|
|
338
|
+
return barLayout(prevRows, view.labels.length, plot, orientation, radius, layout.scale).bars.map((g, k) => {
|
|
339
|
+
const cur = layout.bars[k] ?? g;
|
|
340
|
+
const a = barEnd(g, vertical);
|
|
341
|
+
const z = barEnd(cur, vertical);
|
|
342
|
+
const lo = Math.min(a, z);
|
|
343
|
+
const hi = Math.max(a, z);
|
|
344
|
+
return {
|
|
345
|
+
key: `${ghostState.id}:${k}`,
|
|
346
|
+
path: g.path,
|
|
347
|
+
kind: deltaKind(prevRows[g.seriesIndex]?.[g.categoryIndex] ?? 0, nextRows[g.seriesIndex]?.[g.categoryIndex] ?? 0),
|
|
348
|
+
delta: vertical
|
|
349
|
+
? { x: cur.rect.x, y: lo, width: cur.rect.width, height: hi - lo }
|
|
350
|
+
: { x: lo, y: cur.rect.y, width: hi - lo, height: cur.rect.height },
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
})()
|
|
354
|
+
: [];
|
|
355
|
+
/** Najbliższa kategoria w całym obszarze wykresu: wiersze wszystkich serii tej kategorii. */
|
|
356
|
+
const onMove = (event) => {
|
|
357
|
+
const p = svgPoint(event);
|
|
358
|
+
if (!p)
|
|
359
|
+
return;
|
|
360
|
+
const idx = nearestIndex(catLabels.map((l) => (vertical ? l.x : l.y - 3)), vertical ? p.x : p.y);
|
|
361
|
+
if (keepTip(idx))
|
|
362
|
+
return;
|
|
363
|
+
const group = bars.filter((e) => e.b.categoryIndex === idx);
|
|
364
|
+
const center = catLabels[idx];
|
|
365
|
+
if (!group.length || !center)
|
|
366
|
+
return;
|
|
367
|
+
const label = config.labels[idx] ?? '';
|
|
368
|
+
const x = vertical ? center.x : Math.max(...group.map((e) => e.tipX));
|
|
369
|
+
const y = vertical ? Math.min(...group.map((e) => e.tipY)) : center.y - 3;
|
|
370
|
+
openTip(x, y, label, group.map((e) => row(e.name, label, e.b.value, e.color)), idx);
|
|
371
|
+
};
|
|
372
|
+
return (_jsxs(_Fragment, { children: [_jsx("g", { className: "kpt-chart__grid", children: grid.map((g, i) => (_jsx("line", { className: "kpt-chart__gridline", x1: g.x1, y1: g.y1, x2: g.x2, y2: g.y2 }, i))) }), _jsx("g", { className: "kpt-chart__axis", children: [...valueLabels, ...catLabels].map((l, i) => (_jsx("text", { x: l.x, y: l.y, textAnchor: l.anchor, children: l.text }, i))) }), ghostBars.length ? (_jsx("g", { className: "kpt-chart__ghost", "aria-hidden": "true", children: ghostBars.map((g) => (_jsx("path", { className: "kpt-chart__ghost-bar", d: g.path }, g.key))) })) : null, _jsx("g", { className: "kpt-chart__bars", "data-orientation": orientation, children: bars.map((e) => (_jsx("path", { className: cn('kpt-chart__bar', dim(e.key) && 'kpt-chart__series--dim'), d: e.b.path, style: { fill: e.color, ['--kpt-chart-i']: e.order } }, `${e.key}#${e.order}`))) }), ghostBars.length ? (_jsx("g", { className: "kpt-chart__ghost", "aria-hidden": "true", children: ghostBars.map((g) => (_jsx("rect", { className: "kpt-chart__delta-bar", "data-kind": g.kind, rx: 2, ...g.delta }, g.key))) })) : null, sheen
|
|
373
|
+
? bars.map((e) => (_jsx("path", { className: "kpt-chart__sheen-bar", d: e.b.path, fill: `url(#${sheenId})`, style: { opacity: sheen.opacity * 0.7 }, "aria-hidden": "true" }, `${e.key}#${e.order}`)))
|
|
374
|
+
: null, _jsx("rect", { className: "kpt-chart__hover", x: plot.x, y: plot.y, width: plot.width, height: plot.height, onPointerMove: onMove, onPointerLeave: hideTip })] }));
|
|
169
375
|
})();
|
|
170
376
|
// -- Pie / doughnut / polar area --
|
|
171
377
|
const pieVm = (() => {
|
|
172
378
|
if (!isPieLike)
|
|
173
379
|
return null;
|
|
174
|
-
const first =
|
|
380
|
+
const first = view.series[0];
|
|
175
381
|
if (!first)
|
|
176
382
|
return null;
|
|
177
383
|
const { cx, cy, radius } = radialArea(width, height, 8);
|
|
@@ -198,7 +404,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
198
404
|
return null;
|
|
199
405
|
const { cx, cy, radius } = radialArea(width, height, 30);
|
|
200
406
|
const grid = radarGrid(config.labels.length, cx, cy, radius, 4);
|
|
201
|
-
const active =
|
|
407
|
+
const active = view.series.map((s, i) => ({ s, i })).filter(({ s }) => !hidden.has(`ser:${s.name}`));
|
|
202
408
|
let max = 1;
|
|
203
409
|
for (const { s } of active)
|
|
204
410
|
for (const v of s.data)
|
|
@@ -235,5 +441,5 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
|
|
|
235
441
|
fontWeight: config.subtitle.font?.weight,
|
|
236
442
|
paddingTop: config.subtitle.padding?.top,
|
|
237
443
|
paddingBottom: config.subtitle.padding?.bottom,
|
|
238
|
-
}, children: config.subtitle.text })) : null, _jsxs("div", { className: "kpt-chart__body", children: [_jsxs("div", { className: "kpt-chart__canvas", children: [_jsxs("svg", { className: "kpt-chart__svg", viewBox: `0 0 ${width} ${height}`, role: "img", "aria-label": accessibleName, "aria-describedby": config.description ? descId : undefined, preserveAspectRatio: "xMidYMid meet", children: [lineVm, barVm, radarVm, pieVm] }), config.description ? (_jsx("p", { className: "kpt-chart__sr", id: descId, children: config.description })) : null, _jsxs("table", { className: "kpt-chart__sr", children: [_jsxs("caption", { children: [t('chart.dataTable'), ": ", accessibleName] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: t('chart.category') }), dataTable.columns.map((col, i) => (_jsx("th", { scope: "col", children: col }, i)))] }) }), _jsx("tbody", { children: dataTable.rows.map((row, i) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: row.label }), row.values.map((value, j) => (_jsx("td", { children: value }, j)))] }, i))) })] }), _jsx(KptChartTooltip, { state: tooltip })] }), legendOpts.display ? (_jsx(KptChartLegend, { items: legendItems, position: legendOpts.position, align: legendOpts.align, pointStyle: legendOpts.pointStyle, title: legendOpts.title, onToggle: toggle, onHighlight: setHighlighted })) : null] })] }) }));
|
|
444
|
+
}, children: config.subtitle.text })) : null, _jsxs("div", { className: "kpt-chart__body", children: [_jsxs("div", { className: "kpt-chart__canvas", children: [_jsxs("svg", { className: "kpt-chart__svg", viewBox: `0 0 ${width} ${height}`, role: "img", "aria-label": accessibleName, "aria-describedby": config.description ? descId : undefined, preserveAspectRatio: "xMidYMid meet", children: [sheen ? (_jsx("defs", { children: _jsxs("linearGradient", { id: sheenId, gradientUnits: "userSpaceOnUse", x1: sheen.x1, x2: sheen.x2, y1: sheen.y1, y2: sheen.y2, children: [_jsx("stop", { offset: "0", className: "kpt-chart__sheen-stop", stopOpacity: 0 }), _jsx("stop", { offset: "0.5", className: "kpt-chart__sheen-stop", stopOpacity: 0.95 }), _jsx("stop", { offset: "1", className: "kpt-chart__sheen-stop", stopOpacity: 0 })] }) })) : null, lineVm, barVm, radarVm, pieVm] }), config.description ? (_jsx("p", { className: "kpt-chart__sr", id: descId, children: config.description })) : null, _jsxs("table", { className: "kpt-chart__sr", children: [_jsxs("caption", { children: [t('chart.dataTable'), ": ", accessibleName] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: t('chart.category') }), dataTable.columns.map((col, i) => (_jsx("th", { scope: "col", children: col }, i)))] }) }), _jsx("tbody", { children: dataTable.rows.map((row, i) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: row.label }), row.values.map((value, j) => (_jsx("td", { children: value }, j)))] }, i))) })] }), _jsx(KptChartTooltip, { state: tooltip })] }), legendOpts.display ? (_jsx(KptChartLegend, { items: legendItems, position: legendOpts.position, align: legendOpts.align, pointStyle: legendOpts.pointStyle, title: legendOpts.title, onToggle: toggle, onHighlight: setHighlighted })) : null] })] }) }));
|
|
239
445
|
}
|
package/dist/charts/llms.txt
CHANGED
|
@@ -27,13 +27,27 @@ Scales, paths, bar layout and arcs come from `@konce-pt/chart` - pure functions
|
|
|
27
27
|
coverage, shared with the Angular port. The component only turns them into SVG elements; there is
|
|
28
28
|
no DOM manipulation and no canvas.
|
|
29
29
|
|
|
30
|
-
##
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
## Hover covers the whole plot
|
|
31
|
+
Line and bar charts listen on an invisible surface over the plot: the pointer anywhere picks the nearest
|
|
32
|
+
category, the tooltip lists every visible series there and travels from category to category without
|
|
33
|
+
fading out in between. Slices and radar points react to the element itself. The bubble never leaves the
|
|
34
|
+
chart - it is clamped by its real width.
|
|
35
|
+
|
|
36
|
+
## Motion: data updates and the tooltip
|
|
37
|
+
A new `config` with the same type, categories and series names is a data update: the values travel as a
|
|
38
|
+
wave from left to right on springs, with a light sheen at the wave front. Line and bar charts keep the
|
|
39
|
+
previous state for 1.7 s - a dashed ghost, up/down segments (`--kpt-chart-delta-up` / `--kpt-chart-delta-down`)
|
|
40
|
+
and the difference above points that changed by 2% or more - and the value axis waits for the ghost to
|
|
41
|
+
fade before zooming to the new data. The tooltip, the crosshair and the category pill travel between
|
|
42
|
+
categories, the digits roll on reels and the category name slides in from the direction of travel.
|
|
43
|
+
`animation.update: false`, `animation.delta: false` and `tooltip.motion: false` switch the parts off;
|
|
44
|
+
`animation.enabled: false`, `<KptMotionProvider enabled={false}>` and the system "reduce motion" setting
|
|
45
|
+
switch off all code-driven motion. The frame loop and its numbers live in `@konce-pt/chart`
|
|
46
|
+
(`KptChartMotion`, `springStep`, `reelCells`), shared with the Angular port.
|
|
33
47
|
|
|
34
48
|
## Interaction is local state
|
|
35
|
-
Hidden series, the highlighted one, the tooltip
|
|
36
|
-
stays a plain object the caller owns, and nothing is written back into it.
|
|
49
|
+
Hidden series, the highlighted one, the tooltip, the active category and the motion frame are
|
|
50
|
+
`useState` - the config stays a plain object the caller owns, and nothing is written back into it.
|
|
37
51
|
|
|
38
52
|
## Examples
|
|
39
53
|
<KptChart config={{ type: 'line', labels: ['Jan', 'Feb'], series: [{ name: 'Sales', data: [12, 19] }] }} />
|
|
@@ -49,7 +63,8 @@ stays a plain object the caller owns, and nothing is written back into it.
|
|
|
49
63
|
/>
|
|
50
64
|
|
|
51
65
|
## Tokens
|
|
52
|
-
`--kpt-chart-{duration,stagger,delay}` for the animation;
|
|
66
|
+
`--kpt-chart-{duration,stagger,delay}` for the animation; `--kpt-chart-sheen`,
|
|
67
|
+
`--kpt-chart-delta-up` and `--kpt-chart-delta-down` for the data update; series colours come from
|
|
53
68
|
`KPT_CHART_PALETTE` or `series.color`.
|
|
54
69
|
|
|
55
70
|
## Accessibility
|
|
@@ -15,8 +15,8 @@ const warnedKeys = new Set();
|
|
|
15
15
|
* `translate` z ostrzeżeniem w buildzie deweloperskim, gdy klucza nie ma w żadnym słowniku -
|
|
16
16
|
* inaczej literówka daje po cichu sam klucz zamiast tekstu.
|
|
17
17
|
*/
|
|
18
|
-
function translateChecked(dict, key, params) {
|
|
19
|
-
const text = translate(dict, key, params);
|
|
18
|
+
function translateChecked(dict, key, params, locale) {
|
|
19
|
+
const text = translate(dict, key, params, locale);
|
|
20
20
|
if (text === key && isDevBuild() && !warnedKeys.has(key)) {
|
|
21
21
|
warnedKeys.add(key);
|
|
22
22
|
console.warn(`KptI18n: brak tłumaczenia dla klucza "${key}".`);
|
|
@@ -28,7 +28,7 @@ const FALLBACK = {
|
|
|
28
28
|
setLocale: () => { },
|
|
29
29
|
messages: KPT_MESSAGES_EN,
|
|
30
30
|
messagesFor: () => KPT_MESSAGES_EN,
|
|
31
|
-
t: (key, params) => translateChecked(KPT_MESSAGES_EN, key, params),
|
|
31
|
+
t: (key, params) => translateChecked(KPT_MESSAGES_EN, key, params, 'en'),
|
|
32
32
|
};
|
|
33
33
|
const KptI18nContext = createContext(null);
|
|
34
34
|
/**
|
|
@@ -55,7 +55,7 @@ export function KptI18nProvider({ locale = 'en', messages, children }) {
|
|
|
55
55
|
setLocale: setCurrent,
|
|
56
56
|
messages: active,
|
|
57
57
|
messagesFor,
|
|
58
|
-
t: (key, params) => translateChecked(active, key, params),
|
|
58
|
+
t: (key, params) => translateChecked(active, key, params, current),
|
|
59
59
|
};
|
|
60
60
|
}, [current, messagesFor]);
|
|
61
61
|
return _jsx(KptI18nContext.Provider, { value: value, children: children });
|
|
@@ -81,7 +81,7 @@ export function useKptMessages(overrides) {
|
|
|
81
81
|
...context,
|
|
82
82
|
locale: effectiveLocale,
|
|
83
83
|
messages: merged,
|
|
84
|
-
t: (key, params) => translateChecked(merged, key, params),
|
|
84
|
+
t: (key, params) => translateChecked(merged, key, params, effectiveLocale),
|
|
85
85
|
};
|
|
86
86
|
}, [context, locale, dictionary]);
|
|
87
87
|
}
|
package/dist/internal/overlay.js
CHANGED
|
@@ -16,6 +16,25 @@ import { useKptOverlayContainer } from "./overlay-container.js";
|
|
|
16
16
|
const overlayStack = [];
|
|
17
17
|
/** Ta sama warstwa co `--kpt-z-overlay` w tokenach - panel ma być nad treścią aplikacji. */
|
|
18
18
|
const Z_INDEX = 'var(--kpt-z-overlay, 1100)';
|
|
19
|
+
/** Odstęp panelu od krawędzi okna (px) - jak `OVERLAY_VIEWPORT_MARGIN` w porcie Angulara. */
|
|
20
|
+
const VIEWPORT_MARGIN = 8;
|
|
21
|
+
/**
|
|
22
|
+
* Lewa krawędź panelu: wyrównanie z `align`, a gdy panel wyszedłby poza okno - druga krawędź
|
|
23
|
+
* wyzwalacza (dzwonek w prawym rogu paska otwiera się w lewo). Gdy nie mieści się żadna,
|
|
24
|
+
* panel jest dosuwany do okna z marginesem - jak `cdkConnectedOverlayPush` w porcie Angulara.
|
|
25
|
+
*/
|
|
26
|
+
function panelLeft(rect, panelWidth, align) {
|
|
27
|
+
const viewport = document.documentElement.clientWidth;
|
|
28
|
+
const fits = (left) => left >= VIEWPORT_MARGIN && left + panelWidth <= viewport - VIEWPORT_MARGIN;
|
|
29
|
+
const start = rect.left;
|
|
30
|
+
const end = rect.right - panelWidth;
|
|
31
|
+
const [preferred, other] = align === 'end' ? [end, start] : [start, end];
|
|
32
|
+
if (panelWidth === 0 || fits(preferred))
|
|
33
|
+
return preferred;
|
|
34
|
+
if (fits(other))
|
|
35
|
+
return other;
|
|
36
|
+
return Math.max(VIEWPORT_MARGIN, Math.min(preferred, viewport - panelWidth - VIEWPORT_MARGIN));
|
|
37
|
+
}
|
|
19
38
|
export function KptOverlay({ open, anchor, onClose, matchAnchorWidth = false, minAnchorWidth = false, offset = 4, align = 'start', withBackdrop = true, preferAbove = false, children, }) {
|
|
20
39
|
const container = useKptOverlayContainer();
|
|
21
40
|
const panelRef = useRef(null);
|
|
@@ -34,7 +53,7 @@ export function KptOverlay({ open, anchor, onClose, matchAnchorWidth = false, mi
|
|
|
34
53
|
const flip = preferAbove ? fitsAbove : panelHeight > 0 && below < panelHeight + offset && rect.top > below;
|
|
35
54
|
setPosition({
|
|
36
55
|
top: flip ? rect.top - panelHeight - offset : rect.bottom + offset,
|
|
37
|
-
left:
|
|
56
|
+
left: panelLeft(rect, matchAnchorWidth ? rect.width : panelWidth, align),
|
|
38
57
|
...(matchAnchorWidth ? { width: rect.width } : {}),
|
|
39
58
|
...(minAnchorWidth ? { minWidth: rect.width } : {}),
|
|
40
59
|
});
|
package/dist/popover/llms.txt
CHANGED
|
@@ -15,6 +15,8 @@ Import: `import { KptPopover } from '@konce-pt/react';`
|
|
|
15
15
|
- `children`: ReactNode - the panel content
|
|
16
16
|
- `open` / `defaultOpen` / `onOpenChange` - controlled or uncontrolled
|
|
17
17
|
- `placement`: 'bottom' (default) | 'top' - the preferred side; it flips when there is no room
|
|
18
|
+
- `maxWidth`: string - the panel's maximum width, any CSS length (`'24rem'`; default `20rem`). The panel is
|
|
19
|
+
never wider than the window minus the 8 px overlay margin
|
|
18
20
|
- `className`, `ref` and every other `<span>` prop go to the host
|
|
19
21
|
|
|
20
22
|
## The panel is a portal
|
|
@@ -11,6 +11,11 @@ export interface KptPopoverProps extends Omit<ComponentPropsWithRef<'span'>, 'co
|
|
|
11
11
|
placement?: 'bottom' | 'top';
|
|
12
12
|
/** Nazwa panelu (`role="dialog"`) - okno bez nazwy czytnik ogłasza jako samo "okno dialogowe". */
|
|
13
13
|
ariaLabel?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Maksymalna szerokość panelu (dowolna długość CSS, np. `'24rem'`); brak = `20rem`.
|
|
16
|
+
* Panel i tak nie wyjdzie poza okno - ogranicza go szerokość okna minus margines nakładek.
|
|
17
|
+
*/
|
|
18
|
+
maxWidth?: string;
|
|
14
19
|
}
|
|
15
20
|
/**
|
|
16
21
|
* Popover - dowolna treść w dymku przypiętym do wyzwalacza (bogatszy niż tooltip).
|
|
@@ -28,4 +33,4 @@ export interface KptPopoverProps extends Omit<ComponentPropsWithRef<'span'>, 'co
|
|
|
28
33
|
* <div>Bogata treść popovera...</div>
|
|
29
34
|
* </KptPopover>
|
|
30
35
|
*/
|
|
31
|
-
export declare function KptPopover({ trigger, children, open, defaultOpen, onOpenChange, placement, ariaLabel, className, ...rest }: Readonly<KptPopoverProps>): import("react").JSX.Element;
|
|
36
|
+
export declare function KptPopover({ trigger, children, open, defaultOpen, onOpenChange, placement, ariaLabel, maxWidth, className, ...rest }: Readonly<KptPopoverProps>): import("react").JSX.Element;
|
package/dist/popover/popover.js
CHANGED
|
@@ -23,7 +23,7 @@ const FOCUSABLE = 'a[href], button:not(:disabled), input:not(:disabled), select,
|
|
|
23
23
|
* <div>Bogata treść popovera...</div>
|
|
24
24
|
* </KptPopover>
|
|
25
25
|
*/
|
|
26
|
-
export function KptPopover({ trigger, children, open, defaultOpen = false, onOpenChange, placement = 'bottom', ariaLabel = '', className, ...rest }) {
|
|
26
|
+
export function KptPopover({ trigger, children, open, defaultOpen = false, onOpenChange, placement = 'bottom', ariaLabel = '', maxWidth, className, ...rest }) {
|
|
27
27
|
const triggerRef = useRef(null);
|
|
28
28
|
/** Kotwica panelu - w stanie, bo czyta ją render; ref zostaje dla handlerów. */
|
|
29
29
|
const [anchor, setAnchor] = useState(null);
|
|
@@ -65,5 +65,5 @@ export function KptPopover({ trigger, children, open, defaultOpen = false, onOpe
|
|
|
65
65
|
findControl()?.focus();
|
|
66
66
|
setOpen(false);
|
|
67
67
|
}, [setOpen]);
|
|
68
|
-
return (_jsxs("span", { className: cn('kpt-popover-host', className), ...rest, children: [_jsx("span", { className: "kpt-popover__trigger", ref: setTrigger, onClick: () => setOpen(!isOpen), children: trigger }), _jsx(KptOverlay, { open: isOpen, anchor: anchor, onClose: close, offset: 6, preferAbove: placement === 'top', children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-popover__panel", role: "dialog", "aria-label": ariaLabel || undefined, children: children }) })] }));
|
|
68
|
+
return (_jsxs("span", { className: cn('kpt-popover-host', className), ...rest, children: [_jsx("span", { className: "kpt-popover__trigger", ref: setTrigger, onClick: () => setOpen(!isOpen), children: trigger }), _jsx(KptOverlay, { open: isOpen, anchor: anchor, onClose: close, offset: 6, preferAbove: placement === 'top', children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-popover__panel", role: "dialog", "aria-label": ariaLabel || undefined, style: maxWidth ? { '--_max-width': maxWidth } : undefined, children: children }) })] }));
|
|
69
69
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@konce-pt/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Open-source React 19 component library on the Koncept UI design system - the same kpt-* CSS layer and --kpt-* tokens as the Angular port. Zero runtime dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "konce.pt",
|
|
@@ -65,19 +65,19 @@
|
|
|
65
65
|
"leaflet": "^1.9.0"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@konce-pt/backdrop": "0.
|
|
69
|
-
"@konce-pt/
|
|
70
|
-
"@konce-pt/
|
|
71
|
-
"@konce-pt/mention": "0.
|
|
72
|
-
"@konce-pt/
|
|
73
|
-
"@konce-pt/roadmap": "0.
|
|
74
|
-
"@konce-pt/
|
|
75
|
-
"@konce-pt/
|
|
76
|
-
"@konce-pt/
|
|
77
|
-
"@konce-pt/
|
|
78
|
-
"@konce-pt/
|
|
79
|
-
"@konce-pt/
|
|
80
|
-
"@konce-pt/map": "0.
|
|
68
|
+
"@konce-pt/backdrop": "0.16.1",
|
|
69
|
+
"@konce-pt/datetime": "0.16.1",
|
|
70
|
+
"@konce-pt/chart": "0.16.1",
|
|
71
|
+
"@konce-pt/mention": "0.16.1",
|
|
72
|
+
"@konce-pt/icons": "0.16.1",
|
|
73
|
+
"@konce-pt/roadmap": "0.16.1",
|
|
74
|
+
"@konce-pt/chat": "0.16.1",
|
|
75
|
+
"@konce-pt/tokens": "0.16.1",
|
|
76
|
+
"@konce-pt/i18n": "0.16.1",
|
|
77
|
+
"@konce-pt/speech": "0.16.1",
|
|
78
|
+
"@konce-pt/motion": "0.16.1",
|
|
79
|
+
"@konce-pt/table": "0.16.1",
|
|
80
|
+
"@konce-pt/map": "0.16.1"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@types/react": "^19.2.0",
|