@apotech-os/ui-sdk 0.10.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,14 +1,22 @@
1
1
  import { clsx } from 'clsx';
2
2
  import { twMerge } from 'tailwind-merge';
3
- import * as React from 'react';
3
+ import * as React2 from 'react';
4
4
  import { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react';
5
5
  import { Dialog } from '@base-ui/react/dialog';
6
6
  import { Menu } from '@base-ui/react/menu';
7
+ import { Checkbox as Checkbox$1 } from '@base-ui/react/checkbox';
7
8
  import { Popover as Popover$1 } from '@base-ui/react/popover';
9
+ import { Select as Select$1 } from '@base-ui/react/select';
8
10
  import { Switch as Switch$1 } from '@base-ui/react/switch';
9
11
  import { Tabs as Tabs$1 } from '@base-ui/react/tabs';
10
12
  import { cva } from 'class-variance-authority';
11
- import { jsx, jsxs } from 'react/jsx-runtime';
13
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
14
+ import { useReactTable, getFilteredRowModel, getSortedRowModel, getCoreRowModel, flexRender } from '@tanstack/react-table';
15
+ import { useVirtualizer } from '@tanstack/react-virtual';
16
+ import { fr } from 'date-fns/locale/fr';
17
+ import { DayPicker } from 'react-day-picker';
18
+ import { subYears, subMonths, endOfMonth, startOfMonth, addQuarters, endOfQuarter, startOfQuarter, endOfYear, startOfYear, differenceInCalendarDays, subDays, parse, format, addDays } from 'date-fns';
19
+ import { BarChart as BarChart$1, Bar, LineChart as LineChart$1, Line, AreaChart as AreaChart$1, Area, FunnelChart as FunnelChart$1, Funnel, Cell, LabelList, ResponsiveContainer, Tooltip, Legend, CartesianGrid, XAxis, YAxis } from 'recharts';
12
20
 
13
21
  // src/cn.ts
14
22
  function cn(...inputs) {
@@ -295,7 +303,7 @@ function downloadCsv(rows, filename) {
295
303
  downloadBlob(blob, filename);
296
304
  }
297
305
  function triggerProps(trigger) {
298
- if (React.isValidElement(trigger)) {
306
+ if (React2.isValidElement(trigger)) {
299
307
  return { render: trigger, nativeButton: trigger.type === "button" || trigger.type === Button };
300
308
  }
301
309
  return { render: /* @__PURE__ */ jsx("span", { children: trigger }), nativeButton: false };
@@ -309,7 +317,7 @@ var buttonVariants = cva(
309
317
  secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
310
318
  outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
311
319
  ghost: "hover:bg-accent hover:text-accent-foreground",
312
- destructive: "bg-destructive text-white hover:bg-destructive/90"
320
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90"
313
321
  },
314
322
  size: {
315
323
  default: "h-9 px-4 py-2",
@@ -321,7 +329,7 @@ var buttonVariants = cva(
321
329
  defaultVariants: { variant: "default", size: "default" }
322
330
  }
323
331
  );
324
- var Button = React.forwardRef(
332
+ var Button = React2.forwardRef(
325
333
  ({ className, variant, size, ...props }, ref) => /* @__PURE__ */ jsx("button", { ref, className: cn(buttonVariants({ variant, size, className })), ...props })
326
334
  );
327
335
  Button.displayName = "Button";
@@ -375,7 +383,7 @@ function CardContent({ className, ...props }) {
375
383
  function CardFooter({ className, ...props }) {
376
384
  return /* @__PURE__ */ jsx("div", { className: cn("flex items-center p-6 pt-0", className), ...props });
377
385
  }
378
- var Input = React.forwardRef(({ className, type, ...props }, ref) => /* @__PURE__ */ jsx(
386
+ var Input = React2.forwardRef(({ className, type, ...props }, ref) => /* @__PURE__ */ jsx(
379
387
  "input",
380
388
  {
381
389
  type,
@@ -433,21 +441,55 @@ function TableHead({ className, ...props }) {
433
441
  function TableCell({ className, ...props }) {
434
442
  return /* @__PURE__ */ jsx("td", { className: cn("p-4 align-middle", className), ...props });
435
443
  }
436
- var Select = React.forwardRef(
437
- ({ className, children, ...props }, ref) => /* @__PURE__ */ jsx(
438
- "select",
444
+ function Select({
445
+ value,
446
+ defaultValue,
447
+ onValueChange,
448
+ items,
449
+ placeholder = "Choisir\u2026",
450
+ disabled,
451
+ name,
452
+ className,
453
+ ...props
454
+ }) {
455
+ return /* @__PURE__ */ jsxs(
456
+ Select$1.Root,
439
457
  {
440
- ref,
441
- className: cn(
442
- "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
443
- className
444
- ),
445
- ...props,
446
- children
458
+ items,
459
+ value,
460
+ defaultValue,
461
+ onValueChange: (next) => onValueChange?.(String(next)),
462
+ disabled,
463
+ name,
464
+ children: [
465
+ /* @__PURE__ */ jsxs(
466
+ Select$1.Trigger,
467
+ {
468
+ className: cn(
469
+ "flex h-9 w-full cursor-pointer items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
470
+ className
471
+ ),
472
+ ...props,
473
+ children: [
474
+ /* @__PURE__ */ jsx(Select$1.Value, { placeholder }),
475
+ /* @__PURE__ */ jsx(Select$1.Icon, { "aria-hidden": true, className: "text-xs text-muted-foreground", children: "\u25BE" })
476
+ ]
477
+ }
478
+ ),
479
+ /* @__PURE__ */ jsx(Select$1.Portal, { children: /* @__PURE__ */ jsx(Select$1.Positioner, { className: "z-50", sideOffset: 4, children: /* @__PURE__ */ jsx(Select$1.Popup, { className: "max-h-72 min-w-(--anchor-width) overflow-auto rounded-md border bg-card p-1 shadow-lg", children: items.map((item) => /* @__PURE__ */ jsx(
480
+ Select$1.Item,
481
+ {
482
+ value: item.value,
483
+ disabled: item.disabled,
484
+ className: "cursor-pointer rounded px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
485
+ children: /* @__PURE__ */ jsx(Select$1.ItemText, { children: item.label })
486
+ },
487
+ item.value
488
+ )) }) }) })
489
+ ]
447
490
  }
448
- )
449
- );
450
- Select.displayName = "Select";
491
+ );
492
+ }
451
493
  function Tabs({ defaultValue, value, onValueChange, className, children }) {
452
494
  return /* @__PURE__ */ jsx(
453
495
  Tabs$1.Root,
@@ -500,7 +542,7 @@ function TabsContent({
500
542
  }
501
543
  function Modal({ open, onClose, title, children, className }) {
502
544
  return /* @__PURE__ */ jsx(Dialog.Root, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
503
- /* @__PURE__ */ jsx(Dialog.Backdrop, { "aria-label": "Close", className: "fixed inset-0 z-50 bg-foreground/40" }),
545
+ /* @__PURE__ */ jsx(Dialog.Backdrop, { "aria-label": "Close", className: "fixed inset-0 z-50 bg-overlay" }),
504
546
  /* @__PURE__ */ jsxs(
505
547
  Dialog.Popup,
506
548
  {
@@ -541,7 +583,7 @@ function Spinner({ className, size = "md" }) {
541
583
  }
542
584
  );
543
585
  }
544
- var Link = React.forwardRef(
586
+ var Link = React2.forwardRef(
545
587
  ({ className, variant = "default", ...props }, ref) => /* @__PURE__ */ jsx(
546
588
  "a",
547
589
  {
@@ -580,33 +622,6 @@ function Switch({
580
622
  }
581
623
  );
582
624
  }
583
- function DateRangePicker({ value, onChange, className, disabled }) {
584
- return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2", className), children: [
585
- /* @__PURE__ */ jsx(
586
- "input",
587
- {
588
- type: "date",
589
- disabled,
590
- value: value.from,
591
- max: value.to || void 0,
592
- onChange: (e) => onChange({ ...value, from: e.target.value }),
593
- className: "flex h-9 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
594
- }
595
- ),
596
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "\u2192" }),
597
- /* @__PURE__ */ jsx(
598
- "input",
599
- {
600
- type: "date",
601
- disabled,
602
- value: value.to,
603
- min: value.from || void 0,
604
- onChange: (e) => onChange({ ...value, to: e.target.value }),
605
- className: "flex h-9 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
606
- }
607
- )
608
- ] });
609
- }
610
625
  function IconX({ className }) {
611
626
  return /* @__PURE__ */ jsx(
612
627
  "svg",
@@ -660,43 +675,69 @@ function IconCheck({ className }) {
660
675
  }
661
676
  );
662
677
  }
663
- var Textarea = React.forwardRef(
664
- function Textarea2({ className, ...props }, ref) {
665
- return /* @__PURE__ */ jsx(
666
- "textarea",
667
- {
668
- ref,
669
- className: cn(
670
- "flex min-h-20 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
671
- className
672
- ),
673
- ...props
674
- }
675
- );
676
- }
677
- );
678
- var Checkbox = React.forwardRef(
679
- function Checkbox2({ className, label, onCheckedChange, ...props }, ref) {
680
- const input = /* @__PURE__ */ jsx(
681
- "input",
682
- {
683
- ref,
684
- type: "checkbox",
685
- className: cn(
686
- "size-4 shrink-0 cursor-pointer rounded border-input accent-primary disabled:cursor-not-allowed disabled:opacity-50",
687
- className
688
- ),
689
- onChange: (e) => onCheckedChange?.(e.target.checked),
690
- ...props
691
- }
692
- );
693
- if (!label) return input;
694
- return /* @__PURE__ */ jsxs("label", { className: "inline-flex cursor-pointer items-center gap-2 text-sm", children: [
695
- input,
696
- /* @__PURE__ */ jsx("span", { children: label })
697
- ] });
698
- }
699
- );
678
+ var Textarea = React2.forwardRef(function Textarea2({ className, ...props }, ref) {
679
+ return /* @__PURE__ */ jsx(
680
+ "textarea",
681
+ {
682
+ ref,
683
+ className: cn(
684
+ "flex min-h-20 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
685
+ className
686
+ ),
687
+ ...props
688
+ }
689
+ );
690
+ });
691
+ function Checkbox({
692
+ checked,
693
+ defaultChecked,
694
+ onCheckedChange,
695
+ disabled,
696
+ label,
697
+ name,
698
+ className,
699
+ ...props
700
+ }) {
701
+ const labelId = React2.useId();
702
+ const control = React2.useRef(null);
703
+ const box = /* @__PURE__ */ jsx(
704
+ Checkbox$1.Root,
705
+ {
706
+ ref: control,
707
+ "aria-labelledby": label ? labelId : void 0,
708
+ checked,
709
+ defaultChecked,
710
+ onCheckedChange: (next) => onCheckedChange?.(next),
711
+ disabled,
712
+ name,
713
+ className: cn(
714
+ "flex size-4 shrink-0 cursor-pointer items-center justify-center rounded border border-input text-primary-foreground transition-colors data-[checked]:border-primary data-[checked]:bg-primary data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
715
+ className
716
+ ),
717
+ ...props,
718
+ children: /* @__PURE__ */ jsx(Checkbox$1.Indicator, { className: "text-[10px] leading-none", children: "\u2713" })
719
+ }
720
+ );
721
+ if (!label) return box;
722
+ return (
723
+ // Not a wrapping <label>: Base UI would associate it AND keep its own label,
724
+ // so the text and the control would both answer to the same name and
725
+ // `getByLabelText` would go ambiguous. Pointing `aria-labelledby` at the text
726
+ // names the control once; the click handler keeps the label clickable.
727
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2 text-sm", children: [
728
+ box,
729
+ /* @__PURE__ */ jsx(
730
+ "span",
731
+ {
732
+ id: labelId,
733
+ className: "cursor-pointer",
734
+ onClick: () => control.current?.click(),
735
+ children: label
736
+ }
737
+ )
738
+ ] })
739
+ );
740
+ }
700
741
  function SidePanel({
701
742
  open,
702
743
  onClose,
@@ -706,7 +747,7 @@ function SidePanel({
706
747
  className
707
748
  }) {
708
749
  return /* @__PURE__ */ jsx(Dialog.Root, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
709
- /* @__PURE__ */ jsx(Dialog.Backdrop, { "aria-label": "Fermer", className: "fixed inset-0 z-50 bg-foreground/40" }),
750
+ /* @__PURE__ */ jsx(Dialog.Backdrop, { "aria-label": "Fermer", className: "fixed inset-0 z-50 bg-overlay" }),
710
751
  /* @__PURE__ */ jsxs(
711
752
  Dialog.Popup,
712
753
  {
@@ -744,9 +785,9 @@ function CopyButton({
744
785
  size = "sm",
745
786
  className
746
787
  }) {
747
- const [state, setState] = React.useState("idle");
748
- const timeout = React.useRef(void 0);
749
- React.useEffect(() => () => clearTimeout(timeout.current), []);
788
+ const [state, setState] = React2.useState("idle");
789
+ const timeout = React2.useRef(void 0);
790
+ React2.useEffect(() => () => clearTimeout(timeout.current), []);
750
791
  async function copy() {
751
792
  const ok = await copyToClipboard(value);
752
793
  setState(ok ? "copied" : "error");
@@ -818,6 +859,1286 @@ function DropdownMenu({ trigger, items, align = "end", className }) {
818
859
  ) }) })
819
860
  ] });
820
861
  }
862
+ var EMPTY_STATE = {
863
+ search: "",
864
+ sort: null,
865
+ filters: {},
866
+ hidden: [],
867
+ selected: []
868
+ };
869
+ var MIN_COLUMN_WIDTH_PX = 64;
870
+ var DEFAULT_VIRTUALIZE_FROM = 100;
871
+ var ROW_HEIGHT_PX = 53;
872
+ var VIRTUAL_VIEWPORT_PX = 640;
873
+ function useDataTableState(controlled, initial, onStateChange) {
874
+ const [internal, setInternal] = React2.useState({ ...EMPTY_STATE, ...initial });
875
+ const state = React2.useMemo(
876
+ () => ({ ...EMPTY_STATE, ...initial, ...internal, ...controlled }),
877
+ [controlled, initial, internal]
878
+ );
879
+ const patch = (next) => {
880
+ const merged = { ...state, ...next };
881
+ setInternal(merged);
882
+ onStateChange?.(merged);
883
+ };
884
+ return [state, patch];
885
+ }
886
+ function DataTable({
887
+ columns,
888
+ rows,
889
+ rowId,
890
+ state: controlled,
891
+ defaultState,
892
+ onStateChange,
893
+ facets = [],
894
+ searchPlaceholder = "Rechercher\u2026",
895
+ selectable = false,
896
+ onRowClick,
897
+ pagination,
898
+ manual = false,
899
+ loading = false,
900
+ error,
901
+ empty = "Aucun r\xE9sultat.",
902
+ virtualizeFrom = DEFAULT_VIRTUALIZE_FROM,
903
+ className
904
+ }) {
905
+ const [state, patch] = useDataTableState(controlled, defaultState, onStateChange);
906
+ const [widths, setWidths] = React2.useState({});
907
+ const hidden = React2.useMemo(() => new Set(state.hidden), [state.hidden]);
908
+ const selected = React2.useMemo(() => new Set(state.selected), [state.selected]);
909
+ const visibleColumns = React2.useMemo(
910
+ () => columns.filter((column) => !hidden.has(column.key)),
911
+ [columns, hidden]
912
+ );
913
+ const columnDefs = React2.useMemo(
914
+ () => visibleColumns.map((column) => ({
915
+ id: column.key,
916
+ accessorFn: (row) => column.value ? column.value(row) : row[column.key],
917
+ enableSorting: column.sortable ?? false,
918
+ size: column.width,
919
+ cell: ({ row }) => column.cell ? column.cell(row.original) : String(row.original[column.key] ?? "")
920
+ })),
921
+ [visibleColumns]
922
+ );
923
+ const sorting = React2.useMemo(
924
+ () => state.sort ? [{ id: state.sort.key, desc: state.sort.direction === "desc" }] : [],
925
+ [state.sort]
926
+ );
927
+ const table = useReactTable({
928
+ data: rows,
929
+ columns: columnDefs,
930
+ state: { sorting, globalFilter: manual ? "" : state.search },
931
+ getRowId: (row) => rowId(row),
932
+ getCoreRowModel: getCoreRowModel(),
933
+ ...manual ? { manualSorting: true, manualFiltering: true } : { getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel() }
934
+ });
935
+ const facetedRows = React2.useMemo(() => {
936
+ const active = Object.entries(state.filters).filter(([, filter]) => isApplied(filter));
937
+ if (manual || active.length === 0) return table.getRowModel().rows;
938
+ return table.getRowModel().rows.filter(
939
+ (row) => active.every(([key, filter]) => {
940
+ const column = columns.find((c) => c.key === key);
941
+ const raw = column?.value ? column.value(row.original) : row.original[key];
942
+ return matches(raw, filter);
943
+ })
944
+ );
945
+ }, [columns, manual, state.filters, table.getRowModel().rows]);
946
+ const bodyRef = React2.useRef(null);
947
+ const longEnoughToVirtualise = facetedRows.length > virtualizeFrom;
948
+ const virtualizer = useVirtualizer({
949
+ count: facetedRows.length,
950
+ getScrollElement: () => bodyRef.current,
951
+ estimateSize: () => ROW_HEIGHT_PX,
952
+ overscan: 8,
953
+ enabled: longEnoughToVirtualise
954
+ });
955
+ const virtualItems = virtualizer.getVirtualItems();
956
+ const virtualise = longEnoughToVirtualise && virtualItems.length > 0;
957
+ const toggleSort = (key) => {
958
+ const next = state.sort?.key !== key ? { key, direction: "asc" } : state.sort.direction === "asc" ? { key, direction: "desc" } : null;
959
+ patch({ sort: next });
960
+ };
961
+ const toggleSelected = (id) => {
962
+ const next = new Set(selected);
963
+ if (next.has(id)) next.delete(id);
964
+ else next.add(id);
965
+ patch({ selected: [...next] });
966
+ };
967
+ const allVisibleSelected = facetedRows.length > 0 && facetedRows.every((row) => selected.has(row.id));
968
+ const toggleAll = () => {
969
+ patch({ selected: allVisibleSelected ? [] : facetedRows.map((row) => row.id) });
970
+ };
971
+ const setFilter = (facetKey, filter) => {
972
+ const next = { ...state.filters };
973
+ if (filter) next[facetKey] = filter;
974
+ else delete next[facetKey];
975
+ patch({ filters: next });
976
+ };
977
+ const renderedRows = virtualise ? virtualItems.map((item) => facetedRows[item.index]) : facetedRows;
978
+ const spaceAbove = virtualise ? virtualItems[0]?.start ?? 0 : 0;
979
+ const spaceBelow = virtualise ? virtualizer.getTotalSize() - (virtualItems[virtualItems.length - 1]?.end ?? 0) : 0;
980
+ const columnCount = visibleColumns.length + (selectable ? 1 : 0);
981
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-3", className), children: [
982
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
983
+ /* @__PURE__ */ jsx(
984
+ Input,
985
+ {
986
+ value: state.search,
987
+ onChange: (event) => patch({ search: event.target.value }),
988
+ placeholder: searchPlaceholder,
989
+ "aria-label": searchPlaceholder,
990
+ className: "h-9 max-w-xs"
991
+ }
992
+ ),
993
+ facets.map((facet) => /* @__PURE__ */ jsx(
994
+ FacetFilter,
995
+ {
996
+ facet,
997
+ filter: state.filters[facet.key],
998
+ onChange: (filter) => setFilter(facet.key, filter)
999
+ },
1000
+ facet.key
1001
+ )),
1002
+ /* @__PURE__ */ jsx(
1003
+ ColumnPicker,
1004
+ {
1005
+ columns,
1006
+ hidden,
1007
+ onToggle: (key) => patch({
1008
+ hidden: hidden.has(key) ? state.hidden.filter((k) => k !== key) : [...state.hidden, key]
1009
+ })
1010
+ }
1011
+ ),
1012
+ selectable && selected.size > 0 && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", role: "status", children: [
1013
+ selected.size,
1014
+ " s\xE9lectionn\xE9",
1015
+ selected.size > 1 ? "s" : ""
1016
+ ] })
1017
+ ] }),
1018
+ /* @__PURE__ */ jsx(
1019
+ "div",
1020
+ {
1021
+ ref: bodyRef,
1022
+ className: cn("overflow-auto rounded-xl border bg-card", virtualise && "max-h-160"),
1023
+ style: virtualise ? { maxHeight: VIRTUAL_VIEWPORT_PX } : void 0,
1024
+ children: /* @__PURE__ */ jsxs(Table, { children: [
1025
+ /* @__PURE__ */ jsx(TableHeader, { className: virtualise ? "sticky top-0 z-10 bg-card" : void 0, children: /* @__PURE__ */ jsxs(TableRow, { children: [
1026
+ selectable && /* @__PURE__ */ jsx(TableHead, { className: "w-10", children: /* @__PURE__ */ jsx(
1027
+ Checkbox,
1028
+ {
1029
+ checked: allVisibleSelected,
1030
+ onCheckedChange: toggleAll,
1031
+ "aria-label": "Tout s\xE9lectionner"
1032
+ }
1033
+ ) }),
1034
+ visibleColumns.map((column) => {
1035
+ const label = typeof column.header === "string" ? column.header : column.key;
1036
+ return /* @__PURE__ */ jsxs(
1037
+ TableHead,
1038
+ {
1039
+ style: { width: widths[column.key] ?? column.width },
1040
+ className: cn("relative", column.align === "right" && "text-right"),
1041
+ "aria-sort": !column.sortable ? void 0 : state.sort?.key === column.key ? state.sort.direction === "asc" ? "ascending" : "descending" : "none",
1042
+ children: [
1043
+ column.sortable ? /* @__PURE__ */ jsxs(
1044
+ "button",
1045
+ {
1046
+ type: "button",
1047
+ onClick: () => toggleSort(column.key),
1048
+ className: "inline-flex cursor-pointer items-center gap-1 hover:text-foreground",
1049
+ children: [
1050
+ column.header,
1051
+ /* @__PURE__ */ jsx(
1052
+ SortGlyph,
1053
+ {
1054
+ direction: state.sort?.key === column.key ? state.sort.direction : null
1055
+ }
1056
+ )
1057
+ ]
1058
+ }
1059
+ ) : column.header,
1060
+ /* @__PURE__ */ jsx(
1061
+ ResizeHandle,
1062
+ {
1063
+ label,
1064
+ onResize: (delta, from) => setWidths((current) => ({
1065
+ ...current,
1066
+ [column.key]: Math.max(MIN_COLUMN_WIDTH_PX, from + delta)
1067
+ }))
1068
+ }
1069
+ )
1070
+ ]
1071
+ },
1072
+ column.key
1073
+ );
1074
+ })
1075
+ ] }) }),
1076
+ /* @__PURE__ */ jsx(TableBody, { children: error ? /* @__PURE__ */ jsx(StatusRow, { colSpan: columnCount, tone: "error", children: error }) : loading ? /* @__PURE__ */ jsx(SkeletonRows, { columns: columnCount }) : facetedRows.length === 0 ? /* @__PURE__ */ jsx(StatusRow, { colSpan: columnCount, children: empty }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1077
+ spaceAbove > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: spaceAbove } }),
1078
+ renderedRows.map((row) => /* @__PURE__ */ jsxs(
1079
+ TableRow,
1080
+ {
1081
+ "data-state": selected.has(row.id) ? "selected" : void 0,
1082
+ className: onRowClick ? "cursor-pointer" : "hover:bg-transparent",
1083
+ onClick: onRowClick ? () => onRowClick(row.original) : void 0,
1084
+ children: [
1085
+ selectable && /* @__PURE__ */ jsx(TableCell, { onClick: (event) => event.stopPropagation(), children: /* @__PURE__ */ jsx(
1086
+ Checkbox,
1087
+ {
1088
+ checked: selected.has(row.id),
1089
+ onCheckedChange: () => toggleSelected(row.id),
1090
+ "aria-label": `S\xE9lectionner ${row.id}`
1091
+ }
1092
+ ) }),
1093
+ row.getVisibleCells().map((cell) => {
1094
+ const column = visibleColumns.find((c) => c.key === cell.column.id);
1095
+ return /* @__PURE__ */ jsx(
1096
+ TableCell,
1097
+ {
1098
+ className: column?.align === "right" ? "text-right" : void 0,
1099
+ children: flexRender(cell.column.columnDef.cell, cell.getContext())
1100
+ },
1101
+ cell.id
1102
+ );
1103
+ })
1104
+ ]
1105
+ },
1106
+ row.id
1107
+ )),
1108
+ spaceBelow > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: spaceBelow } })
1109
+ ] }) })
1110
+ ] })
1111
+ }
1112
+ ),
1113
+ pagination && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2", children: [
1114
+ /* @__PURE__ */ jsx(
1115
+ Button,
1116
+ {
1117
+ variant: "outline",
1118
+ size: "sm",
1119
+ disabled: !pagination.hasPrevious,
1120
+ onClick: pagination.onPrevious,
1121
+ children: "Pr\xE9c\xE9dent"
1122
+ }
1123
+ ),
1124
+ /* @__PURE__ */ jsx(
1125
+ Button,
1126
+ {
1127
+ variant: "outline",
1128
+ size: "sm",
1129
+ disabled: !pagination.hasNext,
1130
+ onClick: pagination.onNext,
1131
+ children: "Suivant"
1132
+ }
1133
+ )
1134
+ ] })
1135
+ ] });
1136
+ }
1137
+ function ResizeHandle({
1138
+ label,
1139
+ onResize
1140
+ }) {
1141
+ const start = (event) => {
1142
+ const header = event.currentTarget.parentElement;
1143
+ if (!header) return;
1144
+ const from = header.getBoundingClientRect().width;
1145
+ const originX = event.clientX;
1146
+ const move = (moveEvent) => onResize(moveEvent.clientX - originX, from);
1147
+ const stop = () => {
1148
+ window.removeEventListener("pointermove", move);
1149
+ window.removeEventListener("pointerup", stop);
1150
+ };
1151
+ window.addEventListener("pointermove", move);
1152
+ window.addEventListener("pointerup", stop);
1153
+ event.preventDefault();
1154
+ };
1155
+ return /* @__PURE__ */ jsx(
1156
+ "span",
1157
+ {
1158
+ "aria-hidden": true,
1159
+ "data-resize-handle": label,
1160
+ onPointerDown: start,
1161
+ className: "absolute inset-y-0 right-0 w-1 cursor-col-resize select-none hover:bg-border"
1162
+ }
1163
+ );
1164
+ }
1165
+ function SortGlyph({ direction }) {
1166
+ return /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "text-[10px] leading-none", children: direction === "asc" ? "\u25B2" : direction === "desc" ? "\u25BC" : "\u2195" });
1167
+ }
1168
+ function StatusRow({
1169
+ colSpan,
1170
+ tone,
1171
+ children
1172
+ }) {
1173
+ return /* @__PURE__ */ jsx(TableRow, { className: "hover:bg-transparent", children: /* @__PURE__ */ jsx(
1174
+ TableCell,
1175
+ {
1176
+ colSpan,
1177
+ className: cn(
1178
+ "text-sm",
1179
+ tone === "error" ? "text-status-blocked" : "text-muted-foreground"
1180
+ ),
1181
+ children
1182
+ }
1183
+ ) });
1184
+ }
1185
+ function SkeletonRows({ columns }) {
1186
+ return /* @__PURE__ */ jsx(Fragment, { children: Array.from({ length: 5 }, (_, row) => /* @__PURE__ */ jsx(TableRow, { className: "hover:bg-transparent", children: Array.from({ length: columns }, (_2, cell) => /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx("span", { className: "block h-3 w-full animate-pulse rounded bg-muted" }) }, cell)) }, row)) });
1187
+ }
1188
+ var VALUELESS = /* @__PURE__ */ new Set(["empty", "nempty"]);
1189
+ var SINGLE_VALUE = /* @__PURE__ */ new Set([
1190
+ "contains",
1191
+ "ncontains",
1192
+ "gt",
1193
+ "lt",
1194
+ "gte",
1195
+ "lte"
1196
+ ]);
1197
+ var OPERATOR_LABELS = {
1198
+ eq: "est",
1199
+ neq: "n'est pas",
1200
+ contains: "contient",
1201
+ ncontains: "ne contient pas",
1202
+ gt: "sup\xE9rieur \xE0",
1203
+ lt: "inf\xE9rieur \xE0",
1204
+ gte: "sup\xE9rieur ou \xE9gal \xE0",
1205
+ lte: "inf\xE9rieur ou \xE9gal \xE0",
1206
+ empty: "est vide",
1207
+ nempty: "n'est pas vide"
1208
+ };
1209
+ function isApplied(filter) {
1210
+ if (!filter) return false;
1211
+ return VALUELESS.has(filter.operator) || filter.values.length > 0;
1212
+ }
1213
+ function compare(value, bound) {
1214
+ if (value === "" || bound === "") return null;
1215
+ const boundNumber = Number(bound);
1216
+ if (!Number.isNaN(boundNumber)) {
1217
+ const valueNumber = Number(value);
1218
+ return Number.isNaN(valueNumber) ? null : valueNumber - boundNumber;
1219
+ }
1220
+ const boundDate = Date.parse(bound);
1221
+ if (!Number.isNaN(boundDate)) {
1222
+ const valueDate = Date.parse(value);
1223
+ return Number.isNaN(valueDate) ? null : valueDate - boundDate;
1224
+ }
1225
+ return value < bound ? -1 : value > bound ? 1 : 0;
1226
+ }
1227
+ function matches(raw, filter) {
1228
+ const value = raw == null ? "" : String(raw);
1229
+ const [bound = ""] = filter.values;
1230
+ const ordering = compare(value, bound);
1231
+ switch (filter.operator) {
1232
+ case "eq":
1233
+ return filter.values.includes(value);
1234
+ case "neq":
1235
+ return !filter.values.includes(value);
1236
+ case "contains":
1237
+ return value.toLowerCase().includes(bound.toLowerCase());
1238
+ case "ncontains":
1239
+ return !value.toLowerCase().includes(bound.toLowerCase());
1240
+ case "gt":
1241
+ return ordering !== null && ordering > 0;
1242
+ case "lt":
1243
+ return ordering !== null && ordering < 0;
1244
+ case "gte":
1245
+ return ordering !== null && ordering >= 0;
1246
+ case "lte":
1247
+ return ordering !== null && ordering <= 0;
1248
+ case "empty":
1249
+ return value === "";
1250
+ case "nempty":
1251
+ return value !== "";
1252
+ }
1253
+ }
1254
+ function FacetFilter({
1255
+ facet,
1256
+ filter,
1257
+ onChange
1258
+ }) {
1259
+ const [open, setOpen] = React2.useState(false);
1260
+ const operators = facet.operators ?? ["eq"];
1261
+ const operator = filter?.operator ?? operators[0] ?? "eq";
1262
+ const values = filter?.values ?? [];
1263
+ const summary = !isApplied(filter) ? facet.label : VALUELESS.has(operator) ? `${facet.label} \xB7 ${OPERATOR_LABELS[operator]}` : `${facet.label} \xB7 ${values.length}`;
1264
+ const setOperator = (next) => {
1265
+ const sameKind = SINGLE_VALUE.has(next) === SINGLE_VALUE.has(operator);
1266
+ onChange({
1267
+ operator: next,
1268
+ values: VALUELESS.has(next) || !sameKind ? [] : SINGLE_VALUE.has(next) ? values.slice(0, 1) : values
1269
+ });
1270
+ };
1271
+ return /* @__PURE__ */ jsx(
1272
+ Popover,
1273
+ {
1274
+ open,
1275
+ onOpenChange: setOpen,
1276
+ trigger: (
1277
+ // Named for what it does: a chip reading just "Nom" tells a screen-reader
1278
+ // user nothing, and collides with the sortable column header of that name.
1279
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", "aria-label": `Filtrer par ${summary}`, children: summary })
1280
+ ),
1281
+ children: /* @__PURE__ */ jsxs("div", { className: "flex w-56 flex-col gap-2", children: [
1282
+ operators.length > 1 && /* @__PURE__ */ jsx(
1283
+ Select,
1284
+ {
1285
+ value: operator,
1286
+ "aria-label": `Op\xE9rateur \u2014 ${facet.label}`,
1287
+ onValueChange: (next) => setOperator(next),
1288
+ items: operators.map((candidate) => ({
1289
+ value: candidate,
1290
+ label: OPERATOR_LABELS[candidate]
1291
+ }))
1292
+ }
1293
+ ),
1294
+ VALUELESS.has(operator) ? null : SINGLE_VALUE.has(operator) || !facet.options ? /* @__PURE__ */ jsx(
1295
+ Input,
1296
+ {
1297
+ value: values[0] ?? "",
1298
+ "aria-label": `Valeur \u2014 ${facet.label}`,
1299
+ onChange: (event) => onChange({ operator, values: event.target.value ? [event.target.value] : [] })
1300
+ }
1301
+ ) : facet.options.map((option) => /* @__PURE__ */ jsx(
1302
+ Checkbox,
1303
+ {
1304
+ checked: values.includes(option.value),
1305
+ onCheckedChange: () => onChange({
1306
+ operator,
1307
+ values: values.includes(option.value) ? values.filter((v) => v !== option.value) : [...values, option.value]
1308
+ }),
1309
+ label: option.label ?? option.value
1310
+ },
1311
+ option.value
1312
+ )),
1313
+ isApplied(filter) && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => onChange(null), children: "Effacer" })
1314
+ ] })
1315
+ }
1316
+ );
1317
+ }
1318
+ function ColumnPicker({
1319
+ columns,
1320
+ hidden,
1321
+ onToggle
1322
+ }) {
1323
+ const [open, setOpen] = React2.useState(false);
1324
+ return /* @__PURE__ */ jsx(
1325
+ Popover,
1326
+ {
1327
+ open,
1328
+ onOpenChange: setOpen,
1329
+ align: "end",
1330
+ trigger: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", children: "Colonnes" }),
1331
+ children: /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1", children: columns.map((column) => /* @__PURE__ */ jsx(
1332
+ Checkbox,
1333
+ {
1334
+ checked: !hidden.has(column.key),
1335
+ onCheckedChange: () => onToggle(column.key),
1336
+ label: typeof column.header === "string" ? column.header : column.key
1337
+ },
1338
+ column.key
1339
+ )) })
1340
+ }
1341
+ );
1342
+ }
1343
+ var PERIOD_PRESETS = [
1344
+ { key: "today", label: "Aujourd'hui" },
1345
+ { key: "yesterday", label: "Hier" },
1346
+ { key: "last7", label: "7 derniers jours" },
1347
+ { key: "last30", label: "30 derniers jours" },
1348
+ { key: "last90", label: "90 derniers jours" },
1349
+ { key: "thisMonth", label: "Mois en cours" },
1350
+ { key: "lastMonth", label: "Mois dernier" },
1351
+ { key: "thisQuarter", label: "Trimestre en cours" },
1352
+ { key: "last12Months", label: "12 derniers mois" },
1353
+ { key: "thisYear", label: "Ann\xE9e en cours" },
1354
+ { key: "all", label: "Tout l'historique" }
1355
+ ];
1356
+ var COMPARE_LABELS = {
1357
+ none: "Aucune",
1358
+ previous_period: "P\xE9riode pr\xE9c\xE9dente",
1359
+ previous_year: "Ann\xE9e pr\xE9c\xE9dente",
1360
+ custom: "Personnalis\xE9e"
1361
+ };
1362
+ var DAY_FORMAT = "yyyy-MM-dd";
1363
+ var toDay = (date) => format(date, DAY_FORMAT);
1364
+ var fromDay = (day) => parse(day, DAY_FORMAT, /* @__PURE__ */ new Date());
1365
+ var CALENDAR_UNIT = {
1366
+ thisMonth: "month",
1367
+ lastMonth: "month",
1368
+ thisQuarter: "quarter",
1369
+ thisYear: "year"
1370
+ };
1371
+ function presetRange(preset, today) {
1372
+ const day = toDay(today);
1373
+ switch (preset) {
1374
+ case "today":
1375
+ return { from: day, to: day };
1376
+ case "yesterday": {
1377
+ const yesterday = toDay(subDays(today, 1));
1378
+ return { from: yesterday, to: yesterday };
1379
+ }
1380
+ case "last7":
1381
+ return { from: toDay(subDays(today, 6)), to: day };
1382
+ case "last30":
1383
+ return { from: toDay(subDays(today, 29)), to: day };
1384
+ case "last90":
1385
+ return { from: toDay(subDays(today, 89)), to: day };
1386
+ case "thisMonth":
1387
+ return { from: toDay(startOfMonth(today)), to: toDay(endOfMonth(today)) };
1388
+ case "lastMonth": {
1389
+ const previous = subMonths(today, 1);
1390
+ return { from: toDay(startOfMonth(previous)), to: toDay(endOfMonth(previous)) };
1391
+ }
1392
+ case "thisQuarter":
1393
+ return { from: toDay(startOfQuarter(today)), to: toDay(endOfQuarter(today)) };
1394
+ case "last12Months":
1395
+ return { from: toDay(addDays(subYears(today, 1), 1)), to: day };
1396
+ case "thisYear":
1397
+ return { from: toDay(startOfYear(today)), to: toDay(endOfYear(today)) };
1398
+ case "all":
1399
+ return { to: day };
1400
+ case "custom":
1401
+ return { from: day, to: day };
1402
+ }
1403
+ }
1404
+ function comparisonRange(preset, range, compare2) {
1405
+ if (compare2 === "none" || compare2 === "custom" || range.from === void 0) return void 0;
1406
+ const from = fromDay(range.from);
1407
+ const to = fromDay(range.to);
1408
+ if (compare2 === "previous_year") {
1409
+ return { from: toDay(subYears(from, 1)), to: toDay(subYears(to, 1)) };
1410
+ }
1411
+ const unit = CALENDAR_UNIT[preset];
1412
+ if (unit === "month") {
1413
+ const previous = subMonths(from, 1);
1414
+ return { from: toDay(startOfMonth(previous)), to: toDay(endOfMonth(previous)) };
1415
+ }
1416
+ if (unit === "quarter") {
1417
+ const previous = addQuarters(from, -1);
1418
+ return { from: toDay(startOfQuarter(previous)), to: toDay(endOfQuarter(previous)) };
1419
+ }
1420
+ if (unit === "year") {
1421
+ const previous = subYears(from, 1);
1422
+ return { from: toDay(startOfYear(previous)), to: toDay(endOfYear(previous)) };
1423
+ }
1424
+ const length = differenceInCalendarDays(to, from) + 1;
1425
+ return { from: toDay(subDays(from, length)), to: toDay(subDays(to, length)) };
1426
+ }
1427
+ function makePeriod(preset, range, compare2, customCompareRange) {
1428
+ const resolved = compare2 === "custom" ? customCompareRange : comparisonRange(preset, range, compare2);
1429
+ const effective = resolved === void 0 ? "none" : compare2;
1430
+ return {
1431
+ preset,
1432
+ range,
1433
+ compare: effective,
1434
+ ...resolved ? { compareRange: resolved } : {}
1435
+ };
1436
+ }
1437
+ function periodFromPreset(preset, today, compare2 = "none") {
1438
+ return makePeriod(preset, presetRange(preset, today), compare2);
1439
+ }
1440
+ var MONTHS_ABBR = [
1441
+ "janv.",
1442
+ "f\xE9vr.",
1443
+ "mars",
1444
+ "avr.",
1445
+ "mai",
1446
+ "juin",
1447
+ "juil.",
1448
+ "ao\xFBt",
1449
+ "sept.",
1450
+ "oct.",
1451
+ "nov.",
1452
+ "d\xE9c."
1453
+ ];
1454
+ var formatDay = (day) => {
1455
+ const date = fromDay(day);
1456
+ return `${date.getDate()} ${MONTHS_ABBR[date.getMonth()]} ${date.getFullYear()}`;
1457
+ };
1458
+ function formatRange(range) {
1459
+ if (range.from === void 0) return `Jusqu'au ${formatDay(range.to)}`;
1460
+ if (range.from === range.to) return formatDay(range.from);
1461
+ const from = fromDay(range.from);
1462
+ const to = fromDay(range.to);
1463
+ if (from.getFullYear() === to.getFullYear() && from.getMonth() === to.getMonth()) {
1464
+ return `${from.getDate()} \u2013 ${to.getDate()} ${MONTHS_ABBR[to.getMonth()]} ${to.getFullYear()}`;
1465
+ }
1466
+ return `${formatDay(range.from)} \u2013 ${formatDay(range.to)}`;
1467
+ }
1468
+ function formatPeriod(value) {
1469
+ const preset = PERIOD_PRESETS.find((entry) => entry.key === value.preset);
1470
+ if (preset) return preset.label;
1471
+ return isCompleteRange(value.range) ? formatRange(value.range) : "P\xE9riode";
1472
+ }
1473
+ var DAY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
1474
+ var isDay = (value) => value !== void 0 && DAY_PATTERN.test(value);
1475
+ function isCompleteRange(range) {
1476
+ return isDay(range.to) && (range.from === void 0 || isDay(range.from));
1477
+ }
1478
+ function spansTwoMonths(range) {
1479
+ if (!isDay(range.from) || !isDay(range.to)) return false;
1480
+ const from = fromDay(range.from);
1481
+ const to = fromDay(range.to);
1482
+ return from.getFullYear() !== to.getFullYear() || from.getMonth() !== to.getMonth();
1483
+ }
1484
+ function orderedRange(from, to) {
1485
+ return from <= to ? { from, to } : { from: to, to: from };
1486
+ }
1487
+ var COMPARE_MODES = [
1488
+ "none",
1489
+ "previous_period",
1490
+ "previous_year",
1491
+ "custom"
1492
+ ];
1493
+ var DAY_PICKER_CLASS_NAMES = {
1494
+ // DayPicker renders its Nav as a sibling of the months, before them, so the
1495
+ // arrows are positioned against this box rather than against a caption.
1496
+ months: "relative flex gap-4",
1497
+ month: "space-y-2",
1498
+ month_caption: "flex h-8 items-center justify-center",
1499
+ caption_label: "text-sm font-medium capitalize",
1500
+ nav: "flex items-center justify-between absolute inset-x-0 h-8 px-1 pointer-events-none",
1501
+ button_previous: "pointer-events-auto inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40",
1502
+ button_next: "pointer-events-auto inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40",
1503
+ chevron: "h-4 w-4 fill-current",
1504
+ month_grid: "w-full border-collapse",
1505
+ weekdays: "flex",
1506
+ weekday: "w-9 text-xs font-normal text-muted-foreground",
1507
+ week: "flex w-full",
1508
+ day: "h-9 w-9 p-0 text-center text-sm",
1509
+ day_button: "h-9 w-9 rounded-md transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1510
+ today: "font-semibold text-primary",
1511
+ outside: "text-muted-foreground/50",
1512
+ disabled: "opacity-40",
1513
+ selected: "bg-primary/15",
1514
+ range_start: "rounded-l-md bg-primary [&_button]:bg-primary [&_button]:text-primary-foreground",
1515
+ range_end: "rounded-r-md bg-primary [&_button]:bg-primary [&_button]:text-primary-foreground",
1516
+ range_middle: "bg-primary/15 [&_button]:hover:bg-primary/25"
1517
+ };
1518
+ var COMPARE_MODIFIER_CLASS = "bg-chart-2/25 ring-1 ring-inset ring-chart-2/40";
1519
+ function draftOf(value) {
1520
+ return {
1521
+ preset: value.preset,
1522
+ // Anything that is not a day is "not chosen yet". The picker this replaced
1523
+ // accepted `{ from: '', to: '' }` for exactly that, and its callers still do.
1524
+ from: isDay(value.range.from) ? value.range.from : void 0,
1525
+ to: isDay(value.range.to) ? value.range.to : void 0,
1526
+ compare: value.compare,
1527
+ compareFrom: value.compareRange?.from,
1528
+ compareTo: value.compareRange?.to
1529
+ };
1530
+ }
1531
+ function rangeOf(draft) {
1532
+ if (!isDay(draft.to)) return null;
1533
+ if (draft.preset === "all") return { to: draft.to };
1534
+ if (!isDay(draft.from)) return null;
1535
+ return { from: draft.from, to: draft.to };
1536
+ }
1537
+ function customCompareOf(draft) {
1538
+ if (!isDay(draft.compareFrom) || !isDay(draft.compareTo)) return void 0;
1539
+ return orderedRange(draft.compareFrom, draft.compareTo);
1540
+ }
1541
+ function previewCompare(draft) {
1542
+ const range = rangeOf(draft);
1543
+ if (!range) return void 0;
1544
+ if (draft.compare === "custom") return customCompareOf(draft);
1545
+ return comparisonRange(draft.preset, range, draft.compare);
1546
+ }
1547
+ function activeCompare(draft) {
1548
+ return draft.preset === "all" ? "none" : draft.compare;
1549
+ }
1550
+ function anchorMonth(range) {
1551
+ if (!range) return void 0;
1552
+ if (isDay(range.from)) return fromDay(range.from);
1553
+ return isDay(range.to) ? fromDay(range.to) : void 0;
1554
+ }
1555
+ function DateRangeCompare({
1556
+ value,
1557
+ onChange,
1558
+ comparable = true,
1559
+ presets = PERIOD_PRESETS,
1560
+ align = "start",
1561
+ disabled,
1562
+ className
1563
+ }) {
1564
+ const [open, setOpen] = React2.useState(false);
1565
+ const [draft, setDraft] = React2.useState(() => draftOf(value));
1566
+ const [month, setMonth] = React2.useState(void 0);
1567
+ const [months, setMonths] = React2.useState(1);
1568
+ const showRange = (range2) => {
1569
+ setMonth(anchorMonth(range2));
1570
+ setMonths(spansTwoMonths(range2) ? 2 : 1);
1571
+ };
1572
+ const openPanel = () => {
1573
+ setDraft(draftOf(value));
1574
+ showRange(value.range);
1575
+ setOpen(true);
1576
+ };
1577
+ const pickPreset = (preset) => {
1578
+ const resolved = periodFromPreset(preset, /* @__PURE__ */ new Date(), activeCompare(draft));
1579
+ setDraft({
1580
+ ...draftOf(resolved),
1581
+ // The rail changes the period, not the comparison: the mode the user chose
1582
+ // survives, and `activeCompare` decides whether it can apply here.
1583
+ compare: draft.compare,
1584
+ compareFrom: draft.compareFrom,
1585
+ compareTo: draft.compareTo
1586
+ });
1587
+ showRange(resolved.range);
1588
+ };
1589
+ const pickDays = (selected) => {
1590
+ setDraft({
1591
+ ...draft,
1592
+ preset: "custom",
1593
+ from: selected?.from ? toDay(selected.from) : void 0,
1594
+ to: selected?.to ? toDay(selected.to) : void 0
1595
+ });
1596
+ };
1597
+ const range = rangeOf(draft);
1598
+ const compareRange = previewCompare(draft);
1599
+ const apply = () => {
1600
+ if (!range) return;
1601
+ onChange(makePeriod(draft.preset, range, activeCompare(draft), customCompareOf(draft)));
1602
+ setOpen(false);
1603
+ };
1604
+ const selectedDays = isDay(draft.from) ? { from: fromDay(draft.from), to: isDay(draft.to) ? fromDay(draft.to) : void 0 } : void 0;
1605
+ return /* @__PURE__ */ jsx(
1606
+ Popover,
1607
+ {
1608
+ open,
1609
+ onOpenChange: (next) => next ? openPanel() : setOpen(false),
1610
+ align,
1611
+ className: cn("w-auto p-0", className),
1612
+ trigger: /* @__PURE__ */ jsx(Button, { variant: "outline", disabled, children: formatPeriod(value) }),
1613
+ children: /* @__PURE__ */ jsxs("div", { className: "flex", children: [
1614
+ presets.length > 0 && /* @__PURE__ */ jsx(PresetRail, { presets, selected: draft.preset, onSelect: pickPreset }),
1615
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
1616
+ /* @__PURE__ */ jsx("div", { className: "relative p-3", children: /* @__PURE__ */ jsx(
1617
+ DayPicker,
1618
+ {
1619
+ mode: "range",
1620
+ locale: fr,
1621
+ weekStartsOn: 1,
1622
+ numberOfMonths: months,
1623
+ month,
1624
+ onMonthChange: setMonth,
1625
+ selected: selectedDays,
1626
+ onSelect: pickDays,
1627
+ modifiers: {
1628
+ compare: compareRange?.from ? [{ from: fromDay(compareRange.from), to: fromDay(compareRange.to) }] : []
1629
+ },
1630
+ modifiersClassNames: { compare: COMPARE_MODIFIER_CLASS },
1631
+ classNames: DAY_PICKER_CLASS_NAMES
1632
+ }
1633
+ ) }),
1634
+ comparable && /* @__PURE__ */ jsx(
1635
+ CompareSection,
1636
+ {
1637
+ draft,
1638
+ compareRange,
1639
+ onModeChange: (compare2) => setDraft({ ...draft, compare: compare2 }),
1640
+ onCustomChange: (from, to) => setDraft({ ...draft, compareFrom: from, compareTo: to })
1641
+ }
1642
+ ),
1643
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-4 border-t px-3 py-2", children: [
1644
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
1645
+ range ? formatRange(range) : "Choisissez une date de fin",
1646
+ compareRange && ` vs ${formatRange(compareRange)}`
1647
+ ] }),
1648
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1649
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => setOpen(false), children: "Annuler" }),
1650
+ /* @__PURE__ */ jsx(Button, { size: "sm", onClick: apply, disabled: !range, children: "Appliquer" })
1651
+ ] })
1652
+ ] })
1653
+ ] })
1654
+ ] })
1655
+ }
1656
+ );
1657
+ }
1658
+ function DateRangePicker({ value, onChange, className, disabled }) {
1659
+ return /* @__PURE__ */ jsx(
1660
+ DateRangeCompare,
1661
+ {
1662
+ value: { preset: "custom", range: value, compare: "none" },
1663
+ onChange: (next) => {
1664
+ if (next.range.from !== void 0) onChange({ from: next.range.from, to: next.range.to });
1665
+ },
1666
+ comparable: false,
1667
+ presets: [],
1668
+ disabled,
1669
+ className
1670
+ }
1671
+ );
1672
+ }
1673
+ function PresetRail({
1674
+ presets,
1675
+ selected,
1676
+ onSelect
1677
+ }) {
1678
+ const refs = React2.useRef([]);
1679
+ const activeIndex = Math.max(
1680
+ 0,
1681
+ presets.findIndex((preset) => preset.key === selected)
1682
+ );
1683
+ const moveTo = (index) => {
1684
+ const next = (index + presets.length) % presets.length;
1685
+ refs.current[next]?.focus();
1686
+ onSelect(presets[next].key);
1687
+ };
1688
+ const onKeyDown = (event, index) => {
1689
+ const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
1690
+ if (step !== void 0) {
1691
+ event.preventDefault();
1692
+ moveTo(index + step);
1693
+ return;
1694
+ }
1695
+ if (event.key === "Home" || event.key === "End") {
1696
+ event.preventDefault();
1697
+ moveTo(event.key === "Home" ? 0 : presets.length - 1);
1698
+ }
1699
+ };
1700
+ return /* @__PURE__ */ jsx(
1701
+ "div",
1702
+ {
1703
+ role: "radiogroup",
1704
+ "aria-label": "P\xE9riode pr\xE9d\xE9finie",
1705
+ className: "flex w-44 shrink-0 flex-col gap-0.5 border-r p-2",
1706
+ children: presets.map((preset, index) => /* @__PURE__ */ jsx(
1707
+ "button",
1708
+ {
1709
+ ref: (node) => {
1710
+ refs.current[index] = node;
1711
+ },
1712
+ type: "button",
1713
+ role: "radio",
1714
+ "aria-checked": preset.key === selected,
1715
+ tabIndex: index === activeIndex ? 0 : -1,
1716
+ onKeyDown: (event) => onKeyDown(event, index),
1717
+ onClick: () => onSelect(preset.key),
1718
+ className: cn(
1719
+ "rounded px-3 py-1.5 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1720
+ preset.key === selected ? "bg-muted font-medium text-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground"
1721
+ ),
1722
+ children: preset.label
1723
+ },
1724
+ preset.key
1725
+ ))
1726
+ }
1727
+ );
1728
+ }
1729
+ function CompareSection({
1730
+ draft,
1731
+ compareRange,
1732
+ onModeChange,
1733
+ onCustomChange
1734
+ }) {
1735
+ const derivable = draft.preset !== "all";
1736
+ const active = activeCompare(draft);
1737
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2 border-t px-3 py-2", children: [
1738
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: "Comparer \xE0" }),
1739
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: COMPARE_MODES.map((mode) => /* @__PURE__ */ jsx(
1740
+ "button",
1741
+ {
1742
+ type: "button",
1743
+ "aria-pressed": active === mode,
1744
+ disabled: mode !== "none" && !derivable,
1745
+ onClick: () => onModeChange(mode),
1746
+ className: cn(
1747
+ "rounded-full border px-2.5 py-1 text-xs transition-colors disabled:opacity-40",
1748
+ active === mode ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"
1749
+ ),
1750
+ children: COMPARE_LABELS[mode]
1751
+ },
1752
+ mode
1753
+ )) }),
1754
+ active === "custom" && // Two inputs rather than a second selection mode in the calendar: with
1755
+ // one grid and two ranges to fill, nothing on screen would say which one
1756
+ // a click is about to change.
1757
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1758
+ /* @__PURE__ */ jsx(
1759
+ "input",
1760
+ {
1761
+ type: "date",
1762
+ "aria-label": "D\xE9but de la comparaison",
1763
+ value: draft.compareFrom ?? "",
1764
+ max: draft.compareTo,
1765
+ onChange: (event) => onCustomChange(event.target.value || void 0, draft.compareTo),
1766
+ className: "h-8 rounded-md border border-input bg-transparent px-2 text-sm"
1767
+ }
1768
+ ),
1769
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "\u2192" }),
1770
+ /* @__PURE__ */ jsx(
1771
+ "input",
1772
+ {
1773
+ type: "date",
1774
+ "aria-label": "Fin de la comparaison",
1775
+ value: draft.compareTo ?? "",
1776
+ min: draft.compareFrom,
1777
+ onChange: (event) => onCustomChange(draft.compareFrom, event.target.value || void 0),
1778
+ className: "h-8 rounded-md border border-input bg-transparent px-2 text-sm"
1779
+ }
1780
+ )
1781
+ ] }),
1782
+ compareRange && active !== "custom" && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: formatRange(compareRange) })
1783
+ ] });
1784
+ }
1785
+ var CHART_COLORS = [
1786
+ "var(--chart-1)",
1787
+ "var(--chart-2)",
1788
+ "var(--chart-3)",
1789
+ "var(--chart-4)",
1790
+ "var(--chart-5)"
1791
+ ];
1792
+ var COMPARE_COLOR = CHART_COLORS[1];
1793
+ var colorAt = (index) => CHART_COLORS[index % CHART_COLORS.length];
1794
+ var formatNumber = (value) => new Intl.NumberFormat("fr-FR").format(Math.round(value));
1795
+ var ChartContext = React2.createContext({ formatValue: formatNumber });
1796
+ function ChartContainer({
1797
+ children,
1798
+ height = 320,
1799
+ formatValue = formatNumber,
1800
+ label,
1801
+ className
1802
+ }) {
1803
+ const context = React2.useMemo(() => ({ formatValue }), [formatValue]);
1804
+ return /* @__PURE__ */ jsx(ChartContext.Provider, { value: context, children: /* @__PURE__ */ jsx(
1805
+ "div",
1806
+ {
1807
+ role: "img",
1808
+ "aria-label": label,
1809
+ style: { height },
1810
+ className: cn(
1811
+ "w-full text-xs",
1812
+ "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground",
1813
+ "[&_.recharts-cartesian-grid_line]:stroke-border",
1814
+ "[&_.recharts-curve.recharts-tooltip-cursor]:stroke-border",
1815
+ "[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted/40",
1816
+ "[&_.recharts-sector]:outline-none",
1817
+ "[&_.recharts-surface]:outline-none",
1818
+ className
1819
+ ),
1820
+ children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children })
1821
+ }
1822
+ ) });
1823
+ }
1824
+ function ChartTooltipContent({
1825
+ active,
1826
+ payload,
1827
+ label
1828
+ }) {
1829
+ const { formatValue } = React2.useContext(ChartContext);
1830
+ if (!active || !payload?.length) return null;
1831
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-md border bg-card px-2.5 py-1.5 text-xs shadow-md", children: [
1832
+ label !== void 0 && /* @__PURE__ */ jsx("p", { className: "mb-1 font-medium text-foreground", children: label }),
1833
+ /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-0.5", children: payload.map((entry, index) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-2", children: [
1834
+ /* @__PURE__ */ jsx(
1835
+ "span",
1836
+ {
1837
+ "aria-hidden": true,
1838
+ className: "h-2 w-2 shrink-0 rounded-[2px]",
1839
+ style: { backgroundColor: entry.color }
1840
+ }
1841
+ ),
1842
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: entry.name }),
1843
+ /* @__PURE__ */ jsx("span", { className: "ml-auto font-medium tabular-nums text-foreground", children: typeof entry.value === "number" ? formatValue(entry.value) : entry.value })
1844
+ ] }, `${entry.dataKey}-${index}`)) })
1845
+ ] });
1846
+ }
1847
+ var ChartTooltip = /* @__PURE__ */ jsx(
1848
+ Tooltip,
1849
+ {
1850
+ cursor: { strokeWidth: 1 },
1851
+ content: /* @__PURE__ */ jsx(ChartTooltipContent, {}),
1852
+ wrapperStyle: { outline: "none" }
1853
+ }
1854
+ );
1855
+ function ChartLegendContent({ payload }) {
1856
+ if (!payload?.length) return null;
1857
+ return /* @__PURE__ */ jsx("ul", { className: "flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pt-3", children: payload.map((entry, index) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-1.5", children: [
1858
+ /* @__PURE__ */ jsx(
1859
+ "span",
1860
+ {
1861
+ "aria-hidden": true,
1862
+ className: "h-2 w-2 shrink-0 rounded-[2px]",
1863
+ style: { backgroundColor: entry.color }
1864
+ }
1865
+ ),
1866
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: entry.value })
1867
+ ] }, `${entry.dataKey}-${index}`)) });
1868
+ }
1869
+ var ChartLegend = /* @__PURE__ */ jsx(Legend, { content: /* @__PURE__ */ jsx(ChartLegendContent, {}) });
1870
+ var COMPARE_LINE_PROPS = {
1871
+ stroke: COMPARE_COLOR,
1872
+ strokeDasharray: "4 4",
1873
+ strokeOpacity: 0.4,
1874
+ strokeWidth: 2,
1875
+ dot: false,
1876
+ activeDot: false,
1877
+ // Not a preference: Recharts animates `strokeDasharray` from `0px 0px`, so an
1878
+ // animated comparison line is drawn solid and only becomes dashed once the
1879
+ // animation ends. Off, it is dashed from the first frame — and a reference
1880
+ // line that draws itself in is pulling the eye towards the thing meant to
1881
+ // recede behind the real series anyway.
1882
+ isAnimationActive: false
1883
+ };
1884
+ var AXIS_PROPS = {
1885
+ tickLine: false,
1886
+ axisLine: false,
1887
+ tickMargin: 8
1888
+ };
1889
+ var MARK_PROPS = { isAnimationActive: false };
1890
+ function CartesianFrame({ x, formatValue }) {
1891
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1892
+ /* @__PURE__ */ jsx(CartesianGrid, { vertical: false, strokeDasharray: "3 3" }),
1893
+ /* @__PURE__ */ jsx(XAxis, { dataKey: x, ...AXIS_PROPS }),
1894
+ /* @__PURE__ */ jsx(YAxis, { ...AXIS_PROPS, width: 56, tickFormatter: formatValue })
1895
+ ] });
1896
+ }
1897
+ function accessibleLabel(series, label) {
1898
+ return label ?? series.map((entry) => entry.label).join(", ");
1899
+ }
1900
+ function showsLegend(props) {
1901
+ if (props.hideLegend) return false;
1902
+ return props.series.length > 1 || props.compareSeries !== void 0;
1903
+ }
1904
+ function BarChart(props) {
1905
+ const { data, x, series, compareSeries, formatValue = formatNumber } = props;
1906
+ return /* @__PURE__ */ jsx(
1907
+ ChartContainer,
1908
+ {
1909
+ height: props.height,
1910
+ formatValue,
1911
+ label: accessibleLabel(series, props.label),
1912
+ className: props.className,
1913
+ children: /* @__PURE__ */ jsxs(BarChart$1, { data, children: [
1914
+ /* @__PURE__ */ jsx(CartesianFrame, { x, formatValue }),
1915
+ ChartTooltip,
1916
+ showsLegend(props) && ChartLegend,
1917
+ series.map((entry, index) => /* @__PURE__ */ jsx(
1918
+ Bar,
1919
+ {
1920
+ dataKey: entry.key,
1921
+ name: entry.label,
1922
+ fill: colorAt(entry.colorIndex ?? index),
1923
+ radius: [4, 4, 0, 0],
1924
+ ...MARK_PROPS
1925
+ },
1926
+ entry.key
1927
+ )),
1928
+ compareSeries && /* @__PURE__ */ jsx(
1929
+ Bar,
1930
+ {
1931
+ dataKey: compareSeries.key,
1932
+ name: compareSeries.label,
1933
+ fill: COMPARE_LINE_PROPS.stroke,
1934
+ fillOpacity: COMPARE_LINE_PROPS.strokeOpacity,
1935
+ radius: [4, 4, 0, 0],
1936
+ ...MARK_PROPS
1937
+ }
1938
+ )
1939
+ ] })
1940
+ }
1941
+ );
1942
+ }
1943
+ function StackedBarChart(props) {
1944
+ const { data, x, series, formatValue = formatNumber } = props;
1945
+ return /* @__PURE__ */ jsx(
1946
+ ChartContainer,
1947
+ {
1948
+ height: props.height,
1949
+ formatValue,
1950
+ label: accessibleLabel(series, props.label),
1951
+ className: props.className,
1952
+ children: /* @__PURE__ */ jsxs(BarChart$1, { data, children: [
1953
+ /* @__PURE__ */ jsx(CartesianFrame, { x, formatValue }),
1954
+ ChartTooltip,
1955
+ showsLegend(props) && ChartLegend,
1956
+ series.map((entry, index) => /* @__PURE__ */ jsx(
1957
+ Bar,
1958
+ {
1959
+ dataKey: entry.key,
1960
+ name: entry.label,
1961
+ stackId: "stack",
1962
+ fill: colorAt(entry.colorIndex ?? index),
1963
+ radius: 0,
1964
+ ...MARK_PROPS
1965
+ },
1966
+ entry.key
1967
+ ))
1968
+ ] })
1969
+ }
1970
+ );
1971
+ }
1972
+ function LineChart(props) {
1973
+ const { data, x, series, compareSeries, formatValue = formatNumber } = props;
1974
+ return /* @__PURE__ */ jsx(
1975
+ ChartContainer,
1976
+ {
1977
+ height: props.height,
1978
+ formatValue,
1979
+ label: accessibleLabel(series, props.label),
1980
+ className: props.className,
1981
+ children: /* @__PURE__ */ jsxs(LineChart$1, { data, children: [
1982
+ /* @__PURE__ */ jsx(CartesianFrame, { x, formatValue }),
1983
+ ChartTooltip,
1984
+ showsLegend(props) && ChartLegend,
1985
+ series.map((entry, index) => /* @__PURE__ */ jsx(
1986
+ Line,
1987
+ {
1988
+ type: "monotone",
1989
+ dataKey: entry.key,
1990
+ name: entry.label,
1991
+ stroke: colorAt(entry.colorIndex ?? index),
1992
+ strokeWidth: 2,
1993
+ dot: false,
1994
+ ...MARK_PROPS
1995
+ },
1996
+ entry.key
1997
+ )),
1998
+ compareSeries && /* @__PURE__ */ jsx(
1999
+ Line,
2000
+ {
2001
+ type: "monotone",
2002
+ dataKey: compareSeries.key,
2003
+ name: compareSeries.label,
2004
+ ...COMPARE_LINE_PROPS
2005
+ }
2006
+ )
2007
+ ] })
2008
+ }
2009
+ );
2010
+ }
2011
+ function AreaChart(props) {
2012
+ const { data, x, series, compareSeries, formatValue = formatNumber } = props;
2013
+ return /* @__PURE__ */ jsx(
2014
+ ChartContainer,
2015
+ {
2016
+ height: props.height,
2017
+ formatValue,
2018
+ label: accessibleLabel(series, props.label),
2019
+ className: props.className,
2020
+ children: /* @__PURE__ */ jsxs(AreaChart$1, { data, children: [
2021
+ /* @__PURE__ */ jsx(CartesianFrame, { x, formatValue }),
2022
+ ChartTooltip,
2023
+ showsLegend(props) && ChartLegend,
2024
+ series.map((entry, index) => {
2025
+ const color = colorAt(entry.colorIndex ?? index);
2026
+ return /* @__PURE__ */ jsx(
2027
+ Area,
2028
+ {
2029
+ type: "monotone",
2030
+ dataKey: entry.key,
2031
+ name: entry.label,
2032
+ stroke: color,
2033
+ strokeWidth: 2,
2034
+ fill: color,
2035
+ fillOpacity: 0.15,
2036
+ ...MARK_PROPS
2037
+ },
2038
+ entry.key
2039
+ );
2040
+ }),
2041
+ compareSeries && /* @__PURE__ */ jsx(
2042
+ Area,
2043
+ {
2044
+ type: "monotone",
2045
+ dataKey: compareSeries.key,
2046
+ name: compareSeries.label,
2047
+ fill: "none",
2048
+ ...COMPARE_LINE_PROPS
2049
+ }
2050
+ )
2051
+ ] })
2052
+ }
2053
+ );
2054
+ }
2055
+ function FunnelChart({
2056
+ data,
2057
+ formatValue = formatNumber,
2058
+ height,
2059
+ label,
2060
+ className
2061
+ }) {
2062
+ return /* @__PURE__ */ jsx(
2063
+ ChartContainer,
2064
+ {
2065
+ height,
2066
+ formatValue,
2067
+ label: label ?? data.map((step) => step.label).join(", "),
2068
+ className,
2069
+ children: /* @__PURE__ */ jsxs(FunnelChart$1, { children: [
2070
+ ChartTooltip,
2071
+ /* @__PURE__ */ jsxs(Funnel, { dataKey: "value", nameKey: "label", data, ...MARK_PROPS, children: [
2072
+ data.map((step, index) => /* @__PURE__ */ jsx(Cell, { fill: colorAt(index) }, step.label)),
2073
+ /* @__PURE__ */ jsx(
2074
+ LabelList,
2075
+ {
2076
+ dataKey: "label",
2077
+ position: "right",
2078
+ className: "fill-foreground",
2079
+ stroke: "none",
2080
+ fontSize: 12
2081
+ }
2082
+ )
2083
+ ] })
2084
+ ] })
2085
+ }
2086
+ );
2087
+ }
2088
+ function relativeDelta(value, compareValue) {
2089
+ if (compareValue === void 0 || compareValue === 0) return null;
2090
+ return (value - compareValue) / Math.abs(compareValue);
2091
+ }
2092
+ var DELTA_FORMAT = new Intl.NumberFormat("fr-FR", {
2093
+ style: "percent",
2094
+ maximumFractionDigits: 1,
2095
+ signDisplay: "exceptZero"
2096
+ });
2097
+ function KpiCard({
2098
+ label,
2099
+ value,
2100
+ compareValue,
2101
+ formatValue = formatNumber,
2102
+ lowerIsBetter = false,
2103
+ sparkline,
2104
+ className
2105
+ }) {
2106
+ const delta = relativeDelta(value, compareValue);
2107
+ const improved = delta === null ? null : delta > 0 !== lowerIsBetter;
2108
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(CardContent, { className: "space-y-1 p-4", children: [
2109
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: label }),
2110
+ /* @__PURE__ */ jsx("p", { className: "text-2xl font-semibold tabular-nums text-foreground", children: formatValue(value) }),
2111
+ delta !== null && /* @__PURE__ */ jsxs(
2112
+ "p",
2113
+ {
2114
+ className: cn(
2115
+ "text-xs font-medium tabular-nums",
2116
+ // Direction is in the sign as well as the colour: a red figure and
2117
+ // a green one differ by more than hue for nobody who cannot see it.
2118
+ improved ? "text-status-success" : "text-status-blocked"
2119
+ ),
2120
+ children: [
2121
+ DELTA_FORMAT.format(delta),
2122
+ compareValue !== void 0 && /* @__PURE__ */ jsxs("span", { className: "ml-1 font-normal text-muted-foreground", children: [
2123
+ "vs ",
2124
+ formatValue(compareValue)
2125
+ ] })
2126
+ ]
2127
+ }
2128
+ ),
2129
+ sparkline && sparkline.length > 1 && /* @__PURE__ */ jsx(ChartContainer, { height: 40, label: `Tendance \u2014 ${label}`, className: "pt-1", children: /* @__PURE__ */ jsx(LineChart$1, { data: sparkline.map((point, index) => ({ index, point })), children: /* @__PURE__ */ jsx(
2130
+ Line,
2131
+ {
2132
+ type: "monotone",
2133
+ dataKey: "point",
2134
+ stroke: colorAt(0),
2135
+ strokeWidth: 1.5,
2136
+ dot: false,
2137
+ ...MARK_PROPS
2138
+ }
2139
+ ) }) })
2140
+ ] }) });
2141
+ }
821
2142
  var ToastContext = createContext(null);
