@roy-ui/ui 0.0.5 → 0.0.7

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/index.js CHANGED
@@ -1,14 +1,77 @@
1
1
  "use client";
2
- import { forwardRef, useState, useRef, useEffect, Children, isValidElement, cloneElement } from 'react';
3
- import './GradientButton-TX2GJRIQ.css';
2
+ import { forwardRef, useState, useRef, useEffect, Children, isValidElement, cloneElement, useMemo, useCallback } from 'react';
3
+ import './Button-XBBWB5ZT.css';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
+ import './GradientButton-TX2GJRIQ.css';
5
6
  import './Popover-LSYVKT4M.css';
6
7
  import './MadeBy-JCYGHWSD.css';
7
8
  import './TextMorph-RX2BX25F.css';
8
9
  import './TreeNav-22DY7TP5.css';
10
+ import './Table-YTEWR635.css';
11
+ import './TableSearch-UZO4ZJVE.css';
12
+ import './Pagination-LLKV7XXI.css';
13
+ import './DateRangePicker-BCP26AOC.css';
14
+ import './TimePicker-44EKHQEJ.css';
15
+ import './DataTable-TQ5OBNZF.css';
9
16
 
10
- // src/components/gradient-button/GradientButton.tsx
11
- var DefaultSpinner = () => /* @__PURE__ */ jsx("span", { className: "gradient-btn__spinner", "aria-hidden": "true", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "18", height: "18", fill: "none", children: [
17
+ // src/components/button/Button.tsx
18
+ var DefaultSpinner = () => /* @__PURE__ */ jsx("span", { className: "royui-btn__spinner", "aria-hidden": "true", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "16", height: "16", fill: "none", children: [
19
+ /* @__PURE__ */ jsx(
20
+ "circle",
21
+ {
22
+ cx: "12",
23
+ cy: "12",
24
+ r: "9",
25
+ stroke: "currentColor",
26
+ strokeOpacity: "0.3",
27
+ strokeWidth: "2.5"
28
+ }
29
+ ),
30
+ /* @__PURE__ */ jsx(
31
+ "path",
32
+ {
33
+ d: "M21 12a9 9 0 0 0-9-9",
34
+ stroke: "currentColor",
35
+ strokeWidth: "2.5",
36
+ strokeLinecap: "round"
37
+ }
38
+ )
39
+ ] }) });
40
+ var Button = forwardRef(
41
+ ({
42
+ size = "md",
43
+ fullWidth = false,
44
+ loading = false,
45
+ loadingLabel,
46
+ disabled,
47
+ className = "",
48
+ children,
49
+ type = "button",
50
+ ...rest
51
+ }, ref) => {
52
+ const classes = [
53
+ "royui-btn",
54
+ `royui-btn--${size}`,
55
+ fullWidth ? "royui-btn--full" : "",
56
+ loading ? "royui-btn--loading" : "",
57
+ className
58
+ ].filter(Boolean).join(" ");
59
+ return /* @__PURE__ */ jsx(
60
+ "button",
61
+ {
62
+ ref,
63
+ type,
64
+ disabled: disabled || loading,
65
+ className: classes,
66
+ "aria-busy": loading || void 0,
67
+ ...rest,
68
+ children: loading ? loadingLabel ?? /* @__PURE__ */ jsx(DefaultSpinner, {}) : children
69
+ }
70
+ );
71
+ }
72
+ );
73
+ Button.displayName = "Button";
74
+ var DefaultSpinner2 = () => /* @__PURE__ */ jsx("span", { className: "gradient-btn__spinner", "aria-hidden": "true", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "18", height: "18", fill: "none", children: [
12
75
  /* @__PURE__ */ jsx(
13
76
  "circle",
14
77
  {
@@ -56,7 +119,7 @@ var GradientButton = forwardRef(
56
119
  className: classes,
57
120
  "aria-busy": loading || void 0,
58
121
  ...rest,
59
- children: loading ? loadingLabel ?? /* @__PURE__ */ jsx(DefaultSpinner, {}) : children
122
+ children: loading ? loadingLabel ?? /* @__PURE__ */ jsx(DefaultSpinner2, {}) : children
60
123
  }
61
124
  );
62
125
  }
@@ -90,12 +153,12 @@ function Popover({
90
153
  renderTrigger
91
154
  }) {
92
155
  const [open, setOpen] = useState(defaultOpen);
93
- const wrap = useRef(null);
156
+ const wrap2 = useRef(null);
94
157
  const toggle = () => setOpen((o) => !o);
95
158
  useEffect(() => {
96
159
  if (!open) return;
97
160
  function onDown(e) {
98
- if (wrap.current && !wrap.current.contains(e.target)) {
161
+ if (wrap2.current && !wrap2.current.contains(e.target)) {
99
162
  setOpen(false);
100
163
  }
101
164
  }
@@ -123,7 +186,7 @@ function Popover({
123
186
  const widthClass = typeof width === "string" ? `royui-popover__panel--${width}` : "";
124
187
  const customWidth = typeof width === "number" ? { width: `${width}px` } : void 0;
125
188
  const alignClass = `royui-popover__panel--${align}`;
126
- return /* @__PURE__ */ jsxs("div", { ref: wrap, className: "royui-popover", children: [
189
+ return /* @__PURE__ */ jsxs("div", { ref: wrap2, className: "royui-popover", children: [
127
190
  trigger,
128
191
  open && /* @__PURE__ */ jsxs(
129
192
  "div",
@@ -375,7 +438,2085 @@ var TreeNavItem = forwardRef(
375
438
  }
376
439
  );
377
440
  TreeNavItem.displayName = "TreeNavItem";
441
+ function Spinner({
442
+ size = 16,
443
+ strokeWidth = 2,
444
+ label = "Loading",
445
+ style,
446
+ className = ""
447
+ }) {
448
+ const r = (size - strokeWidth) / 2;
449
+ const c = size / 2;
450
+ const circumference = 2 * Math.PI * r;
451
+ return /* @__PURE__ */ jsx(
452
+ "span",
453
+ {
454
+ role: "status",
455
+ "aria-label": label,
456
+ className: ["royui-spinner", className].filter(Boolean).join(" "),
457
+ style: { width: size, height: size, ...style },
458
+ children: /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, "aria-hidden": true, children: [
459
+ /* @__PURE__ */ jsx(
460
+ "circle",
461
+ {
462
+ cx: c,
463
+ cy: c,
464
+ r,
465
+ fill: "none",
466
+ stroke: "currentColor",
467
+ strokeOpacity: 0.18,
468
+ strokeWidth
469
+ }
470
+ ),
471
+ /* @__PURE__ */ jsx(
472
+ "circle",
473
+ {
474
+ cx: c,
475
+ cy: c,
476
+ r,
477
+ fill: "none",
478
+ stroke: "currentColor",
479
+ strokeWidth,
480
+ strokeLinecap: "round",
481
+ strokeDasharray: circumference,
482
+ strokeDashoffset: circumference * 0.72,
483
+ transform: `rotate(-90 ${c} ${c})`
484
+ }
485
+ )
486
+ ] })
487
+ }
488
+ );
489
+ }
490
+ function fontVars(prefix, spec) {
491
+ if (!spec) return {};
492
+ const out = {};
493
+ if (typeof spec === "string") {
494
+ out[`${prefix}-font`] = spec;
495
+ return out;
496
+ }
497
+ if (spec.family) out[`${prefix}-font`] = spec.family;
498
+ if (spec.size != null)
499
+ out[`${prefix}-size`] = typeof spec.size === "number" ? `${spec.size}px` : spec.size;
500
+ if (spec.weight != null) out[`${prefix}-weight`] = String(spec.weight);
501
+ if (spec.letterSpacing) out[`${prefix}-tracking`] = spec.letterSpacing;
502
+ if (spec.featureSettings) out[`${prefix}-features`] = spec.featureSettings;
503
+ return out;
504
+ }
505
+ var Table = forwardRef(function Table2({
506
+ visibleRows = 7,
507
+ rowHeight = 44,
508
+ stickyHeader = true,
509
+ density = "cozy",
510
+ loading = false,
511
+ empty,
512
+ isEmpty = false,
513
+ fitColumns = false,
514
+ headerFont,
515
+ rowHeaderFont,
516
+ cellFont,
517
+ className = "",
518
+ style,
519
+ children,
520
+ tableProps,
521
+ ...rest
522
+ }, ref) {
523
+ const headerH = 40;
524
+ const maxH = rowHeight * visibleRows + (stickyHeader ? headerH : 0);
525
+ const mergedStyle = {
526
+ ...style,
527
+ ["--royui-table-row-h"]: `${rowHeight}px`,
528
+ ["--royui-table-max-h"]: `${maxH}px`,
529
+ ...fontVars("--royui-table-header", headerFont),
530
+ ...fontVars("--royui-table-row-header", rowHeaderFont),
531
+ ...fontVars("--royui-table-cell", cellFont)
532
+ };
533
+ const classes = [
534
+ "royui-table",
535
+ `royui-table--${density}`,
536
+ stickyHeader && "royui-table--sticky",
537
+ loading && "royui-table--loading",
538
+ fitColumns && "royui-table--fit",
539
+ className
540
+ ].filter(Boolean).join(" ");
541
+ return /* @__PURE__ */ jsx("div", { ref, className: classes, style: mergedStyle, ...rest, children: /* @__PURE__ */ jsxs("div", { className: "royui-table__scroll", role: "region", "aria-label": "Table", children: [
542
+ /* @__PURE__ */ jsx("table", { className: "royui-table__table", ...tableProps, children }),
543
+ isEmpty && !loading && /* @__PURE__ */ jsx("div", { className: "royui-table__empty", role: "status", children: empty ?? /* @__PURE__ */ jsx("span", { children: "No results" }) }),
544
+ loading && /* @__PURE__ */ jsx("div", { className: "royui-table__loading", "aria-hidden": true, children: /* @__PURE__ */ jsx(Spinner, { size: 18 }) })
545
+ ] }) });
546
+ });
547
+ var TableHeader = forwardRef(function TableHeader2({ className = "", ...rest }, ref) {
548
+ return /* @__PURE__ */ jsx(
549
+ "thead",
550
+ {
551
+ ref,
552
+ className: ["royui-table__thead", className].filter(Boolean).join(" "),
553
+ ...rest
554
+ }
555
+ );
556
+ });
557
+ var TableBody = forwardRef(function TableBody2({ className = "", ...rest }, ref) {
558
+ return /* @__PURE__ */ jsx(
559
+ "tbody",
560
+ {
561
+ ref,
562
+ className: ["royui-table__tbody", className].filter(Boolean).join(" "),
563
+ ...rest
564
+ }
565
+ );
566
+ });
567
+ var TableRow = forwardRef(function TableRow2({ className = "", ...rest }, ref) {
568
+ return /* @__PURE__ */ jsx(
569
+ "tr",
570
+ {
571
+ ref,
572
+ className: ["royui-table__tr", className].filter(Boolean).join(" "),
573
+ ...rest
574
+ }
575
+ );
576
+ });
577
+ var TableHead = forwardRef(
578
+ function TableHead2({ className = "", align = "left", ...rest }, ref) {
579
+ return /* @__PURE__ */ jsx(
580
+ "th",
581
+ {
582
+ ref,
583
+ scope: "col",
584
+ className: [
585
+ "royui-table__th",
586
+ align !== "left" && `royui-table__th--${align}`,
587
+ className
588
+ ].filter(Boolean).join(" "),
589
+ ...rest
590
+ }
591
+ );
592
+ }
593
+ );
594
+ var TableCell = forwardRef(
595
+ function TableCell2({ className = "", align = "left", isRowHeader, ...rest }, ref) {
596
+ if (isRowHeader) {
597
+ return /* @__PURE__ */ jsx(
598
+ "th",
599
+ {
600
+ ref,
601
+ scope: "row",
602
+ className: [
603
+ "royui-table__row-header",
604
+ align !== "left" && `royui-table__td--${align}`,
605
+ className
606
+ ].filter(Boolean).join(" "),
607
+ ...rest
608
+ }
609
+ );
610
+ }
611
+ return /* @__PURE__ */ jsx(
612
+ "td",
613
+ {
614
+ ref,
615
+ className: [
616
+ "royui-table__td",
617
+ align !== "left" && `royui-table__td--${align}`,
618
+ className
619
+ ].filter(Boolean).join(" "),
620
+ ...rest
621
+ }
622
+ );
623
+ }
624
+ );
625
+ var TableSearch = forwardRef(
626
+ function TableSearch2({
627
+ value,
628
+ defaultValue,
629
+ onChange,
630
+ debounceMs = 0,
631
+ placeholder = "Search",
632
+ width = 260,
633
+ hideIndicator,
634
+ className = "",
635
+ style,
636
+ ...rest
637
+ }, ref) {
638
+ const controlled = value !== void 0;
639
+ const [internal, setInternal] = useState(defaultValue ?? "");
640
+ const current = controlled ? value : internal;
641
+ const timer = useRef(null);
642
+ useEffect(() => () => {
643
+ if (timer.current) clearTimeout(timer.current);
644
+ }, []);
645
+ const emit = (next) => {
646
+ if (!onChange) return;
647
+ if (debounceMs <= 0) {
648
+ onChange(next);
649
+ return;
650
+ }
651
+ if (timer.current) clearTimeout(timer.current);
652
+ timer.current = setTimeout(() => onChange(next), debounceMs);
653
+ };
654
+ const handle = (e) => {
655
+ const next = e.target.value;
656
+ if (!controlled) setInternal(next);
657
+ emit(next);
658
+ };
659
+ const clear = () => {
660
+ if (!controlled) setInternal("");
661
+ if (onChange) onChange("");
662
+ };
663
+ return /* @__PURE__ */ jsxs(
664
+ "div",
665
+ {
666
+ className: ["royui-tablesearch", className].filter(Boolean).join(" "),
667
+ style: { width, ...style },
668
+ children: [
669
+ !hideIndicator && /* @__PURE__ */ jsx("span", { className: "royui-tablesearch__dot", "aria-hidden": true }),
670
+ /* @__PURE__ */ jsx(
671
+ "input",
672
+ {
673
+ ref,
674
+ type: "text",
675
+ className: "royui-tablesearch__input",
676
+ placeholder,
677
+ value: current,
678
+ onChange: handle,
679
+ ...rest
680
+ }
681
+ ),
682
+ current.length > 0 && /* @__PURE__ */ jsx(
683
+ "button",
684
+ {
685
+ type: "button",
686
+ className: "royui-tablesearch__clear",
687
+ onClick: clear,
688
+ "aria-label": "Clear search",
689
+ children: "Clear"
690
+ }
691
+ )
692
+ ]
693
+ }
694
+ );
695
+ }
696
+ );
697
+ function buildRange(page, pageCount, sibling) {
698
+ if (pageCount <= 1) return [1];
699
+ const first = 1;
700
+ const last = pageCount;
701
+ const left = Math.max(page - sibling, first + 1);
702
+ const right = Math.min(page + sibling, last - 1);
703
+ const cells = [first];
704
+ if (left > first + 1) cells.push("gap");
705
+ for (let i = left; i <= right; i++) cells.push(i);
706
+ if (right < last - 1) cells.push("gap");
707
+ if (last > first) cells.push(last);
708
+ return cells;
709
+ }
710
+ function Pagination({
711
+ page,
712
+ pageCount,
713
+ onPageChange,
714
+ siblingCount = 1,
715
+ showPrevNext = true,
716
+ prevLabel = "Prev",
717
+ nextLabel = "Next",
718
+ showSummary = false,
719
+ summaryRender,
720
+ className = "",
721
+ style
722
+ }) {
723
+ const cells = useMemo(
724
+ () => buildRange(page, pageCount, siblingCount),
725
+ [page, pageCount, siblingCount]
726
+ );
727
+ const canPrev = page > 1;
728
+ const canNext = page < pageCount;
729
+ const go = (n) => {
730
+ if (n < 1 || n > pageCount || n === page) return;
731
+ onPageChange(n);
732
+ };
733
+ return /* @__PURE__ */ jsxs(
734
+ "nav",
735
+ {
736
+ className: ["royui-pagination", className].filter(Boolean).join(" "),
737
+ style,
738
+ "aria-label": "Pagination",
739
+ children: [
740
+ showSummary && /* @__PURE__ */ jsx("span", { className: "royui-pagination__summary", children: summaryRender ? summaryRender(page, pageCount) : `Page ${page} of ${pageCount}` }),
741
+ /* @__PURE__ */ jsxs("div", { className: "royui-pagination__group", children: [
742
+ showPrevNext && /* @__PURE__ */ jsx(
743
+ "button",
744
+ {
745
+ type: "button",
746
+ className: "royui-pagination__step",
747
+ onClick: () => go(page - 1),
748
+ disabled: !canPrev,
749
+ "aria-label": "Previous page",
750
+ children: prevLabel
751
+ }
752
+ ),
753
+ /* @__PURE__ */ jsx("ul", { className: "royui-pagination__pages", children: cells.map(
754
+ (cell, i) => cell === "gap" ? /* @__PURE__ */ jsx("li", { className: "royui-pagination__gap", "aria-hidden": true, children: "\xB7" }, `gap-${i}`) : /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
755
+ "button",
756
+ {
757
+ type: "button",
758
+ className: [
759
+ "royui-pagination__page",
760
+ cell === page && "royui-pagination__page--current"
761
+ ].filter(Boolean).join(" "),
762
+ onClick: () => go(cell),
763
+ "aria-current": cell === page ? "page" : void 0,
764
+ "aria-label": `Page ${cell}`,
765
+ children: cell
766
+ }
767
+ ) }, cell)
768
+ ) }),
769
+ showPrevNext && /* @__PURE__ */ jsx(
770
+ "button",
771
+ {
772
+ type: "button",
773
+ className: "royui-pagination__step",
774
+ onClick: () => go(page + 1),
775
+ disabled: !canNext,
776
+ "aria-label": "Next page",
777
+ children: nextLabel
778
+ }
779
+ )
780
+ ] })
781
+ ]
782
+ }
783
+ );
784
+ }
785
+
786
+ // src/components/date-range-picker/dateUtils.ts
787
+ var MONTHS = [
788
+ "January",
789
+ "February",
790
+ "March",
791
+ "April",
792
+ "May",
793
+ "June",
794
+ "July",
795
+ "August",
796
+ "September",
797
+ "October",
798
+ "November",
799
+ "December"
800
+ ];
801
+ var SHORT_MONTHS = [
802
+ "Jan",
803
+ "Feb",
804
+ "Mar",
805
+ "Apr",
806
+ "May",
807
+ "Jun",
808
+ "Jul",
809
+ "Aug",
810
+ "Sep",
811
+ "Oct",
812
+ "Nov",
813
+ "Dec"
814
+ ];
815
+ var WEEKDAYS = ["S", "M", "T", "W", "T", "F", "S"];
816
+ function startOfDay(d) {
817
+ const n = new Date(d);
818
+ n.setHours(0, 0, 0, 0);
819
+ return n;
820
+ }
821
+ function isSameDay(a, b) {
822
+ if (!a || !b) return false;
823
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
824
+ }
825
+ function isSameMonth(a, b) {
826
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
827
+ }
828
+ function addMonths(d, n) {
829
+ const x = new Date(d);
830
+ x.setDate(1);
831
+ x.setMonth(x.getMonth() + n);
832
+ return x;
833
+ }
834
+ function addDays(d, n) {
835
+ const x = new Date(d);
836
+ x.setDate(x.getDate() + n);
837
+ return x;
838
+ }
839
+ function isBefore(a, b) {
840
+ return a.getTime() < b.getTime();
841
+ }
842
+ function isAfter(a, b) {
843
+ return a.getTime() > b.getTime();
844
+ }
845
+ function isBetween(d, from, to) {
846
+ const t = d.getTime();
847
+ const a = Math.min(from.getTime(), to.getTime());
848
+ const b = Math.max(from.getTime(), to.getTime());
849
+ return t >= a && t <= b;
850
+ }
851
+ function clampToBounds(d, min, max) {
852
+ let r = d;
853
+ if (min && isBefore(r, min)) r = min;
854
+ if (max && isAfter(r, max)) r = max;
855
+ return r;
856
+ }
857
+ function getMonthGrid(year, month, weekStartsOn = 0) {
858
+ const first = new Date(year, month, 1);
859
+ const firstDow = first.getDay();
860
+ const offset = (firstDow - weekStartsOn + 7) % 7;
861
+ const gridStart = addDays(first, -offset);
862
+ const cells = [];
863
+ for (let i = 0; i < 42; i++) {
864
+ const date = addDays(gridStart, i);
865
+ cells.push({
866
+ date,
867
+ inMonth: date.getMonth() === month,
868
+ iso: date.toISOString().slice(0, 10)
869
+ });
870
+ }
871
+ return cells;
872
+ }
873
+ function getWeekdayLabels(weekStartsOn = 0) {
874
+ return Array.from({ length: 7 }, (_, i) => WEEKDAYS[(weekStartsOn + i) % 7] ?? "");
875
+ }
876
+ function formatMonthYear(d) {
877
+ return `${MONTHS[d.getMonth()] ?? ""} ${d.getFullYear()}`;
878
+ }
879
+ function formatShort(d) {
880
+ if (!d) return "";
881
+ const m = SHORT_MONTHS[d.getMonth()] ?? "";
882
+ const sameYear = d.getFullYear() === (/* @__PURE__ */ new Date()).getFullYear();
883
+ return sameYear ? `${m} ${d.getDate()}` : `${m} ${d.getDate()}, ${d.getFullYear()}`;
884
+ }
885
+ function formatRange(range) {
886
+ if (!range.from && !range.to) return "";
887
+ if (range.from && !range.to) return formatShort(range.from);
888
+ if (!range.from && range.to) return formatShort(range.to);
889
+ if (isSameDay(range.from, range.to)) return formatShort(range.from);
890
+ return `${formatShort(range.from)} \u2013 ${formatShort(range.to)}`;
891
+ }
892
+ var today = () => startOfDay(/* @__PURE__ */ new Date());
893
+ var DEFAULT_PRESETS = [
894
+ { label: "Today", range: () => ({ from: today(), to: today() }) },
895
+ {
896
+ label: "Last 7 days",
897
+ range: () => ({ from: addDays(today(), -6), to: today() })
898
+ },
899
+ {
900
+ label: "Last 30 days",
901
+ range: () => ({ from: addDays(today(), -29), to: today() })
902
+ },
903
+ {
904
+ label: "This month",
905
+ range: () => {
906
+ const t = today();
907
+ return {
908
+ from: new Date(t.getFullYear(), t.getMonth(), 1),
909
+ to: t
910
+ };
911
+ }
912
+ },
913
+ {
914
+ label: "Last month",
915
+ range: () => {
916
+ const t = today();
917
+ const first = new Date(t.getFullYear(), t.getMonth() - 1, 1);
918
+ const last = new Date(t.getFullYear(), t.getMonth(), 0);
919
+ return { from: first, to: last };
920
+ }
921
+ }
922
+ ];
923
+ function DateRangePicker({
924
+ value,
925
+ defaultValue,
926
+ onChange,
927
+ monthsVisible = 2,
928
+ weekStartsOn = 0,
929
+ minDate,
930
+ maxDate,
931
+ placeholder = "Pick a range",
932
+ presets = DEFAULT_PRESETS,
933
+ align = "left",
934
+ className = "",
935
+ style,
936
+ triggerLabel,
937
+ disabled
938
+ }) {
939
+ const controlled = value !== void 0;
940
+ const [internal, setInternal] = useState(
941
+ defaultValue ?? { from: null, to: null }
942
+ );
943
+ const current = controlled ? value : internal;
944
+ const [open, setOpen] = useState(false);
945
+ const [draft, setDraft] = useState(current);
946
+ const [hover, setHover] = useState(null);
947
+ const [anchorMonth, setAnchorMonth] = useState(
948
+ () => startOfDay(current.from ?? today())
949
+ );
950
+ const wrap2 = useRef(null);
951
+ useEffect(() => {
952
+ if (!open) return;
953
+ function onDown(e) {
954
+ if (wrap2.current && !wrap2.current.contains(e.target)) {
955
+ setOpen(false);
956
+ }
957
+ }
958
+ function onKey(e) {
959
+ if (e.key === "Escape") setOpen(false);
960
+ }
961
+ document.addEventListener("mousedown", onDown);
962
+ document.addEventListener("keydown", onKey);
963
+ return () => {
964
+ document.removeEventListener("mousedown", onDown);
965
+ document.removeEventListener("keydown", onKey);
966
+ };
967
+ }, [open]);
968
+ useEffect(() => {
969
+ if (open) {
970
+ setDraft(current);
971
+ setHover(null);
972
+ setAnchorMonth(startOfDay(current.from ?? today()));
973
+ }
974
+ }, [open]);
975
+ const months = useMemo(() => {
976
+ return Array.from({ length: monthsVisible }, (_, i) => addMonths(anchorMonth, i));
977
+ }, [anchorMonth, monthsVisible]);
978
+ const commit = (next) => {
979
+ if (!controlled) setInternal(next);
980
+ onChange?.(next);
981
+ };
982
+ const apply = () => {
983
+ commit(draft);
984
+ setOpen(false);
985
+ };
986
+ const clear = () => {
987
+ setDraft({ from: null, to: null });
988
+ setHover(null);
989
+ };
990
+ const selectDay = (d) => {
991
+ const day = startOfDay(d);
992
+ const { from, to } = draft;
993
+ if (!from || from && to) {
994
+ setDraft({ from: day, to: null });
995
+ setHover(day);
996
+ return;
997
+ }
998
+ if (isBefore(day, from)) {
999
+ setDraft({ from: day, to: from });
1000
+ } else {
1001
+ setDraft({ from, to: day });
1002
+ }
1003
+ };
1004
+ const isDisabled = (d) => {
1005
+ if (minDate && isBefore(d, startOfDay(minDate))) return true;
1006
+ if (maxDate && isAfter(d, startOfDay(maxDate))) return true;
1007
+ return false;
1008
+ };
1009
+ const previewTo = !draft.to && draft.from && hover ? hover : draft.to;
1010
+ const previewRange = { from: draft.from, to: previewTo };
1011
+ return /* @__PURE__ */ jsxs(
1012
+ "div",
1013
+ {
1014
+ ref: wrap2,
1015
+ className: ["royui-drp", className].filter(Boolean).join(" "),
1016
+ style,
1017
+ children: [
1018
+ /* @__PURE__ */ jsxs(
1019
+ "button",
1020
+ {
1021
+ type: "button",
1022
+ className: "royui-drp__trigger",
1023
+ onClick: () => !disabled && setOpen((o) => !o),
1024
+ "aria-haspopup": "dialog",
1025
+ "aria-expanded": open,
1026
+ disabled,
1027
+ children: [
1028
+ /* @__PURE__ */ jsx("span", { className: "royui-drp__trigger-dot", "aria-hidden": true }),
1029
+ /* @__PURE__ */ jsx("span", { className: "royui-drp__trigger-label", children: triggerLabel ?? (formatRange(current) || placeholder) })
1030
+ ]
1031
+ }
1032
+ ),
1033
+ open && /* @__PURE__ */ jsxs(
1034
+ "div",
1035
+ {
1036
+ className: `royui-drp__panel royui-drp__panel--${align}`,
1037
+ role: "dialog",
1038
+ "aria-label": "Choose date range",
1039
+ children: [
1040
+ presets.length > 0 && /* @__PURE__ */ jsx("div", { className: "royui-drp__presets", children: presets.map((p) => {
1041
+ const r = p.range();
1042
+ const isActive = isSameDay(draft.from, r.from) && isSameDay(draft.to, r.to);
1043
+ return /* @__PURE__ */ jsx(
1044
+ "button",
1045
+ {
1046
+ type: "button",
1047
+ className: [
1048
+ "royui-drp__preset",
1049
+ isActive && "royui-drp__preset--active"
1050
+ ].filter(Boolean).join(" "),
1051
+ onClick: () => {
1052
+ const next = {
1053
+ from: r.from ? startOfDay(clampToBounds(r.from, minDate, maxDate)) : null,
1054
+ to: r.to ? startOfDay(clampToBounds(r.to, minDate, maxDate)) : null
1055
+ };
1056
+ setDraft(next);
1057
+ if (next.from) setAnchorMonth(next.from);
1058
+ },
1059
+ children: p.label
1060
+ },
1061
+ p.label
1062
+ );
1063
+ }) }),
1064
+ /* @__PURE__ */ jsxs("div", { className: "royui-drp__main", children: [
1065
+ /* @__PURE__ */ jsxs("div", { className: "royui-drp__nav", children: [
1066
+ /* @__PURE__ */ jsx(
1067
+ "button",
1068
+ {
1069
+ type: "button",
1070
+ className: "royui-drp__nav-btn",
1071
+ onClick: () => setAnchorMonth(addMonths(anchorMonth, -1)),
1072
+ "aria-label": "Previous month",
1073
+ children: "Prev"
1074
+ }
1075
+ ),
1076
+ /* @__PURE__ */ jsx(
1077
+ "button",
1078
+ {
1079
+ type: "button",
1080
+ className: "royui-drp__nav-btn",
1081
+ onClick: () => setAnchorMonth(addMonths(anchorMonth, 1)),
1082
+ "aria-label": "Next month",
1083
+ children: "Next"
1084
+ }
1085
+ )
1086
+ ] }),
1087
+ /* @__PURE__ */ jsx("div", { className: "royui-drp__months", children: months.map((m) => /* @__PURE__ */ jsx(
1088
+ MonthGrid,
1089
+ {
1090
+ month: m,
1091
+ range: previewRange,
1092
+ hover,
1093
+ weekStartsOn,
1094
+ isDisabled,
1095
+ onSelect: selectDay,
1096
+ onHover: (d) => setHover(d)
1097
+ },
1098
+ `${m.getFullYear()}-${m.getMonth()}`
1099
+ )) }),
1100
+ /* @__PURE__ */ jsxs("div", { className: "royui-drp__foot", children: [
1101
+ /* @__PURE__ */ jsx("div", { className: "royui-drp__readout", children: formatRange(draft) || "Select start and end" }),
1102
+ /* @__PURE__ */ jsxs("div", { className: "royui-drp__foot-actions", children: [
1103
+ /* @__PURE__ */ jsx(
1104
+ "button",
1105
+ {
1106
+ type: "button",
1107
+ className: "royui-drp__ghost",
1108
+ onClick: clear,
1109
+ children: "Clear"
1110
+ }
1111
+ ),
1112
+ /* @__PURE__ */ jsx(
1113
+ "button",
1114
+ {
1115
+ type: "button",
1116
+ className: "royui-drp__primary",
1117
+ onClick: apply,
1118
+ disabled: !draft.from,
1119
+ children: "Apply"
1120
+ }
1121
+ )
1122
+ ] })
1123
+ ] })
1124
+ ] })
1125
+ ]
1126
+ }
1127
+ )
1128
+ ]
1129
+ }
1130
+ );
1131
+ }
1132
+ function MonthGrid({
1133
+ month,
1134
+ range,
1135
+ hover,
1136
+ weekStartsOn,
1137
+ isDisabled,
1138
+ onSelect,
1139
+ onHover
1140
+ }) {
1141
+ const grid = useMemo(
1142
+ () => getMonthGrid(month.getFullYear(), month.getMonth(), weekStartsOn),
1143
+ [month, weekStartsOn]
1144
+ );
1145
+ const labels = useMemo(() => getWeekdayLabels(weekStartsOn), [weekStartsOn]);
1146
+ const todayD = today();
1147
+ return /* @__PURE__ */ jsxs("div", { className: "royui-drp__month", children: [
1148
+ /* @__PURE__ */ jsx("div", { className: "royui-drp__month-title", children: formatMonthYear(month) }),
1149
+ /* @__PURE__ */ jsx("div", { className: "royui-drp__weekdays", children: labels.map((l, i) => /* @__PURE__ */ jsx("span", { className: "royui-drp__weekday", children: l }, i)) }),
1150
+ /* @__PURE__ */ jsx("div", { className: "royui-drp__grid", onMouseLeave: () => onHover(null), children: grid.map((c) => {
1151
+ const inMonth = isSameMonth(c.date, month);
1152
+ const disabled = isDisabled(c.date);
1153
+ const isStart = isSameDay(c.date, range.from);
1154
+ const isEnd = isSameDay(c.date, range.to);
1155
+ const isInRange = range.from && range.to && isBetween(c.date, range.from, range.to);
1156
+ const isPreview = range.from && !range.to && hover && isBetween(c.date, range.from, hover);
1157
+ const isTodayCell = isSameDay(c.date, todayD);
1158
+ const classes = [
1159
+ "royui-drp__day",
1160
+ !inMonth && "royui-drp__day--out",
1161
+ disabled && "royui-drp__day--disabled",
1162
+ (isStart || isEnd) && "royui-drp__day--edge",
1163
+ isStart && "royui-drp__day--start",
1164
+ isEnd && "royui-drp__day--end",
1165
+ (isInRange || isPreview) && "royui-drp__day--in",
1166
+ isTodayCell && "royui-drp__day--today"
1167
+ ].filter(Boolean).join(" ");
1168
+ return /* @__PURE__ */ jsx(
1169
+ "button",
1170
+ {
1171
+ type: "button",
1172
+ className: classes,
1173
+ disabled,
1174
+ tabIndex: inMonth ? 0 : -1,
1175
+ onClick: () => inMonth && onSelect(c.date),
1176
+ onMouseEnter: () => inMonth && onHover(c.date),
1177
+ "aria-label": c.date.toDateString(),
1178
+ children: /* @__PURE__ */ jsx("span", { children: c.date.getDate() })
1179
+ },
1180
+ c.iso
1181
+ );
1182
+ }) })
1183
+ ] });
1184
+ }
1185
+ function angleFromCenter(cx, cy, px, py) {
1186
+ const dx = px - cx;
1187
+ const dy = py - cy;
1188
+ const a = Math.atan2(dy, dx) * (180 / Math.PI) + 90;
1189
+ return (a + 360) % 360;
1190
+ }
1191
+ function AnalogClock({
1192
+ value,
1193
+ onChange,
1194
+ hourCycle = 24,
1195
+ minuteStep = 1,
1196
+ size = 220
1197
+ }) {
1198
+ const ref = useRef(null);
1199
+ const [mode, setMode] = useState("hours");
1200
+ const [dragging, setDragging] = useState(false);
1201
+ const updateFromPointer = useCallback(
1202
+ (clientX, clientY, currentMode) => {
1203
+ const svg = ref.current;
1204
+ if (!svg) return;
1205
+ const rect = svg.getBoundingClientRect();
1206
+ const cx2 = rect.left + rect.width / 2;
1207
+ const cy2 = rect.top + rect.height / 2;
1208
+ const a = angleFromCenter(cx2, cy2, clientX, clientY);
1209
+ if (currentMode === "hours") {
1210
+ const hours122 = Math.round(a / 30) % 12;
1211
+ const isAm2 = value.hours < 12;
1212
+ const h24 = isAm2 ? hours122 : hours122 + 12;
1213
+ onChange({ ...value, hours: h24 });
1214
+ } else {
1215
+ let m = Math.round(a / 6) % 60;
1216
+ if (minuteStep > 1) m = Math.round(m / minuteStep) * minuteStep;
1217
+ if (m === 60) m = 0;
1218
+ onChange({ ...value, minutes: m });
1219
+ }
1220
+ },
1221
+ [minuteStep, onChange, value]
1222
+ );
1223
+ const togglePeriod = () => {
1224
+ const next = value.hours < 12 ? value.hours + 12 : value.hours - 12;
1225
+ onChange({ ...value, hours: next });
1226
+ };
1227
+ const isAm = value.hours < 12;
1228
+ useEffect(() => {
1229
+ if (!dragging) return;
1230
+ const onMove = (e) => updateFromPointer(e.clientX, e.clientY, mode);
1231
+ const onUp = () => setDragging(false);
1232
+ window.addEventListener("pointermove", onMove);
1233
+ window.addEventListener("pointerup", onUp);
1234
+ return () => {
1235
+ window.removeEventListener("pointermove", onMove);
1236
+ window.removeEventListener("pointerup", onUp);
1237
+ };
1238
+ }, [dragging, mode, updateFromPointer]);
1239
+ const handlePointerDown = (e) => {
1240
+ e.preventDefault();
1241
+ setDragging(true);
1242
+ updateFromPointer(e.clientX, e.clientY, mode);
1243
+ };
1244
+ const hours12 = hourCycle === 12 ? value.hours === 0 ? 12 : value.hours > 12 ? value.hours - 12 : value.hours : value.hours % 12 === 0 ? 12 : value.hours % 12;
1245
+ const hourAngle = (hours12 % 12 + value.minutes / 60) * 30 - 90;
1246
+ const minuteAngle = value.minutes / 60 * 360 - 90;
1247
+ const r = size / 2;
1248
+ const cx = r;
1249
+ const cy = r;
1250
+ const hourLen = r * 0.45;
1251
+ const minuteLen = r * 0.7;
1252
+ const tickOuter = r * 0.92;
1253
+ const tickInner = r * 0.86;
1254
+ const majorInner = r * 0.82;
1255
+ const handX = (len, deg) => cx + len * Math.cos(deg * Math.PI / 180);
1256
+ const handY = (len, deg) => cy + len * Math.sin(deg * Math.PI / 180);
1257
+ return /* @__PURE__ */ jsxs("div", { className: "royui-tp-analog", children: [
1258
+ /* @__PURE__ */ jsxs(
1259
+ "svg",
1260
+ {
1261
+ ref,
1262
+ width: size,
1263
+ height: size,
1264
+ viewBox: `0 0 ${size} ${size}`,
1265
+ onPointerDown: handlePointerDown,
1266
+ className: "royui-tp-analog__face",
1267
+ role: "application",
1268
+ "aria-label": "Analog clock",
1269
+ children: [
1270
+ /* @__PURE__ */ jsx(
1271
+ "circle",
1272
+ {
1273
+ cx,
1274
+ cy,
1275
+ r: r - 1,
1276
+ className: "royui-tp-analog__bezel"
1277
+ }
1278
+ ),
1279
+ Array.from({ length: 60 }).map((_, i) => {
1280
+ const angle = (i * 6 - 90) * (Math.PI / 180);
1281
+ const isMajor = i % 5 === 0;
1282
+ const inner = isMajor ? majorInner : tickInner;
1283
+ return /* @__PURE__ */ jsx(
1284
+ "line",
1285
+ {
1286
+ x1: cx + inner * Math.cos(angle),
1287
+ y1: cy + inner * Math.sin(angle),
1288
+ x2: cx + tickOuter * Math.cos(angle),
1289
+ y2: cy + tickOuter * Math.sin(angle),
1290
+ className: isMajor ? "royui-tp-analog__tick--major" : "royui-tp-analog__tick"
1291
+ },
1292
+ i
1293
+ );
1294
+ }),
1295
+ [12, 3, 6, 9].map((n) => {
1296
+ const idx = n % 12;
1297
+ const angle = (idx * 30 - 90) * (Math.PI / 180);
1298
+ const rr = r * 0.72;
1299
+ return /* @__PURE__ */ jsx(
1300
+ "text",
1301
+ {
1302
+ x: cx + rr * Math.cos(angle),
1303
+ y: cy + rr * Math.sin(angle),
1304
+ dy: "0.34em",
1305
+ textAnchor: "middle",
1306
+ className: "royui-tp-analog__numeral",
1307
+ children: n
1308
+ },
1309
+ n
1310
+ );
1311
+ }),
1312
+ /* @__PURE__ */ jsx(
1313
+ "line",
1314
+ {
1315
+ x1: cx,
1316
+ y1: cy,
1317
+ x2: handX(hourLen, hourAngle),
1318
+ y2: handY(hourLen, hourAngle),
1319
+ className: `royui-tp-analog__hand royui-tp-analog__hand--hour ${mode === "hours" ? "royui-tp-analog__hand--active" : ""}`,
1320
+ onPointerDown: (e) => {
1321
+ e.stopPropagation();
1322
+ setMode("hours");
1323
+ setDragging(true);
1324
+ }
1325
+ }
1326
+ ),
1327
+ /* @__PURE__ */ jsx(
1328
+ "line",
1329
+ {
1330
+ x1: cx,
1331
+ y1: cy,
1332
+ x2: handX(minuteLen, minuteAngle),
1333
+ y2: handY(minuteLen, minuteAngle),
1334
+ className: `royui-tp-analog__hand royui-tp-analog__hand--minute ${mode === "minutes" ? "royui-tp-analog__hand--active" : ""}`,
1335
+ onPointerDown: (e) => {
1336
+ e.stopPropagation();
1337
+ setMode("minutes");
1338
+ setDragging(true);
1339
+ }
1340
+ }
1341
+ ),
1342
+ /* @__PURE__ */ jsx("circle", { cx, cy, r: 3.5, className: "royui-tp-analog__pin" })
1343
+ ]
1344
+ }
1345
+ ),
1346
+ /* @__PURE__ */ jsxs("div", { className: "royui-tp-analog__modes", children: [
1347
+ /* @__PURE__ */ jsx(
1348
+ "button",
1349
+ {
1350
+ type: "button",
1351
+ className: [
1352
+ "royui-tp-analog__mode",
1353
+ mode === "hours" && "royui-tp-analog__mode--on"
1354
+ ].filter(Boolean).join(" "),
1355
+ onClick: () => setMode("hours"),
1356
+ children: "Hours"
1357
+ }
1358
+ ),
1359
+ /* @__PURE__ */ jsx(
1360
+ "button",
1361
+ {
1362
+ type: "button",
1363
+ className: [
1364
+ "royui-tp-analog__mode",
1365
+ mode === "minutes" && "royui-tp-analog__mode--on"
1366
+ ].filter(Boolean).join(" "),
1367
+ onClick: () => setMode("minutes"),
1368
+ children: "Minutes"
1369
+ }
1370
+ ),
1371
+ /* @__PURE__ */ jsxs("div", { className: "royui-tp-analog__period", role: "group", "aria-label": "Day half", children: [
1372
+ /* @__PURE__ */ jsx(
1373
+ "button",
1374
+ {
1375
+ type: "button",
1376
+ className: [
1377
+ "royui-tp-analog__period-btn",
1378
+ isAm && "royui-tp-analog__period-btn--on"
1379
+ ].filter(Boolean).join(" "),
1380
+ onClick: () => {
1381
+ if (!isAm) togglePeriod();
1382
+ },
1383
+ "aria-pressed": isAm,
1384
+ children: "AM"
1385
+ }
1386
+ ),
1387
+ /* @__PURE__ */ jsx(
1388
+ "button",
1389
+ {
1390
+ type: "button",
1391
+ className: [
1392
+ "royui-tp-analog__period-btn",
1393
+ !isAm && "royui-tp-analog__period-btn--on"
1394
+ ].filter(Boolean).join(" "),
1395
+ onClick: () => {
1396
+ if (isAm) togglePeriod();
1397
+ },
1398
+ "aria-pressed": !isAm,
1399
+ children: "PM"
1400
+ }
1401
+ )
1402
+ ] })
1403
+ ] })
1404
+ ] });
1405
+ }
1406
+ function pad(n) {
1407
+ return String(n).padStart(2, "0");
1408
+ }
1409
+ function wrap(n, max) {
1410
+ return (n % max + max) % max;
1411
+ }
1412
+ function DigitalClock({
1413
+ value,
1414
+ onChange,
1415
+ hourCycle = 24,
1416
+ minuteStep = 1
1417
+ }) {
1418
+ const hourBoundary = hourCycle === 12 ? 12 : 24;
1419
+ const displayHour = hourCycle === 12 ? value.hours % 12 === 0 ? 12 : value.hours % 12 : value.hours;
1420
+ const isAm = value.hours < 12;
1421
+ const setDisplayHour = (next) => {
1422
+ if (hourCycle === 24) {
1423
+ onChange({ ...value, hours: wrap(next, 24) });
1424
+ return;
1425
+ }
1426
+ let h = next;
1427
+ if (h <= 0) h = 12;
1428
+ if (h > 12) h = 1;
1429
+ const h24 = isAm ? h === 12 ? 0 : h : h === 12 ? 12 : h + 12;
1430
+ onChange({ ...value, hours: h24 });
1431
+ };
1432
+ const setMinutes = (next) => {
1433
+ const stepped = Math.round(next / minuteStep) * minuteStep;
1434
+ onChange({ ...value, minutes: wrap(stepped, 60) });
1435
+ };
1436
+ const setAm = () => {
1437
+ if (isAm) return;
1438
+ onChange({ ...value, hours: value.hours - 12 });
1439
+ };
1440
+ const setPm = () => {
1441
+ if (!isAm) return;
1442
+ onChange({ ...value, hours: value.hours + 12 });
1443
+ };
1444
+ return /* @__PURE__ */ jsxs("div", { className: "royui-tp-digital", children: [
1445
+ /* @__PURE__ */ jsxs("div", { className: "royui-tp-digital__row", children: [
1446
+ /* @__PURE__ */ jsx(
1447
+ Segment,
1448
+ {
1449
+ label: "Hours",
1450
+ value: pad(displayHour),
1451
+ onWheelStep: (d) => setDisplayHour(displayHour + d),
1452
+ onArrow: (d) => setDisplayHour(displayHour + d),
1453
+ max: hourBoundary
1454
+ }
1455
+ ),
1456
+ /* @__PURE__ */ jsx("span", { className: "royui-tp-digital__sep", "aria-hidden": true, children: ":" }),
1457
+ /* @__PURE__ */ jsx(
1458
+ Segment,
1459
+ {
1460
+ label: "Minutes",
1461
+ value: pad(value.minutes),
1462
+ onWheelStep: (d) => setMinutes(value.minutes + d * minuteStep),
1463
+ onArrow: (d) => setMinutes(value.minutes + d * minuteStep),
1464
+ max: 60
1465
+ }
1466
+ )
1467
+ ] }),
1468
+ /* @__PURE__ */ jsxs("div", { className: "royui-tp-digital__period", role: "group", "aria-label": "Day half", children: [
1469
+ /* @__PURE__ */ jsx(
1470
+ "button",
1471
+ {
1472
+ type: "button",
1473
+ className: [
1474
+ "royui-tp-digital__period-btn",
1475
+ isAm && "royui-tp-digital__period-btn--on"
1476
+ ].filter(Boolean).join(" "),
1477
+ onClick: setAm,
1478
+ "aria-pressed": isAm,
1479
+ children: "AM"
1480
+ }
1481
+ ),
1482
+ /* @__PURE__ */ jsx(
1483
+ "button",
1484
+ {
1485
+ type: "button",
1486
+ className: [
1487
+ "royui-tp-digital__period-btn",
1488
+ !isAm && "royui-tp-digital__period-btn--on"
1489
+ ].filter(Boolean).join(" "),
1490
+ onClick: setPm,
1491
+ "aria-pressed": !isAm,
1492
+ children: "PM"
1493
+ }
1494
+ )
1495
+ ] }),
1496
+ /* @__PURE__ */ jsx("div", { className: "royui-tp-digital__hint", children: "Scroll or use arrow keys" })
1497
+ ] });
1498
+ }
1499
+ function Segment({
1500
+ label,
1501
+ value,
1502
+ onWheelStep,
1503
+ onArrow,
1504
+ max
1505
+ }) {
1506
+ const ref = useRef(null);
1507
+ const handleWheel = (e) => {
1508
+ e.preventDefault();
1509
+ onWheelStep(e.deltaY > 0 ? 1 : -1);
1510
+ };
1511
+ const handleKey = (e) => {
1512
+ if (e.key === "ArrowUp" || e.key === "ArrowRight") {
1513
+ e.preventDefault();
1514
+ onArrow(1);
1515
+ } else if (e.key === "ArrowDown" || e.key === "ArrowLeft") {
1516
+ e.preventDefault();
1517
+ onArrow(-1);
1518
+ }
1519
+ };
1520
+ return /* @__PURE__ */ jsx(
1521
+ "div",
1522
+ {
1523
+ ref,
1524
+ role: "spinbutton",
1525
+ tabIndex: 0,
1526
+ "aria-label": label,
1527
+ "aria-valuetext": value,
1528
+ "aria-valuemax": max,
1529
+ className: "royui-tp-digital__seg",
1530
+ onWheel: handleWheel,
1531
+ onKeyDown: handleKey,
1532
+ children: value
1533
+ }
1534
+ );
1535
+ }
1536
+ function pad2(n) {
1537
+ return String(n).padStart(2, "0");
1538
+ }
1539
+ function formatTime(t, hourCycle = 24) {
1540
+ if (!t) return "";
1541
+ if (hourCycle === 24) return `${pad2(t.hours)}:${pad2(t.minutes)}`;
1542
+ const h = t.hours % 12 === 0 ? 12 : t.hours % 12;
1543
+ const ampm = t.hours < 12 ? "AM" : "PM";
1544
+ return `${pad2(h)}:${pad2(t.minutes)} ${ampm}`;
1545
+ }
1546
+ function TimePicker({
1547
+ value,
1548
+ defaultValue,
1549
+ onChange,
1550
+ variant = "analog",
1551
+ switchable = true,
1552
+ hourCycle = 24,
1553
+ minuteStep = 1,
1554
+ placeholder = "Pick a time",
1555
+ align = "left",
1556
+ className = "",
1557
+ style,
1558
+ triggerLabel,
1559
+ disabled
1560
+ }) {
1561
+ const controlled = value !== void 0;
1562
+ const [internal, setInternal] = useState(defaultValue ?? null);
1563
+ const current = controlled ? value : internal;
1564
+ const [open, setOpen] = useState(false);
1565
+ const [mode, setMode] = useState(variant);
1566
+ const [draft, setDraft] = useState(current ?? { hours: 12, minutes: 0 });
1567
+ const wrap2 = useRef(null);
1568
+ useEffect(() => {
1569
+ if (!open) return;
1570
+ function onDown(e) {
1571
+ if (wrap2.current && !wrap2.current.contains(e.target)) {
1572
+ setOpen(false);
1573
+ }
1574
+ }
1575
+ function onKey(e) {
1576
+ if (e.key === "Escape") setOpen(false);
1577
+ }
1578
+ document.addEventListener("mousedown", onDown);
1579
+ document.addEventListener("keydown", onKey);
1580
+ return () => {
1581
+ document.removeEventListener("mousedown", onDown);
1582
+ document.removeEventListener("keydown", onKey);
1583
+ };
1584
+ }, [open]);
1585
+ useEffect(() => {
1586
+ if (open) setDraft(current ?? { hours: (/* @__PURE__ */ new Date()).getHours(), minutes: 0 });
1587
+ }, [open]);
1588
+ useEffect(() => {
1589
+ setMode(variant);
1590
+ }, [variant]);
1591
+ const commit = (next) => {
1592
+ if (!controlled) setInternal(next);
1593
+ onChange?.(next);
1594
+ };
1595
+ const setNow = () => {
1596
+ const now = /* @__PURE__ */ new Date();
1597
+ setDraft({ hours: now.getHours(), minutes: now.getMinutes() });
1598
+ };
1599
+ const apply = () => {
1600
+ commit(draft);
1601
+ setOpen(false);
1602
+ };
1603
+ return /* @__PURE__ */ jsxs(
1604
+ "div",
1605
+ {
1606
+ ref: wrap2,
1607
+ className: ["royui-tp", className].filter(Boolean).join(" "),
1608
+ style,
1609
+ children: [
1610
+ /* @__PURE__ */ jsxs(
1611
+ "button",
1612
+ {
1613
+ type: "button",
1614
+ className: "royui-tp__trigger",
1615
+ onClick: () => !disabled && setOpen((o) => !o),
1616
+ "aria-haspopup": "dialog",
1617
+ "aria-expanded": open,
1618
+ disabled,
1619
+ children: [
1620
+ /* @__PURE__ */ jsx("span", { className: "royui-tp__trigger-dot", "aria-hidden": true }),
1621
+ /* @__PURE__ */ jsx("span", { className: "royui-tp__trigger-label", children: triggerLabel ?? (formatTime(current ?? null, hourCycle) || placeholder) })
1622
+ ]
1623
+ }
1624
+ ),
1625
+ open && /* @__PURE__ */ jsxs(
1626
+ "div",
1627
+ {
1628
+ className: `royui-tp__panel royui-tp__panel--${align}`,
1629
+ role: "dialog",
1630
+ "aria-label": "Choose time",
1631
+ children: [
1632
+ /* @__PURE__ */ jsxs("div", { className: "royui-tp__head", children: [
1633
+ /* @__PURE__ */ jsx("div", { className: "royui-tp__readout", children: formatTime(draft, hourCycle) }),
1634
+ switchable && /* @__PURE__ */ jsxs("div", { className: "royui-tp__variants", role: "tablist", children: [
1635
+ /* @__PURE__ */ jsx(
1636
+ "button",
1637
+ {
1638
+ type: "button",
1639
+ role: "tab",
1640
+ "aria-selected": mode === "analog",
1641
+ className: [
1642
+ "royui-tp__variant",
1643
+ mode === "analog" && "royui-tp__variant--on"
1644
+ ].filter(Boolean).join(" "),
1645
+ onClick: () => setMode("analog"),
1646
+ children: "Analog"
1647
+ }
1648
+ ),
1649
+ /* @__PURE__ */ jsx(
1650
+ "button",
1651
+ {
1652
+ type: "button",
1653
+ role: "tab",
1654
+ "aria-selected": mode === "digital",
1655
+ className: [
1656
+ "royui-tp__variant",
1657
+ mode === "digital" && "royui-tp__variant--on"
1658
+ ].filter(Boolean).join(" "),
1659
+ onClick: () => setMode("digital"),
1660
+ children: "Digital"
1661
+ }
1662
+ )
1663
+ ] })
1664
+ ] }),
1665
+ /* @__PURE__ */ jsx("div", { className: "royui-tp__body", children: mode === "analog" ? /* @__PURE__ */ jsx(
1666
+ AnalogClock,
1667
+ {
1668
+ value: draft,
1669
+ onChange: setDraft,
1670
+ hourCycle,
1671
+ minuteStep
1672
+ }
1673
+ ) : /* @__PURE__ */ jsx(
1674
+ DigitalClock,
1675
+ {
1676
+ value: draft,
1677
+ onChange: setDraft,
1678
+ hourCycle,
1679
+ minuteStep
1680
+ }
1681
+ ) }),
1682
+ /* @__PURE__ */ jsxs("div", { className: "royui-tp__foot", children: [
1683
+ /* @__PURE__ */ jsx("button", { type: "button", className: "royui-tp__ghost", onClick: setNow, children: "Now" }),
1684
+ /* @__PURE__ */ jsx("button", { type: "button", className: "royui-tp__primary", onClick: apply, children: "Apply" })
1685
+ ] })
1686
+ ]
1687
+ }
1688
+ )
1689
+ ]
1690
+ }
1691
+ );
1692
+ }
1693
+ function ColumnMenu({
1694
+ columns,
1695
+ layout,
1696
+ onToggle,
1697
+ onReset
1698
+ }) {
1699
+ const [open, setOpen] = useState(false);
1700
+ const wrap2 = useRef(null);
1701
+ useEffect(() => {
1702
+ if (!open) return;
1703
+ function onDown(e) {
1704
+ if (wrap2.current && !wrap2.current.contains(e.target)) {
1705
+ setOpen(false);
1706
+ }
1707
+ }
1708
+ function onKey(e) {
1709
+ if (e.key === "Escape") setOpen(false);
1710
+ }
1711
+ document.addEventListener("mousedown", onDown);
1712
+ document.addEventListener("keydown", onKey);
1713
+ return () => {
1714
+ document.removeEventListener("mousedown", onDown);
1715
+ document.removeEventListener("keydown", onKey);
1716
+ };
1717
+ }, [open]);
1718
+ const hiddenCount = layout.hidden.length;
1719
+ return /* @__PURE__ */ jsxs("div", { className: "royui-dt-colmenu", ref: wrap2, children: [
1720
+ /* @__PURE__ */ jsx(
1721
+ "button",
1722
+ {
1723
+ type: "button",
1724
+ className: "royui-dt-colmenu__trigger",
1725
+ onClick: () => setOpen((o) => !o),
1726
+ "aria-expanded": open,
1727
+ "aria-haspopup": "menu",
1728
+ children: "Columns"
1729
+ }
1730
+ ),
1731
+ hiddenCount > 0 && /* @__PURE__ */ jsxs(
1732
+ "button",
1733
+ {
1734
+ type: "button",
1735
+ className: "royui-dt-colmenu__chip",
1736
+ onClick: () => setOpen(true),
1737
+ children: [
1738
+ hiddenCount,
1739
+ " hidden"
1740
+ ]
1741
+ }
1742
+ ),
1743
+ open && /* @__PURE__ */ jsxs("div", { className: "royui-dt-colmenu__panel", role: "menu", children: [
1744
+ /* @__PURE__ */ jsxs("div", { className: "royui-dt-colmenu__head", children: [
1745
+ /* @__PURE__ */ jsx("span", { className: "royui-dt-colmenu__title", children: "Columns" }),
1746
+ /* @__PURE__ */ jsx(
1747
+ "button",
1748
+ {
1749
+ type: "button",
1750
+ className: "royui-dt-colmenu__reset",
1751
+ onClick: () => {
1752
+ onReset();
1753
+ },
1754
+ children: "Reset"
1755
+ }
1756
+ )
1757
+ ] }),
1758
+ /* @__PURE__ */ jsx("ul", { className: "royui-dt-colmenu__list", children: columns.map((c) => {
1759
+ const isHidden = layout.hidden.includes(c.key);
1760
+ const disabled = c.hideable === false;
1761
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
1762
+ "button",
1763
+ {
1764
+ type: "button",
1765
+ role: "menuitemcheckbox",
1766
+ "aria-checked": !isHidden,
1767
+ className: [
1768
+ "royui-dt-colmenu__row",
1769
+ isHidden && "royui-dt-colmenu__row--off",
1770
+ disabled && "royui-dt-colmenu__row--locked"
1771
+ ].filter(Boolean).join(" "),
1772
+ onClick: () => !disabled && onToggle(c.key),
1773
+ disabled,
1774
+ children: [
1775
+ /* @__PURE__ */ jsx(
1776
+ "span",
1777
+ {
1778
+ className: [
1779
+ "royui-dt-colmenu__dot",
1780
+ !isHidden && "royui-dt-colmenu__dot--on"
1781
+ ].filter(Boolean).join(" "),
1782
+ "aria-hidden": true
1783
+ }
1784
+ ),
1785
+ /* @__PURE__ */ jsx("span", { className: "royui-dt-colmenu__label", children: c.header })
1786
+ ]
1787
+ }
1788
+ ) }, c.key);
1789
+ }) })
1790
+ ] })
1791
+ ] });
1792
+ }
1793
+ function defaultLayout(columns) {
1794
+ return {
1795
+ order: columns.map((c) => c.key),
1796
+ sizes: columns.reduce((acc, c) => {
1797
+ if (c.defaultWidth != null) acc[c.key] = c.defaultWidth;
1798
+ return acc;
1799
+ }, {}),
1800
+ hidden: columns.filter((c) => c.defaultHidden).map((c) => c.key)
1801
+ };
1802
+ }
1803
+ function loadLayout(key) {
1804
+ if (!key || typeof window === "undefined") return null;
1805
+ try {
1806
+ const raw = window.localStorage.getItem(key);
1807
+ if (!raw) return null;
1808
+ return JSON.parse(raw);
1809
+ } catch {
1810
+ return null;
1811
+ }
1812
+ }
1813
+ function saveLayout(key, layout) {
1814
+ if (!key || typeof window === "undefined") return;
1815
+ try {
1816
+ window.localStorage.setItem(key, JSON.stringify(layout));
1817
+ } catch {
1818
+ }
1819
+ }
1820
+ function useTableLayout(columns, storageKey) {
1821
+ const initial = useMemo(() => {
1822
+ return loadLayout(storageKey) ?? defaultLayout(columns);
1823
+ }, []);
1824
+ const [layout, setLayout] = useState(initial);
1825
+ useEffect(() => {
1826
+ setLayout((prev) => {
1827
+ const known = new Set(columns.map((c) => c.key));
1828
+ const order = prev.order.filter((k) => known.has(k));
1829
+ columns.forEach((c) => {
1830
+ if (!order.includes(c.key)) order.push(c.key);
1831
+ });
1832
+ const sizes = {};
1833
+ Object.entries(prev.sizes).forEach(([k, v]) => {
1834
+ if (known.has(k)) sizes[k] = v;
1835
+ });
1836
+ const hidden = prev.hidden.filter((k) => known.has(k));
1837
+ return { order, sizes, hidden };
1838
+ });
1839
+ }, [columns]);
1840
+ useEffect(() => {
1841
+ saveLayout(storageKey, layout);
1842
+ }, [layout, storageKey]);
1843
+ const orderedColumns = useMemo(() => {
1844
+ const pinnedLeft = columns.filter((c) => c.pinned === "left");
1845
+ const pinnedRight = columns.filter((c) => c.pinned === "right");
1846
+ const pinnedKeys = new Set(
1847
+ [...pinnedLeft, ...pinnedRight].map((c) => c.key)
1848
+ );
1849
+ const rest = layout.order.filter((k) => !pinnedKeys.has(k)).map((k) => columns.find((c) => c.key === k)).filter(Boolean);
1850
+ return [...pinnedLeft, ...rest, ...pinnedRight];
1851
+ }, [columns, layout.order]);
1852
+ const visibleColumns = useMemo(
1853
+ () => orderedColumns.filter((c) => !layout.hidden.includes(c.key)),
1854
+ [orderedColumns, layout.hidden]
1855
+ );
1856
+ const reorder = useCallback((key, toIndex) => {
1857
+ setLayout((prev) => {
1858
+ const order = [...prev.order];
1859
+ const from = order.indexOf(key);
1860
+ if (from === -1 || from === toIndex) return prev;
1861
+ const item = order.splice(from, 1)[0];
1862
+ if (item === void 0) return prev;
1863
+ const insertAt = toIndex > from ? toIndex - 1 : toIndex;
1864
+ order.splice(Math.max(0, Math.min(insertAt, order.length)), 0, item);
1865
+ return { ...prev, order };
1866
+ });
1867
+ }, []);
1868
+ const resize = useCallback((key, px) => {
1869
+ setLayout((prev) => {
1870
+ const sizes = { ...prev.sizes };
1871
+ if (px == null) delete sizes[key];
1872
+ else sizes[key] = Math.max(40, Math.round(px));
1873
+ return { ...prev, sizes };
1874
+ });
1875
+ }, []);
1876
+ const toggleHidden = useCallback((key) => {
1877
+ setLayout((prev) => {
1878
+ const hidden = prev.hidden.includes(key) ? prev.hidden.filter((k) => k !== key) : [...prev.hidden, key];
1879
+ return { ...prev, hidden };
1880
+ });
1881
+ }, []);
1882
+ const reset = useCallback(() => {
1883
+ setLayout(defaultLayout(columns));
1884
+ }, [columns]);
1885
+ return {
1886
+ layout,
1887
+ orderedColumns,
1888
+ visibleColumns,
1889
+ reorder,
1890
+ resize,
1891
+ toggleHidden,
1892
+ reset
1893
+ };
1894
+ }
1895
+
1896
+ // src/components/data-table/filters.ts
1897
+ function defaultSearchPredicate(row, query, columns) {
1898
+ if (!query) return true;
1899
+ const q = query.toLowerCase();
1900
+ return columns.some((c) => {
1901
+ const v = c.accessor(row);
1902
+ if (v == null) return false;
1903
+ const s = v instanceof Date ? v.toLocaleString() : String(v);
1904
+ return s.toLowerCase().includes(q);
1905
+ });
1906
+ }
1907
+ function applyFilters(rows, columns, filters, cfg) {
1908
+ const searchFn = cfg.searchPredicate ?? defaultSearchPredicate;
1909
+ return rows.filter((row) => {
1910
+ if (filters.search && !searchFn(row, filters.search, columns)) return false;
1911
+ if (cfg.dateColumn && (filters.dateRange.from || filters.dateRange.to)) {
1912
+ const col = columns.find((c) => c.key === cfg.dateColumn);
1913
+ if (col) {
1914
+ const raw = col.accessor(row);
1915
+ const d = raw instanceof Date ? raw : raw ? new Date(raw) : null;
1916
+ if (!d || isNaN(d.getTime())) return false;
1917
+ const day = startOfDay(d);
1918
+ const from = filters.dateRange.from ?? day;
1919
+ const to = filters.dateRange.to ?? filters.dateRange.from ?? day;
1920
+ if (!isBetween(day, startOfDay(from), startOfDay(to))) return false;
1921
+ }
1922
+ }
1923
+ if (cfg.timeColumn && filters.time) {
1924
+ const col = columns.find((c) => c.key === cfg.timeColumn);
1925
+ if (col) {
1926
+ const raw = col.accessor(row);
1927
+ const d = raw instanceof Date ? raw : raw ? new Date(raw) : null;
1928
+ if (!d || isNaN(d.getTime())) return false;
1929
+ const rowMin = d.getHours() * 60 + d.getMinutes();
1930
+ const filterMin = filters.time.hours * 60 + filters.time.minutes;
1931
+ const tol = cfg.timeTolerance ?? 0;
1932
+ if (Math.abs(rowMin - filterMin) > tol) return false;
1933
+ }
1934
+ }
1935
+ return true;
1936
+ });
1937
+ }
1938
+ function applySort(rows, columns, sort) {
1939
+ if (!sort || !sort.dir) return rows;
1940
+ const col = columns.find((c) => c.key === sort.key);
1941
+ if (!col) return rows;
1942
+ const dirMul = sort.dir === "asc" ? 1 : -1;
1943
+ const getter = col.sortBy ?? col.accessor;
1944
+ return [...rows].sort((a, b) => {
1945
+ const av = getter(a);
1946
+ const bv = getter(b);
1947
+ if (av == null && bv == null) return 0;
1948
+ if (av == null) return 1;
1949
+ if (bv == null) return -1;
1950
+ if (av instanceof Date && bv instanceof Date) {
1951
+ return (av.getTime() - bv.getTime()) * dirMul;
1952
+ }
1953
+ if (typeof av === "number" && typeof bv === "number") {
1954
+ return (av - bv) * dirMul;
1955
+ }
1956
+ return String(av).localeCompare(String(bv)) * dirMul;
1957
+ });
1958
+ }
1959
+ function paginate(rows, page, pageSize) {
1960
+ const start = (page - 1) * pageSize;
1961
+ return rows.slice(start, start + pageSize);
1962
+ }
1963
+
1964
+ // src/components/data-table/io.ts
1965
+ function csvEscape(v) {
1966
+ if (v == null) return "";
1967
+ const s = typeof v === "string" ? v : v instanceof Date ? v.toISOString() : String(v);
1968
+ if (/[",\n\r]/.test(s)) {
1969
+ return `"${s.replace(/"/g, '""')}"`;
1970
+ }
1971
+ return s;
1972
+ }
1973
+ function toCsv(rows, cols) {
1974
+ const visible = cols.filter((c) => !c.defaultHidden);
1975
+ const header = visible.map((c) => csvEscape(c.header)).join(",");
1976
+ const body = rows.map(
1977
+ (row) => visible.map((c) => csvEscape(c.accessor(row))).join(",")
1978
+ ).join("\n");
1979
+ return body ? `${header}
1980
+ ${body}` : header;
1981
+ }
1982
+ function fromCsv(text) {
1983
+ const rows = [];
1984
+ let cur = [];
1985
+ let field = "";
1986
+ let i = 0;
1987
+ let inQuotes = false;
1988
+ const len = text.length;
1989
+ while (i < len) {
1990
+ const ch = text[i];
1991
+ if (inQuotes) {
1992
+ if (ch === '"') {
1993
+ if (text[i + 1] === '"') {
1994
+ field += '"';
1995
+ i += 2;
1996
+ continue;
1997
+ }
1998
+ inQuotes = false;
1999
+ i++;
2000
+ continue;
2001
+ }
2002
+ field += ch;
2003
+ i++;
2004
+ continue;
2005
+ }
2006
+ if (ch === '"') {
2007
+ inQuotes = true;
2008
+ i++;
2009
+ continue;
2010
+ }
2011
+ if (ch === ",") {
2012
+ cur.push(field);
2013
+ field = "";
2014
+ i++;
2015
+ continue;
2016
+ }
2017
+ if (ch === "\r") {
2018
+ i++;
2019
+ continue;
2020
+ }
2021
+ if (ch === "\n") {
2022
+ cur.push(field);
2023
+ rows.push(cur);
2024
+ cur = [];
2025
+ field = "";
2026
+ i++;
2027
+ continue;
2028
+ }
2029
+ field += ch;
2030
+ i++;
2031
+ }
2032
+ if (field.length > 0 || cur.length > 0) {
2033
+ cur.push(field);
2034
+ rows.push(cur);
2035
+ }
2036
+ if (rows.length === 0) return [];
2037
+ const head = rows[0] ?? [];
2038
+ const body = rows.slice(1);
2039
+ return body.map((r) => {
2040
+ const obj = {};
2041
+ head.forEach((h, idx) => {
2042
+ obj[h] = r[idx] ?? "";
2043
+ });
2044
+ return obj;
2045
+ });
2046
+ }
2047
+ function toJson(rows, cols) {
2048
+ const visible = cols.filter((c) => !c.defaultHidden);
2049
+ const out = rows.map((row) => {
2050
+ const obj = {};
2051
+ visible.forEach((c) => {
2052
+ obj[c.key] = c.accessor(row);
2053
+ });
2054
+ return obj;
2055
+ });
2056
+ return JSON.stringify(out, null, 2);
2057
+ }
2058
+ function fromJson(text) {
2059
+ const parsed = JSON.parse(text);
2060
+ if (!Array.isArray(parsed)) {
2061
+ throw new Error("Expected a JSON array of rows");
2062
+ }
2063
+ return parsed;
2064
+ }
2065
+ function downloadString(text, filename, mime) {
2066
+ if (typeof window === "undefined") return;
2067
+ const blob = new Blob([text], { type: mime });
2068
+ const url = URL.createObjectURL(blob);
2069
+ const a = document.createElement("a");
2070
+ a.href = url;
2071
+ a.download = filename;
2072
+ document.body.appendChild(a);
2073
+ a.click();
2074
+ a.remove();
2075
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
2076
+ }
2077
+ function fontStyleFor(spec) {
2078
+ if (!spec) return void 0;
2079
+ if (typeof spec === "string") return { fontFamily: spec };
2080
+ const s = {};
2081
+ if (spec.family) s.fontFamily = spec.family;
2082
+ if (spec.size != null)
2083
+ s.fontSize = typeof spec.size === "number" ? `${spec.size}px` : spec.size;
2084
+ if (spec.weight != null) s.fontWeight = spec.weight;
2085
+ if (spec.letterSpacing) s.letterSpacing = spec.letterSpacing;
2086
+ if (spec.featureSettings) s.fontFeatureSettings = spec.featureSettings;
2087
+ return s;
2088
+ }
2089
+ function renderCellValue(value, type) {
2090
+ if (value == null) return "";
2091
+ if (value instanceof Date) {
2092
+ if (type === "time") {
2093
+ return value.toLocaleTimeString([], {
2094
+ hour: "2-digit",
2095
+ minute: "2-digit"
2096
+ });
2097
+ }
2098
+ if (type === "date") {
2099
+ return value.toLocaleDateString();
2100
+ }
2101
+ return value.toLocaleString();
2102
+ }
2103
+ return String(value);
2104
+ }
2105
+ function DataTable({
2106
+ data,
2107
+ columns,
2108
+ getRowId,
2109
+ visibleRows = 7,
2110
+ rowHeight = 44,
2111
+ stickyHeader = true,
2112
+ density = "cozy",
2113
+ loading,
2114
+ empty,
2115
+ fitColumns = false,
2116
+ search,
2117
+ dateFilter,
2118
+ timeFilter,
2119
+ pagination,
2120
+ reorderable = true,
2121
+ resizable = true,
2122
+ columnMenu = true,
2123
+ dataIO,
2124
+ headerFont,
2125
+ rowHeaderFont,
2126
+ cellFont,
2127
+ storageKey,
2128
+ className = "",
2129
+ toolbarExtras
2130
+ }) {
2131
+ const {
2132
+ layout,
2133
+ orderedColumns,
2134
+ visibleColumns,
2135
+ reorder,
2136
+ resize,
2137
+ toggleHidden,
2138
+ reset
2139
+ } = useTableLayout(columns, storageKey);
2140
+ const [filters, setFilters] = useState({
2141
+ search: "",
2142
+ dateRange: { from: null, to: null },
2143
+ time: null
2144
+ });
2145
+ const [sort, setSort] = useState(null);
2146
+ const pageSize = pagination === false ? Infinity : pagination?.pageSize ?? 25;
2147
+ const [page, setPage] = useState(1);
2148
+ const filtered = useMemo(
2149
+ () => applyFilters(data, columns, filters, {
2150
+ dateColumn: dateFilter?.column,
2151
+ timeColumn: timeFilter?.column,
2152
+ timeTolerance: timeFilter?.toleranceMinutes,
2153
+ searchPredicate: search?.predicate
2154
+ }),
2155
+ [data, columns, filters, dateFilter?.column, timeFilter?.column, timeFilter?.toleranceMinutes, search?.predicate]
2156
+ );
2157
+ const sorted = useMemo(() => applySort(filtered, columns, sort), [filtered, columns, sort]);
2158
+ const pageCount = pagination === false ? 1 : Math.max(1, Math.ceil(sorted.length / pageSize));
2159
+ const currentPage = Math.min(page, pageCount);
2160
+ const pageRows = useMemo(
2161
+ () => pagination === false ? sorted : paginate(sorted, currentPage, pageSize),
2162
+ [pagination, sorted, currentPage, pageSize]
2163
+ );
2164
+ const [dragKey, setDragKey] = useState(null);
2165
+ const [dropIndex, setDropIndex] = useState(null);
2166
+ const onDragStart = (e, key) => {
2167
+ if (!reorderable) return;
2168
+ setDragKey(key);
2169
+ e.dataTransfer.effectAllowed = "move";
2170
+ try {
2171
+ e.dataTransfer.setData("text/plain", key);
2172
+ } catch {
2173
+ }
2174
+ };
2175
+ const onDragOver = (e, idx) => {
2176
+ if (!reorderable || !dragKey) return;
2177
+ e.preventDefault();
2178
+ e.dataTransfer.dropEffect = "move";
2179
+ const rect = e.currentTarget.getBoundingClientRect();
2180
+ const isRight = e.clientX - rect.left > rect.width / 2;
2181
+ setDropIndex(idx + (isRight ? 1 : 0));
2182
+ };
2183
+ const onDrop = (e) => {
2184
+ if (!reorderable || !dragKey || dropIndex == null) return;
2185
+ e.preventDefault();
2186
+ reorder(dragKey, dropIndex);
2187
+ setDragKey(null);
2188
+ setDropIndex(null);
2189
+ };
2190
+ const onDragEnd = () => {
2191
+ setDragKey(null);
2192
+ setDropIndex(null);
2193
+ };
2194
+ const resizingKey = useRef(null);
2195
+ const resizeStartX = useRef(0);
2196
+ const resizeStartW = useRef(0);
2197
+ const beginResize = (e, key) => {
2198
+ if (!resizable) return;
2199
+ e.stopPropagation();
2200
+ e.preventDefault();
2201
+ const th = e.currentTarget.parentElement ?? null;
2202
+ const startW = th ? th.getBoundingClientRect().width : 120;
2203
+ resizingKey.current = key;
2204
+ resizeStartX.current = e.clientX;
2205
+ resizeStartW.current = startW;
2206
+ window.addEventListener("pointermove", onResizeMove);
2207
+ window.addEventListener("pointerup", endResize);
2208
+ };
2209
+ const onResizeMove = (e) => {
2210
+ if (!resizingKey.current) return;
2211
+ const dx = e.clientX - resizeStartX.current;
2212
+ const col = columns.find((c) => c.key === resizingKey.current);
2213
+ const min = col?.minWidth ?? 80;
2214
+ const max = col?.maxWidth ?? 2e3;
2215
+ const next = Math.max(min, Math.min(max, resizeStartW.current + dx));
2216
+ resize(resizingKey.current, next);
2217
+ };
2218
+ const endResize = () => {
2219
+ resizingKey.current = null;
2220
+ window.removeEventListener("pointermove", onResizeMove);
2221
+ window.removeEventListener("pointerup", endResize);
2222
+ };
2223
+ const doubleClickReset = (key) => resize(key, null);
2224
+ const cycleSort = useCallback((key) => {
2225
+ setSort((prev) => {
2226
+ if (!prev || prev.key !== key) return { key, dir: "asc" };
2227
+ if (prev.dir === "asc") return { key, dir: "desc" };
2228
+ return null;
2229
+ });
2230
+ }, []);
2231
+ const fileInput = useRef(null);
2232
+ const [ioFlash, setIoFlash] = useState(null);
2233
+ const flashTimer = useRef(null);
2234
+ const flash = (text, ms = 1400) => {
2235
+ if (flashTimer.current) clearTimeout(flashTimer.current);
2236
+ setIoFlash(text);
2237
+ flashTimer.current = setTimeout(() => setIoFlash(null), ms);
2238
+ };
2239
+ const exportRows = (format) => {
2240
+ if (!dataIO?.export) return;
2241
+ const scope = dataIO.export.scope ?? "filtered";
2242
+ let rows;
2243
+ if (scope === "all") rows = data;
2244
+ else if (scope === "page") rows = pageRows;
2245
+ else rows = sorted;
2246
+ const cols = visibleColumns;
2247
+ const text = dataIO.export.serialize ? dataIO.export.serialize(rows, cols, format) : format === "csv" ? toCsv(rows, cols) : toJson(rows, cols);
2248
+ const baseName = typeof dataIO.export.filename === "function" ? dataIO.export.filename() : dataIO.export.filename ?? "table-export";
2249
+ const filename = `${baseName}.${format}`;
2250
+ const mime = format === "csv" ? "text/csv;charset=utf-8" : "application/json";
2251
+ downloadString(text, filename, mime);
2252
+ flash("Exported");
2253
+ };
2254
+ const onPickFile = async (file) => {
2255
+ if (!dataIO?.import) return;
2256
+ const text = await file.text();
2257
+ try {
2258
+ const parsed = dataIO.import.parse ? await dataIO.import.parse(text, file) : file.name.endsWith(".json") ? fromJson(text) : fromCsv(text);
2259
+ dataIO.import.onImport(parsed, {
2260
+ mode: dataIO.import.mode ?? "replace",
2261
+ file
2262
+ });
2263
+ flash(`Imported ${parsed.length} ${parsed.length === 1 ? "row" : "rows"}`);
2264
+ } catch (err) {
2265
+ dataIO.import.onError?.(err, file);
2266
+ flash("Couldn't read file", 2e3);
2267
+ }
2268
+ };
2269
+ const showToolbar = search?.enabled || !!dateFilter || !!timeFilter || columnMenu || !!dataIO?.export?.enabled || !!dataIO?.import?.enabled || !!toolbarExtras;
2270
+ const exportFormats = dataIO?.export?.formats ?? ["csv", "json"];
2271
+ const [exportMenuOpen, setExportMenuOpen] = useState(false);
2272
+ return /* @__PURE__ */ jsxs("div", { className: ["royui-dt", className].filter(Boolean).join(" "), children: [
2273
+ showToolbar && /* @__PURE__ */ jsxs("div", { className: "royui-dt__toolbar", children: [
2274
+ /* @__PURE__ */ jsxs("div", { className: "royui-dt__toolbar-left", children: [
2275
+ search?.enabled && /* @__PURE__ */ jsx(
2276
+ TableSearch,
2277
+ {
2278
+ value: filters.search,
2279
+ onChange: (v) => {
2280
+ setFilters((f) => ({ ...f, search: v }));
2281
+ setPage(1);
2282
+ },
2283
+ placeholder: search.placeholder,
2284
+ debounceMs: search.debounceMs
2285
+ }
2286
+ ),
2287
+ dateFilter && /* @__PURE__ */ jsx(
2288
+ DateRangePicker,
2289
+ {
2290
+ value: filters.dateRange,
2291
+ onChange: (r) => {
2292
+ setFilters((f) => ({ ...f, dateRange: r }));
2293
+ setPage(1);
2294
+ },
2295
+ monthsVisible: dateFilter.monthsVisible ?? 2,
2296
+ placeholder: dateFilter.placeholder ?? "Date range"
2297
+ }
2298
+ ),
2299
+ timeFilter && /* @__PURE__ */ jsx(
2300
+ TimePicker,
2301
+ {
2302
+ value: filters.time,
2303
+ onChange: (t) => {
2304
+ setFilters((f) => ({ ...f, time: t }));
2305
+ setPage(1);
2306
+ },
2307
+ variant: timeFilter.variant ?? "analog",
2308
+ hourCycle: timeFilter.hourCycle ?? 24,
2309
+ placeholder: timeFilter.placeholder ?? "Time"
2310
+ }
2311
+ )
2312
+ ] }),
2313
+ /* @__PURE__ */ jsxs("div", { className: "royui-dt__toolbar-right", children: [
2314
+ toolbarExtras,
2315
+ columnMenu && /* @__PURE__ */ jsx(
2316
+ ColumnMenu,
2317
+ {
2318
+ columns: orderedColumns,
2319
+ layout,
2320
+ onToggle: toggleHidden,
2321
+ onReset: reset
2322
+ }
2323
+ ),
2324
+ (dataIO?.export?.enabled || dataIO?.import?.enabled) && /* @__PURE__ */ jsx("span", { className: "royui-dt__sep", "aria-hidden": true, children: "\xB7" }),
2325
+ dataIO?.export?.enabled && /* @__PURE__ */ jsxs("div", { className: "royui-dt-io", children: [
2326
+ /* @__PURE__ */ jsx(
2327
+ "button",
2328
+ {
2329
+ type: "button",
2330
+ className: "royui-dt-io__btn",
2331
+ onClick: () => {
2332
+ const only = exportFormats[0];
2333
+ if (exportFormats.length === 1 && only) {
2334
+ exportRows(only);
2335
+ } else {
2336
+ setExportMenuOpen((o) => !o);
2337
+ }
2338
+ },
2339
+ children: ioFlash && ioFlash.startsWith("Export") ? ioFlash : "Export"
2340
+ }
2341
+ ),
2342
+ exportMenuOpen && exportFormats.length > 1 && /* @__PURE__ */ jsx(
2343
+ "div",
2344
+ {
2345
+ className: "royui-dt-io__menu",
2346
+ onMouseLeave: () => setExportMenuOpen(false),
2347
+ children: exportFormats.map((f) => /* @__PURE__ */ jsx(
2348
+ "button",
2349
+ {
2350
+ type: "button",
2351
+ className: "royui-dt-io__menu-item",
2352
+ onClick: () => {
2353
+ exportRows(f);
2354
+ setExportMenuOpen(false);
2355
+ },
2356
+ children: f.toUpperCase()
2357
+ },
2358
+ f
2359
+ ))
2360
+ }
2361
+ )
2362
+ ] }),
2363
+ dataIO?.import?.enabled && /* @__PURE__ */ jsxs(Fragment, { children: [
2364
+ /* @__PURE__ */ jsx(
2365
+ "button",
2366
+ {
2367
+ type: "button",
2368
+ className: "royui-dt-io__btn",
2369
+ onClick: () => fileInput.current?.click(),
2370
+ children: ioFlash && (ioFlash.startsWith("Import") || ioFlash.startsWith("Couldn't")) ? ioFlash : "Import"
2371
+ }
2372
+ ),
2373
+ /* @__PURE__ */ jsx(
2374
+ "input",
2375
+ {
2376
+ ref: fileInput,
2377
+ type: "file",
2378
+ accept: dataIO.import.accept ?? ".csv,.json",
2379
+ style: { display: "none" },
2380
+ onChange: (e) => {
2381
+ const f = e.target.files?.[0];
2382
+ if (f) onPickFile(f);
2383
+ e.target.value = "";
2384
+ }
2385
+ }
2386
+ )
2387
+ ] })
2388
+ ] })
2389
+ ] }),
2390
+ /* @__PURE__ */ jsxs(
2391
+ Table,
2392
+ {
2393
+ visibleRows,
2394
+ rowHeight,
2395
+ stickyHeader,
2396
+ density,
2397
+ loading,
2398
+ empty,
2399
+ isEmpty: pageRows.length === 0,
2400
+ fitColumns,
2401
+ headerFont,
2402
+ rowHeaderFont,
2403
+ cellFont,
2404
+ children: [
2405
+ /* @__PURE__ */ jsx("colgroup", { children: visibleColumns.map((c) => {
2406
+ const w = fitColumns ? void 0 : layout.sizes[c.key];
2407
+ return /* @__PURE__ */ jsx(
2408
+ "col",
2409
+ {
2410
+ style: w ? { width: w } : void 0,
2411
+ className: dragKey === c.key ? "royui-dt__col--dragging" : void 0
2412
+ },
2413
+ c.key
2414
+ );
2415
+ }) }),
2416
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
2417
+ visibleColumns.map((c, idx) => {
2418
+ const isDragging = dragKey === c.key;
2419
+ const showDropBefore = dropIndex === idx;
2420
+ const sortDir = sort && sort.key === c.key && sort.dir ? sort.dir : null;
2421
+ const canReorder = reorderable && c.reorderable !== false && !c.pinned;
2422
+ const canResize = resizable && c.resizable !== false;
2423
+ return /* @__PURE__ */ jsxs(
2424
+ TableHead,
2425
+ {
2426
+ align: c.align ?? (c.type === "number" ? "right" : "left"),
2427
+ className: [
2428
+ "royui-dt__th",
2429
+ canReorder && "royui-dt__th--reorderable",
2430
+ canResize && "royui-dt__th--resizable",
2431
+ isDragging && "royui-dt__th--dragging",
2432
+ showDropBefore && "royui-dt__th--drop-before"
2433
+ ].filter(Boolean).join(" "),
2434
+ draggable: canReorder,
2435
+ onDragStart: (e) => onDragStart(e, c.key),
2436
+ onDragOver: (e) => onDragOver(e, idx),
2437
+ onDrop,
2438
+ onDragEnd,
2439
+ title: canReorder && canResize ? "Drag to reorder \xB7 drag the right edge to resize" : canReorder ? "Drag to reorder" : canResize ? "Drag the right edge to resize" : void 0,
2440
+ children: [
2441
+ /* @__PURE__ */ jsxs(
2442
+ "span",
2443
+ {
2444
+ className: "royui-dt__th-inner",
2445
+ onClick: () => cycleSort(c.key),
2446
+ role: "button",
2447
+ tabIndex: 0,
2448
+ onKeyDown: (e) => {
2449
+ if (e.key === "Enter" || e.key === " ") {
2450
+ e.preventDefault();
2451
+ cycleSort(c.key);
2452
+ }
2453
+ },
2454
+ children: [
2455
+ /* @__PURE__ */ jsx("span", { className: "royui-dt__th-label", children: c.header }),
2456
+ sortDir && /* @__PURE__ */ jsx(
2457
+ "span",
2458
+ {
2459
+ className: `royui-dt__sort-indicator royui-dt__sort-indicator--${sortDir}`,
2460
+ "aria-hidden": true
2461
+ }
2462
+ )
2463
+ ]
2464
+ }
2465
+ ),
2466
+ canResize && /* @__PURE__ */ jsx(
2467
+ "span",
2468
+ {
2469
+ className: "royui-dt__resize",
2470
+ onPointerDown: (e) => beginResize(e, c.key),
2471
+ onDoubleClick: () => doubleClickReset(c.key),
2472
+ "aria-hidden": true,
2473
+ children: /* @__PURE__ */ jsx("span", { className: "royui-dt__resize-bar", "aria-hidden": true })
2474
+ }
2475
+ )
2476
+ ]
2477
+ },
2478
+ c.key
2479
+ );
2480
+ }),
2481
+ dropIndex === visibleColumns.length && /* @__PURE__ */ jsx(TableHead, { className: "royui-dt__th--drop-end", "aria-hidden": true })
2482
+ ] }) }),
2483
+ /* @__PURE__ */ jsx(TableBody, { children: pageRows.map((row, i) => {
2484
+ const id = getRowId ? getRowId(row, i) : row?.["id"] != null ? String(row["id"]) : i;
2485
+ return /* @__PURE__ */ jsx(TableRow, { children: visibleColumns.map((c) => {
2486
+ const value = c.accessor(row);
2487
+ const display = c.cell ? c.cell(value, row) : renderCellValue(value, c.type);
2488
+ const cellStyle = fontStyleFor(c.font);
2489
+ const isNum = c.type === "number";
2490
+ return /* @__PURE__ */ jsx(
2491
+ TableCell,
2492
+ {
2493
+ isRowHeader: c.isRowHeader,
2494
+ align: c.align ?? (isNum ? "right" : "left"),
2495
+ className: isNum ? "royui-table__td--num" : void 0,
2496
+ style: cellStyle,
2497
+ children: display
2498
+ },
2499
+ c.key
2500
+ );
2501
+ }) }, id);
2502
+ }) })
2503
+ ]
2504
+ }
2505
+ ),
2506
+ pagination !== false && pageCount > 1 && /* @__PURE__ */ jsx("div", { className: "royui-dt__pagination", children: /* @__PURE__ */ jsx(
2507
+ Pagination,
2508
+ {
2509
+ page: currentPage,
2510
+ pageCount,
2511
+ onPageChange: setPage,
2512
+ siblingCount: pagination?.siblingCount ?? 1,
2513
+ showSummary: pagination?.showSummary ?? true,
2514
+ summaryRender: (p, pc) => `${(p - 1) * pageSize + 1}\u2013${Math.min(p * pageSize, sorted.length)} of ${sorted.length}`
2515
+ }
2516
+ ) })
2517
+ ] });
2518
+ }
378
2519
 
379
- export { GradientButton, MadeBy, Popover, TextMorph, TreeNav, TreeNavItem };
2520
+ export { AnalogClock, Button, ColumnMenu, DEFAULT_PRESETS, DataTable, DateRangePicker, DigitalClock, GradientButton, MadeBy, Pagination, Popover, Spinner, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, TableSearch, TextMorph, TimePicker, TreeNav, TreeNavItem, addDays, addMonths, downloadString, formatMonthYear, formatRange, formatShort, formatTime, fromCsv, fromJson, isBetween, isSameDay, startOfDay, toCsv, toJson };
380
2521
  //# sourceMappingURL=index.js.map
381
2522
  //# sourceMappingURL=index.js.map