@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/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
- const repulsion = k * k * 0.9;
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) && "st-forceGraph__edge--dim");
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: () => setHoveredNodeIndex(p.i),
908
- onMouseLeave: () => setHoveredNodeIndex(null),
909
- onFocus: () => setHoveredNodeIndex(p.i),
910
- onBlur: () => setHoveredNodeIndex(null),
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}`, ((hoveredNodeIndex !== null && hoveredNodeIndex !== p.i) ||
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