822
2143
  function useToast() {
823
2144
  const toast = useContext(ToastContext);
@@ -863,7 +2184,7 @@ var SIDES = {
863
2184
  bottom: "top-full left-1/2 -translate-x-1/2 mt-1.5",
864
2185
  left: "right-full top-1/2 -translate-y-1/2 mr-1.5"
865
2186
  };
866
- function Tooltip({ label, children, side = "top", className }) {
2187
+ function Tooltip2({ label, children, side = "top", className }) {
867
2188
  return /* @__PURE__ */ jsxs("span", { className: "group/tt relative inline-flex", children: [
868
2189
  children,
869
2190
  /* @__PURE__ */ jsx(
@@ -881,6 +2202,6 @@ function Tooltip({ label, children, side = "top", className }) {
881
2202
  ] });
882
2203
  }
883
2204
 
884
- export { AppApiProvider, AppContextProvider, AppInvokeError, Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, CopyButton, DateRangePicker, DropdownMenu, Input, Link, Modal, Popover, Select, SidePanel, Spinner, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, ToastProvider, Tooltip, badgeVariants, buttonVariants, cn, copyToClipboard, downloadBlob, downloadCsv, toCsv, useAppApi, useAppContext, useAppRun, useAppRuns, useAppSettings, useToast };
2205
+ export { AppApiProvider, AppContextProvider, AppInvokeError, AreaChart, Badge, BarChart, Button, COMPARE_LABELS, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, CopyButton, DataTable, DateRangeCompare, DateRangePicker, DropdownMenu, FunnelChart, Input, KpiCard, LineChart, Link, Modal, PERIOD_PRESETS, Popover, Select, SidePanel, Spinner, StackedBarChart, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, ToastProvider, Tooltip2 as Tooltip, badgeVariants, buttonVariants, cn, comparisonRange, copyToClipboard, downloadBlob, downloadCsv, formatPeriod, formatRange, makePeriod, periodFromPreset, relativeDelta, toCsv, useAppApi, useAppContext, useAppRun, useAppRuns, useAppSettings, useToast };
885
2206
  //# sourceMappingURL=index.js.map
886
2207
  //# sourceMappingURL=index.js.map