@roy-ui/ui 0.0.5 → 0.0.6

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