@sentropic/design-system-react 0.7.0 → 0.9.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/dist/Autosave.d.ts +3 -0
- package/dist/Autosave.d.ts.map +1 -0
- package/dist/Autosave.js +2 -0
- package/dist/Autosave.js.map +1 -0
- package/dist/Calendar.d.ts +3 -0
- package/dist/Calendar.d.ts.map +1 -0
- package/dist/Calendar.js +2 -0
- package/dist/Calendar.js.map +1 -0
- package/dist/Rating.d.ts +3 -0
- package/dist/Rating.d.ts.map +1 -0
- package/dist/Rating.js +2 -0
- package/dist/Rating.js.map +1 -0
- package/dist/SlideIndicator.d.ts +3 -0
- package/dist/SlideIndicator.d.ts.map +1 -0
- package/dist/SlideIndicator.js +2 -0
- package/dist/SlideIndicator.js.map +1 -0
- package/dist/TimePicker.d.ts +3 -0
- package/dist/TimePicker.d.ts.map +1 -0
- package/dist/TimePicker.js +2 -0
- package/dist/TimePicker.js.map +1 -0
- package/dist/catalog.d.ts +117 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +445 -12
- package/dist/catalog.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/styles.css +449 -0
- package/package.json +1 -1
package/dist/catalog.js
CHANGED
|
@@ -518,7 +518,7 @@ function forceGraphMulberry32(seed) {
|
|
|
518
518
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
519
519
|
};
|
|
520
520
|
}
|
|
521
|
-
function runForceGraphSimulation(ns, es, w, h, ticks, nodeRadius) {
|
|
521
|
+
function runForceGraphSimulation(ns, es, w, h, ticks, nodeRadius, repulsionFactor) {
|
|
522
522
|
const cx = w / 2;
|
|
523
523
|
const cy = h / 2;
|
|
524
524
|
const rand = forceGraphMulberry32(ns.length * 2654435761 + es.length);
|
|
@@ -543,7 +543,11 @@ function runForceGraphSimulation(ns, es, w, h, ticks, nodeRadius) {
|
|
|
543
543
|
.filter((l) => l.s !== undefined && l.t !== undefined);
|
|
544
544
|
const area = w * h;
|
|
545
545
|
const k = Math.sqrt(area / Math.max(ns.length, 1)); // ideal node distance
|
|
546
|
-
|
|
546
|
+
// Clamp the caller-supplied factor so extreme values can't explode or
|
|
547
|
+
// collapse the layout. >1 spreads nodes out, <1 packs them tighter; the
|
|
548
|
+
// fit-to-content viewBox is recomputed afterwards so spacing just fills space.
|
|
549
|
+
const clampedRepulsion = Math.min(Math.max(repulsionFactor, 0.1), 10);
|
|
550
|
+
const repulsion = k * k * 0.9 * clampedRepulsion;
|
|
547
551
|
const restLength = k * 0.8;
|
|
548
552
|
const springK = 0.04;
|
|
549
553
|
const gravity = 0.012;
|
|
@@ -626,7 +630,7 @@ function runForceGraphSimulation(ns, es, w, h, ticks, nodeRadius) {
|
|
|
626
630
|
const FORCE_GRAPH_CURVE_FACTOR = 0.5;
|
|
627
631
|
// Fit-to-content margin (per side, fraction of the content box).
|
|
628
632
|
const FORCE_GRAPH_CONTENT_MARGIN = 0.08;
|
|
629
|
-
export function ForceGraph({ nodes, edges, label, width = 480, height = 360, nodeRadius = 7, showLabels = true, iterations = 300, selectedIds = [], focusId = null, onSelect, onOpenEntity, onEdgeHover, legend, edgeCurve = 0.15, className, ...rest }) {
|
|
633
|
+
export function ForceGraph({ nodes, edges, label, width = 480, height = 360, nodeRadius = 7, showLabels = true, iterations = 300, selectedIds = [], focusId = null, onSelect, onOpenEntity, onEdgeHover, legend, edgeCurve = 0.15, repulsion = 1, onNodeHover, className, ...rest }) {
|
|
630
634
|
// SSR-safe reduced-motion check (window may be undefined during SSR/tests).
|
|
631
635
|
const prefersReducedMotion = typeof window !== "undefined" &&
|
|
632
636
|
typeof window.matchMedia === "function" &&
|
|
@@ -638,8 +642,8 @@ export function ForceGraph({ nodes, edges, label, width = 480, height = 360, nod
|
|
|
638
642
|
// honouring the motion preference (no rAF loop, no jitter).
|
|
639
643
|
const layout = React.useMemo(() => {
|
|
640
644
|
const ticks = Math.max(1, Math.round(iterations));
|
|
641
|
-
return runForceGraphSimulation(nodes, edges, width, height, ticks, nodeRadius);
|
|
642
|
-
}, [nodes, edges, width, height, iterations, nodeRadius]);
|
|
645
|
+
return runForceGraphSimulation(nodes, edges, width, height, ticks, nodeRadius, repulsion);
|
|
646
|
+
}, [nodes, edges, width, height, iterations, nodeRadius, repulsion]);
|
|
643
647
|
const positionedNodes = React.useMemo(() => nodes.map((n, i) => {
|
|
644
648
|
const p = layout.get(n.id) ?? { x: width / 2, y: height / 2 };
|
|
645
649
|
const r = nodeRadius * Math.sqrt(Math.max(n.weight ?? 1, 0.25));
|
|
@@ -794,6 +798,38 @@ export function ForceGraph({ nodes, edges, label, width = 480, height = 360, nod
|
|
|
794
798
|
const tgtActive = selectedSet.has(e.target) || focusId === e.target;
|
|
795
799
|
return !(srcActive || tgtActive);
|
|
796
800
|
}
|
|
801
|
+
// ---------------------------------------------------------------------------
|
|
802
|
+
// Hover-connexe (demand 7): hovering a node fades the rest of the graph the
|
|
803
|
+
// same way selection does — the hovered node and its direct neighbours stay
|
|
804
|
+
// full, every other node dims, and only edges incident to the hovered node
|
|
805
|
+
// keep their opacity. Composes with selection (predicates OR'd together).
|
|
806
|
+
// ---------------------------------------------------------------------------
|
|
807
|
+
const hoveredNodeId = hoveredNodeIndex !== null ? (positionedNodes[hoveredNodeIndex]?.node.id ?? null) : null;
|
|
808
|
+
const hoverActiveSet = React.useMemo(() => {
|
|
809
|
+
const set = new Set();
|
|
810
|
+
if (hoveredNodeId == null)
|
|
811
|
+
return set;
|
|
812
|
+
set.add(hoveredNodeId);
|
|
813
|
+
const nb = adjacency.get(hoveredNodeId);
|
|
814
|
+
if (nb)
|
|
815
|
+
for (const n of nb)
|
|
816
|
+
set.add(n);
|
|
817
|
+
return set;
|
|
818
|
+
}, [hoveredNodeId, adjacency]);
|
|
819
|
+
// A node is dimmed by hover when a node is hovered and this one is neither
|
|
820
|
+
// the hovered node nor one of its direct neighbours.
|
|
821
|
+
function isHoverDimmedNode(id) {
|
|
822
|
+
if (hoveredNodeId == null)
|
|
823
|
+
return false;
|
|
824
|
+
return !hoverActiveSet.has(id);
|
|
825
|
+
}
|
|
826
|
+
// An edge is dimmed by hover when a node is hovered and the edge is not
|
|
827
|
+
// incident to it (keep only the hovered node's own edges full).
|
|
828
|
+
function isHoverDimmedEdge(e) {
|
|
829
|
+
if (hoveredNodeId == null)
|
|
830
|
+
return false;
|
|
831
|
+
return e.source !== hoveredNodeId && e.target !== hoveredNodeId;
|
|
832
|
+
}
|
|
797
833
|
// Keyboard handler for a node element: Space/Enter → onSelect, Enter → onOpenEntity.
|
|
798
834
|
function handleNodeKeydown(id, e) {
|
|
799
835
|
if (e.key === "Enter" || e.key === " ") {
|
|
@@ -893,7 +929,8 @@ export function ForceGraph({ nodes, edges, label, width = 480, height = 360, nod
|
|
|
893
929
|
onEdgeHover?.(e.edge);
|
|
894
930
|
};
|
|
895
931
|
const onHitLeave = () => setHoveredEdgeIndex(null);
|
|
896
|
-
const edgeClass = classNames("st-forceGraph__edge", e.edge.weak && "st-forceGraph__edge--weak", e.edge.emphasis && "st-forceGraph__edge--emphasis", hoveredEdgeIndex === e.i && "st-forceGraph__edge--hovered", isEdgeSelectionDimmed(e.edge)
|
|
932
|
+
const edgeClass = classNames("st-forceGraph__edge", e.edge.weak && "st-forceGraph__edge--weak", e.edge.emphasis && "st-forceGraph__edge--emphasis", hoveredEdgeIndex === e.i && "st-forceGraph__edge--hovered", (isEdgeSelectionDimmed(e.edge) || isHoverDimmedEdge(e.edge)) &&
|
|
933
|
+
"st-forceGraph__edge--dim");
|
|
897
934
|
return (_jsxs(React.Fragment, { children: [e.path ? (_jsx("path", { className: "st-forceGraph__edgeHit", role: "presentation", d: e.path, fill: "none", onMouseEnter: onHitEnter, onMouseLeave: onHitLeave })) : (_jsx("line", { className: "st-forceGraph__edgeHit", role: "presentation", x1: e.x1, y1: e.y1, x2: e.x2, y2: e.y2, onMouseEnter: onHitEnter, onMouseLeave: onHitLeave })), e.path ? (_jsx("path", { className: edgeClass, d: e.path, fill: "none", strokeDasharray: e.dashArray ?? undefined, strokeWidth: e.strokeWidth ?? undefined, pointerEvents: "none" })) : (_jsx("line", { className: edgeClass, x1: e.x1, y1: e.y1, x2: e.x2, y2: e.y2, strokeDasharray: e.dashArray ?? undefined, strokeWidth: e.strokeWidth ?? undefined, pointerEvents: "none" }))] }, e.i));
|
|
898
935
|
}) }), _jsx("g", { className: "st-forceGraph__nodes", children: positionedNodes.map((p) => {
|
|
899
936
|
const ariaLabel = `${p.title}${p.node.group !== undefined ? `: ${p.node.group}` : ""}`;
|
|
@@ -904,16 +941,27 @@ export function ForceGraph({ nodes, edges, label, width = 480, height = 360, nod
|
|
|
904
941
|
role: "button",
|
|
905
942
|
"aria-label": ariaLabel,
|
|
906
943
|
"aria-pressed": pressed,
|
|
907
|
-
onMouseEnter: () =>
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
944
|
+
onMouseEnter: () => {
|
|
945
|
+
setHoveredNodeIndex(p.i);
|
|
946
|
+
onNodeHover?.(p.node);
|
|
947
|
+
},
|
|
948
|
+
onMouseLeave: () => {
|
|
949
|
+
setHoveredNodeIndex(null);
|
|
950
|
+
onNodeHover?.(null);
|
|
951
|
+
},
|
|
952
|
+
onFocus: () => {
|
|
953
|
+
setHoveredNodeIndex(p.i);
|
|
954
|
+
onNodeHover?.(p.node);
|
|
955
|
+
},
|
|
956
|
+
onBlur: () => {
|
|
957
|
+
setHoveredNodeIndex(null);
|
|
958
|
+
onNodeHover?.(null);
|
|
959
|
+
},
|
|
911
960
|
onClick: () => onSelect?.(p.node.id),
|
|
912
961
|
onDoubleClick: () => onOpenEntity?.(p.node.id),
|
|
913
962
|
onKeyDown: (e) => handleNodeKeydown(p.node.id, e),
|
|
914
963
|
};
|
|
915
|
-
return (_jsxs("g", { className: classNames("st-forceGraph__node", `st-forceGraph__node--${p.tone}`, ((
|
|
916
|
-
isSelectionDimmed(p.node.id)) &&
|
|
964
|
+
return (_jsxs("g", { className: classNames("st-forceGraph__node", `st-forceGraph__node--${p.tone}`, (isHoverDimmedNode(p.node.id) || isSelectionDimmed(p.node.id)) &&
|
|
917
965
|
"st-forceGraph__node--dim", pressed && "st-forceGraph__node--selected", focusId === p.node.id && "st-forceGraph__node--focus"), transform: `translate(${p.x} ${p.y})`, children: [p.shapePath ? (_jsx("path", { ...shapeProps, d: p.shapePath })) : (_jsx("circle", { ...shapeProps, r: p.r })), showLabels ? (_jsx("text", { className: "st-forceGraph__label", x: p.r + 3, y: 0, dominantBaseline: "middle", children: p.title })) : null] }, p.node.id));
|
|
918
966
|
}) })] }), hoveredNode ? (_jsxs("div", { className: "st-forceGraph__tooltip", role: "presentation", style: {
|
|
919
967
|
left: `${((hoveredNode.x - vbX) / vbW) * 100}%`,
|
|
@@ -1268,4 +1316,389 @@ export function TreeView({ nodes, selectedId, expandedIds, defaultExpandedIds =
|
|
|
1268
1316
|
export function UnorderedList({ items, className, ...rest }) {
|
|
1269
1317
|
return (_jsx("ul", { ...rest, className: classNames("st-unorderedList", className), children: items.map((item, index) => renderListItem(item, index, false)) }));
|
|
1270
1318
|
}
|
|
1319
|
+
function StarIcon({ size, fill }) {
|
|
1320
|
+
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: fill, stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("polygon", { points: "12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" }) }));
|
|
1321
|
+
}
|
|
1322
|
+
function StarHalfIcon({ size }) {
|
|
1323
|
+
return (_jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2v15.77z", fill: "currentColor" }), _jsx("path", { d: "M12 2v15.77l6.18 3.25L17 14.14 22 9.27l-6.91-1.01L12 2z" })] }));
|
|
1324
|
+
}
|
|
1325
|
+
export function Rating({ value, max = 5, onChange, readonly = false, allowHalf = false, size = "md", name, label, className, ...rest }) {
|
|
1326
|
+
const [current, setCurrent] = useControlled(value, value ?? 0, onChange);
|
|
1327
|
+
const iconSize = size === "sm" ? 16 : size === "lg" ? 28 : 22;
|
|
1328
|
+
const stars = Array.from({ length: max }, (_, i) => i + 1);
|
|
1329
|
+
// L'étoile « focusable » (tabindex 0) suit la valeur ; à 0 c'est la première.
|
|
1330
|
+
const focusedStar = current > 0 ? Math.ceil(current) : 1;
|
|
1331
|
+
function fill(star) {
|
|
1332
|
+
if (current >= star)
|
|
1333
|
+
return "full";
|
|
1334
|
+
if (allowHalf && current >= star - 0.5)
|
|
1335
|
+
return "half";
|
|
1336
|
+
return "empty";
|
|
1337
|
+
}
|
|
1338
|
+
function commit(next) {
|
|
1339
|
+
if (readonly)
|
|
1340
|
+
return;
|
|
1341
|
+
setCurrent(Math.max(0, Math.min(max, next)));
|
|
1342
|
+
}
|
|
1343
|
+
function onStarClick(event, star) {
|
|
1344
|
+
if (readonly)
|
|
1345
|
+
return;
|
|
1346
|
+
let next = star;
|
|
1347
|
+
if (allowHalf) {
|
|
1348
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
1349
|
+
const isLeftHalf = event.clientX - rect.left < rect.width / 2;
|
|
1350
|
+
next = isLeftHalf ? star - 0.5 : star;
|
|
1351
|
+
}
|
|
1352
|
+
// Re-cliquer la valeur déjà sélectionnée remet à zéro.
|
|
1353
|
+
if (next === current) {
|
|
1354
|
+
commit(0);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
commit(next);
|
|
1358
|
+
}
|
|
1359
|
+
function onKeyDown(event) {
|
|
1360
|
+
if (readonly)
|
|
1361
|
+
return;
|
|
1362
|
+
const step = allowHalf ? 0.5 : 1;
|
|
1363
|
+
let handled = true;
|
|
1364
|
+
switch (event.key) {
|
|
1365
|
+
case "ArrowRight":
|
|
1366
|
+
case "ArrowUp":
|
|
1367
|
+
commit(Math.min(max, current + step));
|
|
1368
|
+
break;
|
|
1369
|
+
case "ArrowLeft":
|
|
1370
|
+
case "ArrowDown":
|
|
1371
|
+
commit(Math.max(0, current - step));
|
|
1372
|
+
break;
|
|
1373
|
+
case "Home":
|
|
1374
|
+
commit(0);
|
|
1375
|
+
break;
|
|
1376
|
+
case "End":
|
|
1377
|
+
commit(max);
|
|
1378
|
+
break;
|
|
1379
|
+
default:
|
|
1380
|
+
handled = false;
|
|
1381
|
+
}
|
|
1382
|
+
if (handled)
|
|
1383
|
+
event.preventDefault();
|
|
1384
|
+
}
|
|
1385
|
+
return (_jsx("div", { ...rest, className: classNames("st-rating", `st-rating--${size}`, readonly && "st-rating--readonly", className), role: "radiogroup", "aria-label": label, "aria-readonly": readonly ? "true" : undefined, children: stars.map((star) => {
|
|
1386
|
+
const state = fill(star);
|
|
1387
|
+
return (_jsx("button", { type: "button", className: classNames("st-rating__star", state === "full" && "st-rating__star--full", state === "half" && "st-rating__star--half"), role: "radio", name: name, "aria-checked": Math.ceil(current) === star ? "true" : "false", "aria-label": `${star} / ${max}`, tabIndex: !readonly && star === focusedStar ? 0 : -1, disabled: readonly, onClick: (event) => onStarClick(event, star), onKeyDown: onKeyDown, children: state === "half" ? (_jsx(StarHalfIcon, { size: iconSize })) : (_jsx(StarIcon, { size: iconSize, fill: state === "full" ? "currentColor" : "none" })) }, star));
|
|
1388
|
+
}) }));
|
|
1389
|
+
}
|
|
1390
|
+
function timeToMinutes(hhmm) {
|
|
1391
|
+
if (!hhmm)
|
|
1392
|
+
return null;
|
|
1393
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(hhmm);
|
|
1394
|
+
if (!match)
|
|
1395
|
+
return null;
|
|
1396
|
+
const h = Number(match[1]);
|
|
1397
|
+
const m = Number(match[2]);
|
|
1398
|
+
if (h < 0 || h > 23 || m < 0 || m > 59)
|
|
1399
|
+
return null;
|
|
1400
|
+
return h * 60 + m;
|
|
1401
|
+
}
|
|
1402
|
+
function timeFromMinutes(total) {
|
|
1403
|
+
const h = Math.floor(total / 60);
|
|
1404
|
+
const m = total % 60;
|
|
1405
|
+
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
|
|
1406
|
+
}
|
|
1407
|
+
export function TimePicker({ value, onChange, step = 15, min, max, format = "24", size = "md", disabled = false, label, className, id, ...rest }) {
|
|
1408
|
+
const reactId = React.useId();
|
|
1409
|
+
const fieldId = id ?? `st-timepicker-${reactId}`;
|
|
1410
|
+
const listId = `${fieldId}-list`;
|
|
1411
|
+
const hostRef = React.useRef(null);
|
|
1412
|
+
const [open, setOpen] = React.useState(false);
|
|
1413
|
+
const [current, setCurrent] = useControlled(value, value ?? "", onChange);
|
|
1414
|
+
function display(hhmm) {
|
|
1415
|
+
if (format === "24")
|
|
1416
|
+
return hhmm;
|
|
1417
|
+
const total = timeToMinutes(hhmm);
|
|
1418
|
+
if (total === null)
|
|
1419
|
+
return hhmm;
|
|
1420
|
+
const h24 = Math.floor(total / 60);
|
|
1421
|
+
const m = total % 60;
|
|
1422
|
+
const period = h24 < 12 ? "AM" : "PM";
|
|
1423
|
+
let h12 = h24 % 12;
|
|
1424
|
+
if (h12 === 0)
|
|
1425
|
+
h12 = 12;
|
|
1426
|
+
return `${String(h12).padStart(2, "0")}:${String(m).padStart(2, "0")} ${period}`;
|
|
1427
|
+
}
|
|
1428
|
+
const slots = React.useMemo(() => {
|
|
1429
|
+
const safeStep = step > 0 ? step : 15;
|
|
1430
|
+
const lower = timeToMinutes(min) ?? 0;
|
|
1431
|
+
const upper = timeToMinutes(max) ?? 23 * 60 + 59;
|
|
1432
|
+
const result = [];
|
|
1433
|
+
for (let t = lower; t <= upper; t += safeStep) {
|
|
1434
|
+
result.push(timeFromMinutes(t));
|
|
1435
|
+
}
|
|
1436
|
+
return result;
|
|
1437
|
+
}, [step, min, max]);
|
|
1438
|
+
const displayValue = current ? display(current) : "";
|
|
1439
|
+
function toggleOpen() {
|
|
1440
|
+
if (disabled)
|
|
1441
|
+
return;
|
|
1442
|
+
setOpen((prev) => !prev);
|
|
1443
|
+
}
|
|
1444
|
+
function pick(slot) {
|
|
1445
|
+
setCurrent(slot);
|
|
1446
|
+
setOpen(false);
|
|
1447
|
+
}
|
|
1448
|
+
useOutsideMouseDown(open, hostRef, () => setOpen(false));
|
|
1449
|
+
return (_jsxs("div", { className: classNames("st-field", className), ref: hostRef, ...rest, children: [_jsxs("div", { className: "st-field__control", children: [label ? (_jsx("label", { className: "st-field__label", htmlFor: fieldId, children: label })) : null, _jsxs("span", { className: classNames("st-timepicker", `st-timepicker--${size}`), children: [_jsx("input", { id: fieldId, type: "text", readOnly: true, className: "st-timepicker__control", value: displayValue, placeholder: format === "24" ? "HH:mm" : "hh:mm AM", disabled: disabled, role: "combobox", "aria-haspopup": "listbox", "aria-controls": listId, "aria-expanded": open ? "true" : "false", onClick: toggleOpen }), _jsx("button", { type: "button", className: "st-timepicker__trigger", "aria-label": "Ouvrir la liste des horaires", "aria-haspopup": "listbox", "aria-expanded": open ? "true" : "false", disabled: disabled, onClick: toggleOpen, children: _jsx(ClockIcon, { size: 16 }) })] })] }), open ? (_jsx("ul", { id: listId, className: "st-timepicker__list", role: "listbox", "aria-label": label ?? "Horaires", tabIndex: -1, onKeyDown: (event) => {
|
|
1450
|
+
if (event.key === "Escape") {
|
|
1451
|
+
event.preventDefault();
|
|
1452
|
+
setOpen(false);
|
|
1453
|
+
}
|
|
1454
|
+
}, children: slots.map((slot) => (_jsx("li", { role: "presentation", children: _jsx("button", { type: "button", className: classNames("st-timepicker__option", slot === current && "st-timepicker__option--selected"), role: "option", "aria-selected": slot === current ? "true" : "false", onClick: () => pick(slot), children: display(slot) }) }, slot))) })) : null] }));
|
|
1455
|
+
}
|
|
1456
|
+
function ClockIcon({ size }) {
|
|
1457
|
+
return (_jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("polyline", { points: "12 6 12 12 16 14" })] }));
|
|
1458
|
+
}
|
|
1459
|
+
function calStartOfDay(date) {
|
|
1460
|
+
const d = new Date(date);
|
|
1461
|
+
d.setHours(0, 0, 0, 0);
|
|
1462
|
+
return d;
|
|
1463
|
+
}
|
|
1464
|
+
function calToISO(date) {
|
|
1465
|
+
const y = date.getFullYear();
|
|
1466
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
1467
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
1468
|
+
return `${y}-${m}-${d}`;
|
|
1469
|
+
}
|
|
1470
|
+
function calParseISO(iso) {
|
|
1471
|
+
if (!iso)
|
|
1472
|
+
return null;
|
|
1473
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
|
1474
|
+
if (!match)
|
|
1475
|
+
return null;
|
|
1476
|
+
const d = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
1477
|
+
return Number.isNaN(d.getTime()) ? null : calStartOfDay(d);
|
|
1478
|
+
}
|
|
1479
|
+
function calIsSameDay(a, b) {
|
|
1480
|
+
if (!a || !b)
|
|
1481
|
+
return false;
|
|
1482
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
1483
|
+
}
|
|
1484
|
+
export function Calendar({ value, onChange, min, max, range = false, weekStartsOn = 1, locale = "fr-FR", month, className, previousMonthLabel, nextMonthLabel, ...rest }) {
|
|
1485
|
+
const [activeValue, setCurrent] = useControlled(value, value ?? null, onChange);
|
|
1486
|
+
const isFr = (locale ?? "fr-FR").toLowerCase().startsWith("fr");
|
|
1487
|
+
const resolvedPrevLabel = previousMonthLabel ?? (isFr ? "Mois précédent" : "Previous month");
|
|
1488
|
+
const resolvedNextLabel = nextMonthLabel ?? (isFr ? "Mois suivant" : "Next month");
|
|
1489
|
+
const monthFormatter = React.useMemo(() => new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }), [locale]);
|
|
1490
|
+
const weekdayFormatter = React.useMemo(() => new Intl.DateTimeFormat(locale, { weekday: "short" }), [locale]);
|
|
1491
|
+
const cellFormatter = React.useMemo(() => new Intl.DateTimeFormat(locale, { day: "numeric", month: "long", year: "numeric" }), [locale]);
|
|
1492
|
+
const single = range ? null : calParseISO(activeValue);
|
|
1493
|
+
const rangeStart = range && Array.isArray(activeValue) ? calParseISO(activeValue[0]) : null;
|
|
1494
|
+
const rangeEnd = range && Array.isArray(activeValue) ? calParseISO(activeValue[1]) : null;
|
|
1495
|
+
function pickInitialMonth() {
|
|
1496
|
+
const parsed = calParseISO(month ? `${month}-01` : undefined);
|
|
1497
|
+
if (parsed)
|
|
1498
|
+
return parsed;
|
|
1499
|
+
if (!range && single)
|
|
1500
|
+
return single;
|
|
1501
|
+
if (range && rangeStart)
|
|
1502
|
+
return rangeStart;
|
|
1503
|
+
return calStartOfDay(new Date());
|
|
1504
|
+
}
|
|
1505
|
+
const initial = React.useRef(pickInitialMonth());
|
|
1506
|
+
const [viewYear, setViewYear] = React.useState(initial.current.getFullYear());
|
|
1507
|
+
const [viewMonth, setViewMonth] = React.useState(initial.current.getMonth());
|
|
1508
|
+
// Resynchronise le mois affiché lorsque la prop `month` change.
|
|
1509
|
+
React.useEffect(() => {
|
|
1510
|
+
const parsed = calParseISO(month ? `${month}-01` : undefined);
|
|
1511
|
+
if (parsed) {
|
|
1512
|
+
setViewYear(parsed.getFullYear());
|
|
1513
|
+
setViewMonth(parsed.getMonth());
|
|
1514
|
+
}
|
|
1515
|
+
}, [month]);
|
|
1516
|
+
const today = React.useMemo(() => calStartOfDay(new Date()), []);
|
|
1517
|
+
const weekdayLabels = React.useMemo(() => {
|
|
1518
|
+
// 2024-01-07 est un dimanche : on énumère puis on tourne selon weekStartsOn.
|
|
1519
|
+
const sample = new Date(Date.UTC(2024, 0, 7));
|
|
1520
|
+
const labels = [];
|
|
1521
|
+
for (let i = 0; i < 7; i++) {
|
|
1522
|
+
const d = new Date(sample);
|
|
1523
|
+
d.setUTCDate(sample.getUTCDate() + i);
|
|
1524
|
+
labels.push(weekdayFormatter.format(d));
|
|
1525
|
+
}
|
|
1526
|
+
return [...labels.slice(weekStartsOn), ...labels.slice(0, weekStartsOn)];
|
|
1527
|
+
}, [weekdayFormatter, weekStartsOn]);
|
|
1528
|
+
const grid = React.useMemo(() => {
|
|
1529
|
+
const first = new Date(viewYear, viewMonth, 1);
|
|
1530
|
+
const firstDayIdx = first.getDay();
|
|
1531
|
+
const offset = (firstDayIdx - weekStartsOn + 7) % 7;
|
|
1532
|
+
const start = new Date(viewYear, viewMonth, 1 - offset);
|
|
1533
|
+
const cells = [];
|
|
1534
|
+
for (let i = 0; i < 42; i++) {
|
|
1535
|
+
const d = new Date(start);
|
|
1536
|
+
d.setDate(start.getDate() + i);
|
|
1537
|
+
cells.push({ date: calStartOfDay(d), inMonth: d.getMonth() === viewMonth });
|
|
1538
|
+
}
|
|
1539
|
+
return cells;
|
|
1540
|
+
}, [viewYear, viewMonth, weekStartsOn]);
|
|
1541
|
+
const minDate = calParseISO(min);
|
|
1542
|
+
const maxDate = calParseISO(max);
|
|
1543
|
+
function isOutOfBounds(date) {
|
|
1544
|
+
const d = calStartOfDay(date).getTime();
|
|
1545
|
+
if (minDate && d < minDate.getTime())
|
|
1546
|
+
return true;
|
|
1547
|
+
if (maxDate && d > maxDate.getTime())
|
|
1548
|
+
return true;
|
|
1549
|
+
return false;
|
|
1550
|
+
}
|
|
1551
|
+
function isSelected(date) {
|
|
1552
|
+
if (!range)
|
|
1553
|
+
return calIsSameDay(single, date);
|
|
1554
|
+
return calIsSameDay(rangeStart, date) || calIsSameDay(rangeEnd, date);
|
|
1555
|
+
}
|
|
1556
|
+
function isInRange(date) {
|
|
1557
|
+
if (!range || !rangeStart || !rangeEnd)
|
|
1558
|
+
return false;
|
|
1559
|
+
const d = calStartOfDay(date).getTime();
|
|
1560
|
+
return d > rangeStart.getTime() && d < rangeEnd.getTime();
|
|
1561
|
+
}
|
|
1562
|
+
function previousMonth() {
|
|
1563
|
+
if (viewMonth === 0) {
|
|
1564
|
+
setViewMonth(11);
|
|
1565
|
+
setViewYear((y) => y - 1);
|
|
1566
|
+
}
|
|
1567
|
+
else {
|
|
1568
|
+
setViewMonth((m) => m - 1);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
function nextMonth() {
|
|
1572
|
+
if (viewMonth === 11) {
|
|
1573
|
+
setViewMonth(0);
|
|
1574
|
+
setViewYear((y) => y + 1);
|
|
1575
|
+
}
|
|
1576
|
+
else {
|
|
1577
|
+
setViewMonth((m) => m + 1);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
function pickDate(date) {
|
|
1581
|
+
if (isOutOfBounds(date))
|
|
1582
|
+
return;
|
|
1583
|
+
const picked = calStartOfDay(date);
|
|
1584
|
+
const iso = calToISO(picked);
|
|
1585
|
+
if (!range) {
|
|
1586
|
+
setCurrent(iso);
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
// Mode plage : (re)démarrage si pas de début, ou si plage déjà complète,
|
|
1590
|
+
// ou si la date est antérieure au début courant.
|
|
1591
|
+
if (!rangeStart || (rangeStart && rangeEnd) || picked.getTime() < rangeStart.getTime()) {
|
|
1592
|
+
setCurrent([iso, null]);
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
setCurrent([calToISO(rangeStart), iso]);
|
|
1596
|
+
}
|
|
1597
|
+
const monthLabel = monthFormatter.format(new Date(viewYear, viewMonth, 1));
|
|
1598
|
+
return (_jsxs("div", { className: classNames("st-calendar", className), ...rest, children: [_jsxs("div", { className: "st-calendar__nav", children: [_jsx("button", { type: "button", className: "st-calendar__navBtn", "aria-label": resolvedPrevLabel, onClick: previousMonth, children: _jsx(ChevronLeftIcon, { size: 18 }) }), _jsx("span", { className: "st-calendar__monthLabel", "aria-live": "polite", children: monthLabel }), _jsx("button", { type: "button", className: "st-calendar__navBtn", "aria-label": resolvedNextLabel, onClick: nextMonth, children: _jsx(ChevronRightIcon, { size: 18 }) })] }), _jsxs("div", { className: "st-calendar__grid", role: "grid", tabIndex: -1, "aria-label": monthLabel, onKeyDown: (event) => {
|
|
1599
|
+
if (event.key === "PageUp") {
|
|
1600
|
+
event.preventDefault();
|
|
1601
|
+
previousMonth();
|
|
1602
|
+
}
|
|
1603
|
+
else if (event.key === "PageDown") {
|
|
1604
|
+
event.preventDefault();
|
|
1605
|
+
nextMonth();
|
|
1606
|
+
}
|
|
1607
|
+
}, children: [_jsx("div", { className: "st-calendar__weekdays", role: "row", children: weekdayLabels.map((wd, i) => (_jsx("span", { className: "st-calendar__weekday", role: "columnheader", children: wd }, `${wd}-${i}`))) }), _jsx("div", { className: "st-calendar__days", children: grid.map((cell, i) => {
|
|
1608
|
+
const oob = isOutOfBounds(cell.date);
|
|
1609
|
+
const selected = isSelected(cell.date);
|
|
1610
|
+
const inRange = isInRange(cell.date);
|
|
1611
|
+
const isToday = calIsSameDay(cell.date, today);
|
|
1612
|
+
return (_jsx("button", { type: "button", className: classNames("st-calendar__day", !cell.inMonth && "st-calendar__day--outside", selected && "st-calendar__day--selected", inRange && "st-calendar__day--inRange", isToday && "st-calendar__day--today"), role: "gridcell", "aria-label": cellFormatter.format(cell.date), "aria-selected": selected ? "true" : "false", "aria-current": isToday ? "date" : undefined, "aria-disabled": oob ? "true" : undefined, disabled: oob, onClick: () => pickDate(cell.date), children: cell.date.getDate() }, i));
|
|
1613
|
+
}) })] })] }));
|
|
1614
|
+
}
|
|
1615
|
+
function ChevronLeftIcon({ size }) {
|
|
1616
|
+
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("polyline", { points: "15 18 9 12 15 6" }) }));
|
|
1617
|
+
}
|
|
1618
|
+
function ChevronRightIcon({ size }) {
|
|
1619
|
+
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("polyline", { points: "9 18 15 12 9 6" }) }));
|
|
1620
|
+
}
|
|
1621
|
+
export function SlideIndicator({ count, current = 0, onChange, size = "md", variant = "dots", label = "Diapositive", className, ...rest }) {
|
|
1622
|
+
const items = Array.from({ length: Math.max(0, count) }, (_, i) => i);
|
|
1623
|
+
function select(index) {
|
|
1624
|
+
if (index < 0 || index >= count || index === current)
|
|
1625
|
+
return;
|
|
1626
|
+
onChange?.(index);
|
|
1627
|
+
}
|
|
1628
|
+
function onKeyDown(event, index) {
|
|
1629
|
+
let target = index;
|
|
1630
|
+
switch (event.key) {
|
|
1631
|
+
case "ArrowRight":
|
|
1632
|
+
case "ArrowDown":
|
|
1633
|
+
target = Math.min(count - 1, index + 1);
|
|
1634
|
+
break;
|
|
1635
|
+
case "ArrowLeft":
|
|
1636
|
+
case "ArrowUp":
|
|
1637
|
+
target = Math.max(0, index - 1);
|
|
1638
|
+
break;
|
|
1639
|
+
case "Home":
|
|
1640
|
+
target = 0;
|
|
1641
|
+
break;
|
|
1642
|
+
case "End":
|
|
1643
|
+
target = count - 1;
|
|
1644
|
+
break;
|
|
1645
|
+
default:
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
event.preventDefault();
|
|
1649
|
+
select(target);
|
|
1650
|
+
}
|
|
1651
|
+
return (_jsx("div", { ...rest, className: classNames("st-slideIndicator", `st-slideIndicator--${size}`, `st-slideIndicator--${variant}`, className), role: "tablist", "aria-label": label, children: items.map((index) => (_jsx("button", { type: "button", className: classNames("st-slideIndicator__dot", index === current && "st-slideIndicator__dot--current"), role: "tab", "aria-selected": index === current ? "true" : "false", "aria-current": index === current ? "true" : undefined, "aria-label": `${label} ${index + 1}`, tabIndex: index === current ? 0 : -1, onClick: () => select(index), onKeyDown: (event) => onKeyDown(event, index) }, index))) }));
|
|
1652
|
+
}
|
|
1653
|
+
export function Autosave({ status = "idle", lastSaved, onRetry, labels, retryLabel, locale = "fr-FR", className, ...rest }) {
|
|
1654
|
+
const isFr = (locale ?? "fr-FR").toLowerCase().startsWith("fr");
|
|
1655
|
+
const DEFAULT_LABELS = isFr
|
|
1656
|
+
? {
|
|
1657
|
+
idle: "Modifications enregistrées",
|
|
1658
|
+
saving: "Enregistrement…",
|
|
1659
|
+
saved: "Enregistré",
|
|
1660
|
+
error: "Échec de l'enregistrement",
|
|
1661
|
+
}
|
|
1662
|
+
: {
|
|
1663
|
+
idle: "All changes saved",
|
|
1664
|
+
saving: "Saving…",
|
|
1665
|
+
saved: "Saved",
|
|
1666
|
+
error: "Failed to save",
|
|
1667
|
+
};
|
|
1668
|
+
const resolvedRetryLabel = retryLabel ?? (isFr ? "Réessayer" : "Retry");
|
|
1669
|
+
const statusLabel = labels?.[status] ?? DEFAULT_LABELS[status];
|
|
1670
|
+
const role = status === "error" ? "alert" : "status";
|
|
1671
|
+
// Heure relative de la dernière sauvegarde (rendu uniquement sur idle/saved).
|
|
1672
|
+
const relativeTime = (() => {
|
|
1673
|
+
if (!lastSaved)
|
|
1674
|
+
return "";
|
|
1675
|
+
const date = lastSaved instanceof Date ? lastSaved : new Date(lastSaved);
|
|
1676
|
+
if (Number.isNaN(date.getTime()))
|
|
1677
|
+
return "";
|
|
1678
|
+
const diffMs = Date.now() - date.getTime();
|
|
1679
|
+
const diffSec = Math.round(diffMs / 1000);
|
|
1680
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
1681
|
+
if (Math.abs(diffSec) < 60)
|
|
1682
|
+
return rtf.format(-diffSec, "second");
|
|
1683
|
+
const diffMin = Math.round(diffSec / 60);
|
|
1684
|
+
if (Math.abs(diffMin) < 60)
|
|
1685
|
+
return rtf.format(-diffMin, "minute");
|
|
1686
|
+
const diffHour = Math.round(diffMin / 60);
|
|
1687
|
+
if (Math.abs(diffHour) < 24)
|
|
1688
|
+
return rtf.format(-diffHour, "hour");
|
|
1689
|
+
const diffDay = Math.round(diffHour / 24);
|
|
1690
|
+
return rtf.format(-diffDay, "day");
|
|
1691
|
+
})();
|
|
1692
|
+
const showRelative = (status === "saved" || status === "idle") && relativeTime !== "";
|
|
1693
|
+
return (_jsxs("div", { ...rest, className: classNames("st-autosave", `st-autosave--${status}`, className), role: role, "aria-live": "polite", children: [_jsx("span", { className: "st-autosave__icon", "aria-hidden": "true", children: status === "saving" ? (_jsx("span", { className: "st-autosave__spinner", children: _jsx(LoaderCircleIcon, { size: 16 }) })) : status === "saved" ? (_jsx(CircleCheckIcon, { size: 16 })) : status === "error" ? (_jsx(CircleAlertIcon, { size: 16 })) : null }), _jsx("span", { className: "st-autosave__label", children: statusLabel }), showRelative ? _jsx("span", { className: "st-autosave__time", children: relativeTime }) : null, status === "error" && onRetry ? (_jsx("button", { type: "button", className: "st-autosave__retry", onClick: () => onRetry?.(), children: resolvedRetryLabel })) : null] }));
|
|
1694
|
+
}
|
|
1695
|
+
function LoaderCircleIcon({ size }) {
|
|
1696
|
+
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) }));
|
|
1697
|
+
}
|
|
1698
|
+
function CircleCheckIcon({ size }) {
|
|
1699
|
+
return (_jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "m9 12 2 2 4-4" })] }));
|
|
1700
|
+
}
|
|
1701
|
+
function CircleAlertIcon({ size }) {
|
|
1702
|
+
return (_jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("line", { x1: "12", x2: "12", y1: "8", y2: "12" }), _jsx("line", { x1: "12", x2: "12.01", y1: "16", y2: "16" })] }));
|
|
1703
|
+
}
|
|
1271
1704
|
//# sourceMappingURL=catalog.js.map
|