@mediaradar/design-components 0.6.0 → 0.7.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
@@ -1545,7 +1545,7 @@ function AxisFrame({
1545
1545
  "div",
1546
1546
  {
1547
1547
  className: "absolute inset-x-0 border-t border-dashed",
1548
- style: { top: `${pct}%`, borderColor: "var(--color-border-default)" }
1548
+ style: { top: `${pct}%`, borderColor: "var(--mr-color-border-default)" }
1549
1549
  },
1550
1550
  pct
1551
1551
  )),
@@ -1618,7 +1618,7 @@ function LineChartPlaceholder({ height = 280 }) {
1618
1618
  {
1619
1619
  points: toPath(pts, 0, LINE_FORECAST_FROM + 1),
1620
1620
  fill: "none",
1621
- stroke: "var(--color-border-strong)",
1621
+ stroke: "var(--mr-color-border-strong)",
1622
1622
  strokeWidth: "2",
1623
1623
  vectorEffect: "non-scaling-stroke",
1624
1624
  strokeLinecap: "round",
@@ -1630,7 +1630,7 @@ function LineChartPlaceholder({ height = 280 }) {
1630
1630
  {
1631
1631
  points: toPath(pts, LINE_FORECAST_FROM, pts.length),
1632
1632
  fill: "none",
1633
- stroke: "var(--color-border-strong)",
1633
+ stroke: "var(--mr-color-border-strong)",
1634
1634
  strokeWidth: "2",
1635
1635
  strokeDasharray: "5 4",
1636
1636
  vectorEffect: "non-scaling-stroke",
@@ -1647,7 +1647,7 @@ function LineChartPlaceholder({ height = 280 }) {
1647
1647
  style: {
1648
1648
  left: `${i / (pts.length - 1) * 100}%`,
1649
1649
  top: `${y}%`,
1650
- backgroundColor: "var(--color-border-strong)"
1650
+ backgroundColor: "var(--mr-color-border-strong)"
1651
1651
  }
1652
1652
  },
1653
1653
  `${s}-${i}`
@@ -1663,7 +1663,7 @@ function PieChartPlaceholder({ height = 220, legendRows = 8 }) {
1663
1663
  const r = 45;
1664
1664
  return /* @__PURE__ */ jsxs14("div", { className: "flex w-full items-center gap-3", style: { height }, role: "presentation", "aria-hidden": "true", children: [
1665
1665
  /* @__PURE__ */ jsx16("div", { className: "flex h-full items-center justify-center", style: { width: "60%" }, children: /* @__PURE__ */ jsxs14("svg", { viewBox: "0 0 100 100", className: "h-full", style: { aspectRatio: "1 / 1" }, children: [
1666
- /* @__PURE__ */ jsx16("circle", { cx: "50", cy: "50", r, fill: "var(--color-subtle)" }),
1666
+ /* @__PURE__ */ jsx16("circle", { cx: "50", cy: "50", r, fill: "var(--mr-color-subtle)" }),
1667
1667
  PIE_SEAMS.map((deg) => {
1668
1668
  const rad = (deg - 90) * Math.PI / 180;
1669
1669
  return /* @__PURE__ */ jsx16(
@@ -1673,7 +1673,7 @@ function PieChartPlaceholder({ height = 220, legendRows = 8 }) {
1673
1673
  y1: "50",
1674
1674
  x2: 50 + r * Math.cos(rad),
1675
1675
  y2: 50 + r * Math.sin(rad),
1676
- stroke: "var(--color-border-default)",
1676
+ stroke: "var(--mr-color-border-default)",
1677
1677
  strokeWidth: "1"
1678
1678
  },
1679
1679
  deg
@@ -1705,7 +1705,7 @@ function StackedColumnPlaceholder({
1705
1705
  height: `${pct}%`,
1706
1706
  /* border-strong, not border-default: on a subtle fill the softer
1707
1707
  seam disappears and the stack reads as one undivided block. */
1708
- borderTop: i === 0 ? void 0 : "1px solid var(--color-border-strong)"
1708
+ borderTop: i === 0 ? void 0 : "1px solid var(--mr-color-border-strong)"
1709
1709
  }
1710
1710
  },
1711
1711
  i
@@ -2552,22 +2552,387 @@ function PublisherBreakdownList({ rows, defaultVisibleCount = 10, padded = true,
2552
2552
  ] }))
2553
2553
  ] });
2554
2554
  }
2555
+
2556
+ // src/Button.tsx
2557
+ import { forwardRef as forwardRef2 } from "react";
2558
+ import { Slot } from "@radix-ui/react-slot";
2559
+ import { cva } from "class-variance-authority";
2560
+ import { Loader2 as Loader22 } from "lucide-react";
2561
+
2562
+ // src/cn.ts
2563
+ import { clsx } from "clsx";
2564
+ import { twMerge } from "tailwind-merge";
2565
+ function cn(...inputs) {
2566
+ return twMerge(clsx(inputs));
2567
+ }
2568
+
2569
+ // src/Button.tsx
2570
+ import { jsx as jsx24, jsxs as jsxs22 } from "react/jsx-runtime";
2571
+ var buttonVariants = cva(
2572
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-control font-medium transition-all duration-(--mr-motion-duration-interactive) ease-interactive disabled:pointer-events-none disabled:opacity-40 focus-visible:outline-hidden focus-visible:ring-0",
2573
+ {
2574
+ variants: {
2575
+ variant: {
2576
+ primary: "bg-brand-primaryAction text-brand-onprimary hover:brightness-110",
2577
+ secondary: "bg-surface text-text-primary border border-border-interactive hover:bg-subtle",
2578
+ ghost: "text-text-primary hover:bg-subtle",
2579
+ danger: "bg-status-danger text-status-onstatus hover:brightness-110",
2580
+ link: "text-text-link underline-offset-4 hover:underline px-0"
2581
+ },
2582
+ size: {
2583
+ sm: "h-8 px-3 text-sm",
2584
+ md: "h-10 px-4 text-sm",
2585
+ lg: "h-11 px-6 text-sm",
2586
+ icon: "h-9 w-9 p-0"
2587
+ }
2588
+ },
2589
+ defaultVariants: {
2590
+ variant: "primary",
2591
+ size: "md"
2592
+ }
2593
+ }
2594
+ );
2595
+ var Button = forwardRef2(
2596
+ ({ className, variant, size, asChild = false, loading = false, disabled, children, ...props }, ref) => {
2597
+ const Comp = asChild ? Slot : "button";
2598
+ return /* @__PURE__ */ jsxs22(
2599
+ Comp,
2600
+ {
2601
+ className: cn(buttonVariants({ variant, size, className })),
2602
+ ref,
2603
+ disabled: disabled || loading,
2604
+ "aria-busy": loading || void 0,
2605
+ ...props,
2606
+ children: [
2607
+ loading && /* @__PURE__ */ jsx24(Loader22, { className: "size-4 shrink-0 animate-spin", "aria-hidden": "true" }),
2608
+ children
2609
+ ]
2610
+ }
2611
+ );
2612
+ }
2613
+ );
2614
+ Button.displayName = "Button";
2615
+
2616
+ // src/FilterDropdownFooter.tsx
2617
+ import { jsx as jsx25, jsxs as jsxs23 } from "react/jsx-runtime";
2618
+ function FilterDropdownFooter({
2619
+ onClear,
2620
+ onApply,
2621
+ clearLabel = "Clear all",
2622
+ applyLabel = "Apply"
2623
+ }) {
2624
+ return /* @__PURE__ */ jsxs23(
2625
+ "div",
2626
+ {
2627
+ className: cn(
2628
+ "flex items-center gap-2 border-t border-border-default p-2",
2629
+ onClear ? "justify-between" : "justify-end"
2630
+ ),
2631
+ children: [
2632
+ onClear && /* @__PURE__ */ jsx25(Button, { type: "button", variant: "ghost", size: "sm", onClick: onClear, children: clearLabel }),
2633
+ /* @__PURE__ */ jsx25(Button, { type: "button", variant: "primary", size: "sm", onClick: onApply, children: applyLabel })
2634
+ ]
2635
+ }
2636
+ );
2637
+ }
2638
+
2639
+ // src/NameListModal.tsx
2640
+ import { useEffect as useEffect10, useState as useState11 } from "react";
2641
+ import { createPortal as createPortal5 } from "react-dom";
2642
+ import { jsx as jsx26, jsxs as jsxs24 } from "react/jsx-runtime";
2643
+ function NameListModal({ title, initialName, submitLabel, onCancel, onSubmit }) {
2644
+ const [name, setName] = useState11(initialName);
2645
+ useEffect10(() => {
2646
+ const onKey = (e) => e.key === "Escape" && onCancel();
2647
+ window.addEventListener("keydown", onKey);
2648
+ return () => window.removeEventListener("keydown", onKey);
2649
+ }, [onCancel]);
2650
+ return createPortal5(
2651
+ // The scrim is a literal: @mediaradar/design-tokens has no scrim token yet.
2652
+ // Same light/dark values as insights-studio-prototype's local --color-scrim;
2653
+ // the dark one keys off data-theme, the attribute design-tokens' dark.css uses.
2654
+ /* @__PURE__ */ jsx26(
2655
+ "div",
2656
+ {
2657
+ className: "fixed inset-0 z-70 flex items-center justify-center bg-[rgba(20,17,13,0.55)] in-data-[theme=dark]:bg-[rgba(0,0,0,0.65)] px-4",
2658
+ onMouseDown: (e) => e.target === e.currentTarget && onCancel(),
2659
+ children: /* @__PURE__ */ jsxs24(
2660
+ "div",
2661
+ {
2662
+ role: "dialog",
2663
+ "aria-modal": "true",
2664
+ "aria-label": title,
2665
+ className: "w-full max-w-[420px] rounded-card border border-border-default bg-surface p-5 shadow-overlay",
2666
+ children: [
2667
+ /* @__PURE__ */ jsx26("h2", { className: "text-[16px] font-semibold text-text-primary", children: title }),
2668
+ /* @__PURE__ */ jsxs24("div", { className: "mt-4 flex flex-col gap-1.5", children: [
2669
+ /* @__PURE__ */ jsx26("label", { htmlFor: "account-list-name", className: "text-[12px] font-medium text-text-secondary", children: "List name" }),
2670
+ /* @__PURE__ */ jsx26(
2671
+ "input",
2672
+ {
2673
+ id: "account-list-name",
2674
+ autoFocus: true,
2675
+ value: name,
2676
+ onChange: (e) => setName(e.target.value),
2677
+ placeholder: "e.g. Q4 prospecting targets",
2678
+ className: "h-10 rounded-control border border-border-default bg-input px-3 text-sm outline-hidden focus:border-brand-primaryAction"
2679
+ }
2680
+ )
2681
+ ] }),
2682
+ /* @__PURE__ */ jsxs24("div", { className: "mt-5 flex items-center justify-end gap-2", children: [
2683
+ /* @__PURE__ */ jsx26(Button, { variant: "secondary", onClick: onCancel, children: "Cancel" }),
2684
+ /* @__PURE__ */ jsx26(Button, { disabled: !name.trim(), onClick: () => onSubmit(name.trim()), children: submitLabel })
2685
+ ] })
2686
+ ]
2687
+ }
2688
+ )
2689
+ }
2690
+ ),
2691
+ document.body
2692
+ );
2693
+ }
2694
+
2695
+ // src/ChipTrigger.tsx
2696
+ import { jsx as jsx27, jsxs as jsxs25 } from "react/jsx-runtime";
2697
+ function chipTriggerClass() {
2698
+ return cn(
2699
+ "flex h-11 items-center gap-2 rounded-control border bg-input px-2.5 text-text-secondary transition-colors hover:bg-subtle hover:text-text-primary focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-border-focus",
2700
+ // Sits on the input fill, with a stroke dark enough to still read as a
2701
+ // control against the warm canvas.
2702
+ "border-border-strong"
2703
+ );
2704
+ }
2705
+ function ChipContent({ caption, value, icon }) {
2706
+ const stack = /* @__PURE__ */ jsxs25("span", { className: "flex flex-col gap-1 text-left leading-none", children: [
2707
+ /* @__PURE__ */ jsx27("span", { className: "text-[10px] uppercase tracking-wide text-text-tertiary", children: caption }),
2708
+ /* @__PURE__ */ jsx27("span", { className: "text-[12px] font-medium text-text-primary", children: value })
2709
+ ] });
2710
+ if (icon) {
2711
+ return /* @__PURE__ */ jsxs25("span", { className: "flex items-center gap-2", children: [
2712
+ /* @__PURE__ */ jsx27("span", { className: "shrink-0 text-text-tertiary", children: icon }),
2713
+ stack
2714
+ ] });
2715
+ }
2716
+ return stack;
2717
+ }
2718
+
2719
+ // src/FilterChipBar.tsx
2720
+ import { CircleSlash, X as X3 } from "lucide-react";
2721
+ import { jsx as jsx28, jsxs as jsxs26 } from "react/jsx-runtime";
2722
+ function FilterChipBar({ chips, onRemove, onClearAll, parkedKeys }) {
2723
+ if (chips.length === 0) return null;
2724
+ return /* @__PURE__ */ jsxs26("div", { className: "flex flex-wrap items-center gap-1.5 border-t border-border-default pt-2.5", children: [
2725
+ chips.map((c) => /* @__PURE__ */ jsx28(
2726
+ Chip,
2727
+ {
2728
+ label: c.label,
2729
+ groupLabel: c.groupLabel,
2730
+ parked: !!parkedKeys?.has(c.key),
2731
+ onRemove: () => onRemove(c)
2732
+ },
2733
+ c.key
2734
+ )),
2735
+ /* @__PURE__ */ jsx28("button", { type: "button", onClick: onClearAll, className: "ml-1 text-[12px] font-medium text-text-link hover:underline", children: "Clear all" })
2736
+ ] });
2737
+ }
2738
+ function Chip({
2739
+ label,
2740
+ groupLabel,
2741
+ parked,
2742
+ onRemove
2743
+ }) {
2744
+ return /* @__PURE__ */ jsxs26("span", { className: "group relative inline-flex", children: [
2745
+ /* @__PURE__ */ jsxs26(
2746
+ "span",
2747
+ {
2748
+ className: cn(
2749
+ "flex items-center gap-1 rounded-full px-2.5 py-1 text-[12px] font-medium ring-1 transition-colors",
2750
+ parked ? (
2751
+ // No fill, dashed edge, muted label: reads as "held, not acting on
2752
+ // anything" against the solid applied chips beside it.
2753
+ "border border-dashed border-border-default bg-transparent text-text-tertiary ring-transparent group-hover:text-text-secondary"
2754
+ ) : "bg-subtle text-text-secondary ring-transparent group-hover:bg-border-default group-hover:text-text-primary group-hover:ring-border-interactive"
2755
+ ),
2756
+ children: [
2757
+ parked && /* @__PURE__ */ jsx28(CircleSlash, { size: 11, "aria-hidden": "true", className: "shrink-0 text-text-tertiary" }),
2758
+ label,
2759
+ /* @__PURE__ */ jsx28(
2760
+ "button",
2761
+ {
2762
+ type: "button",
2763
+ onClick: onRemove,
2764
+ "aria-label": `Remove ${groupLabel}: ${label}`,
2765
+ className: "text-text-tertiary transition-colors hover:text-text-primary",
2766
+ children: /* @__PURE__ */ jsx28(X3, { size: 12 })
2767
+ }
2768
+ )
2769
+ ]
2770
+ }
2771
+ ),
2772
+ /* @__PURE__ */ jsx28(
2773
+ "span",
2774
+ {
2775
+ role: "tooltip",
2776
+ className: "pointer-events-none absolute bottom-full left-1/2 z-20 mb-1.5 -translate-x-1/2 whitespace-nowrap rounded-md bg-text-primary px-2 py-1 text-[11px] font-medium text-surface opacity-0 shadow-raised transition-opacity duration-150 group-hover:opacity-100",
2777
+ children: parked ? "Not in this workspace" : groupLabel
2778
+ }
2779
+ )
2780
+ ] });
2781
+ }
2782
+
2783
+ // src/SortButton.tsx
2784
+ import { ArrowUp, ArrowDown, ArrowUpDown } from "lucide-react";
2785
+ import { jsx as jsx29, jsxs as jsxs27 } from "react/jsx-runtime";
2786
+ function SortButton({ label, sortKey, sort, onSort, disabled = false }) {
2787
+ const active = !disabled && sort.key === sortKey;
2788
+ return /* @__PURE__ */ jsxs27(
2789
+ "button",
2790
+ {
2791
+ type: "button",
2792
+ disabled,
2793
+ onClick: () => onSort(sortKey),
2794
+ "aria-sort": active ? sort.dir === "asc" ? "ascending" : "descending" : "none",
2795
+ className: cn(
2796
+ // text-left overrides the UA default on <button>, which would centre a
2797
+ // label that wraps onto two lines, out of step with the column under it.
2798
+ "group inline-flex items-center gap-1 text-left uppercase tracking-wide transition-colors",
2799
+ !disabled && "hover:text-text-primary"
2800
+ ),
2801
+ children: [
2802
+ label,
2803
+ !disabled && (active ? sort.dir === "asc" ? /* @__PURE__ */ jsx29(ArrowUp, { size: 12, className: "shrink-0 text-brand-primaryAction" }) : /* @__PURE__ */ jsx29(ArrowDown, { size: 12, className: "shrink-0 text-brand-primaryAction" }) : /* @__PURE__ */ jsx29(
2804
+ ArrowUpDown,
2805
+ {
2806
+ size: 12,
2807
+ className: "shrink-0 text-text-tertiary opacity-50 transition-opacity group-hover:opacity-100"
2808
+ }
2809
+ ))
2810
+ ]
2811
+ }
2812
+ );
2813
+ }
2814
+
2815
+ // src/tableSort.ts
2816
+ function cycleSort(current, key, firstDir = "desc") {
2817
+ if (!current || current.key !== key) return { key, dir: firstDir };
2818
+ if (current.dir === firstDir) return { key, dir: firstDir === "desc" ? "asc" : "desc" };
2819
+ return null;
2820
+ }
2821
+ function sortProp(sort) {
2822
+ return sort ?? { key: null, dir: "desc" };
2823
+ }
2824
+
2825
+ // src/Pager.tsx
2826
+ import { ChevronLeft as ChevronLeft3, ChevronRight as ChevronRight4 } from "lucide-react";
2827
+ import { jsx as jsx30, jsxs as jsxs28 } from "react/jsx-runtime";
2828
+ function pageWindow(current, total) {
2829
+ if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
2830
+ const keep = /* @__PURE__ */ new Set([1, total, current, current - 1, current + 1]);
2831
+ const pages = [...keep].filter((p) => p >= 1 && p <= total).sort((a, b) => a - b);
2832
+ const out = [];
2833
+ let prev = 0;
2834
+ for (const p of pages) {
2835
+ if (p - prev > 1) out.push(`gap-${p}`);
2836
+ out.push(p);
2837
+ prev = p;
2838
+ }
2839
+ return out;
2840
+ }
2841
+ function Pager({ page, totalPages, onPage }) {
2842
+ const btn = "flex h-8 min-w-8 items-center justify-center rounded-control px-2 text-[13px] transition-colors";
2843
+ return /* @__PURE__ */ jsxs28("div", { className: "flex items-center gap-1", children: [
2844
+ /* @__PURE__ */ jsx30(
2845
+ "button",
2846
+ {
2847
+ type: "button",
2848
+ onClick: () => onPage(page - 1),
2849
+ disabled: page === 1,
2850
+ "aria-label": "Previous page",
2851
+ className: cn(btn, "text-text-secondary hover:bg-subtle hover:text-text-primary disabled:pointer-events-none disabled:opacity-30"),
2852
+ children: /* @__PURE__ */ jsx30(ChevronLeft3, { size: 16 })
2853
+ }
2854
+ ),
2855
+ pageWindow(page, totalPages).map(
2856
+ (p) => typeof p === "string" ? /* @__PURE__ */ jsx30("span", { className: "px-1 text-[13px] text-text-tertiary", children: "\u2026" }, p) : /* @__PURE__ */ jsx30(
2857
+ "button",
2858
+ {
2859
+ type: "button",
2860
+ onClick: () => onPage(p),
2861
+ "aria-current": p === page ? "page" : void 0,
2862
+ className: cn(
2863
+ btn,
2864
+ p === page ? "bg-brand-primaryAction font-semibold text-brand-onprimary" : "text-text-secondary hover:bg-subtle hover:text-text-primary"
2865
+ ),
2866
+ children: p
2867
+ },
2868
+ p
2869
+ )
2870
+ ),
2871
+ /* @__PURE__ */ jsx30(
2872
+ "button",
2873
+ {
2874
+ type: "button",
2875
+ onClick: () => onPage(page + 1),
2876
+ disabled: page === totalPages,
2877
+ "aria-label": "Next page",
2878
+ className: cn(btn, "text-text-secondary hover:bg-subtle hover:text-text-primary disabled:pointer-events-none disabled:opacity-30"),
2879
+ children: /* @__PURE__ */ jsx30(ChevronRight4, { size: 16 })
2880
+ }
2881
+ )
2882
+ ] });
2883
+ }
2884
+
2885
+ // src/WorkspaceAvatar.tsx
2886
+ import { jsx as jsx31 } from "react/jsx-runtime";
2887
+ function workspaceInitials(name) {
2888
+ const words = name.trim().split(/\s+/).filter(Boolean);
2889
+ if (words.length === 0) return "\xB7";
2890
+ if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
2891
+ return (words[0][0] + words[1][0]).toUpperCase();
2892
+ }
2893
+ var SIZES = {
2894
+ /** Matches a 15px lucide icon exactly, so a workspace name in a menu row
2895
+ * left-aligns with labels in icon-led rows above and below it. */
2896
+ xs: "h-[15px] w-[15px] rounded-[4px] text-[8px] tracking-tight",
2897
+ sm: "h-6 w-6 text-[10px]",
2898
+ md: "h-7 w-7 text-[11px]",
2899
+ lg: "h-11 w-11 text-[15px]"
2900
+ };
2901
+ function WorkspaceAvatar({ name, size = "md" }) {
2902
+ return /* @__PURE__ */ jsx31(
2903
+ "span",
2904
+ {
2905
+ "aria-hidden": "true",
2906
+ className: cn(
2907
+ "flex shrink-0 items-center justify-center rounded-control bg-subtle font-semibold leading-none text-text-secondary",
2908
+ SIZES[size]
2909
+ ),
2910
+ children: workspaceInitials(name ?? "All data")
2911
+ }
2912
+ );
2913
+ }
2555
2914
  export {
2556
2915
  AdvertiserNoResults,
2557
2916
  AdvertiserQuickFilter,
2558
2917
  AdvertiserSearchPrompt,
2559
2918
  BarChartPlaceholder,
2919
+ Button,
2560
2920
  Card,
2561
2921
  CheckBox,
2922
+ ChipContent,
2562
2923
  DEFAULT_SEARCH_MATCH,
2563
2924
  DataScopeFilter,
2564
2925
  DateRangeFilter,
2926
+ FilterChipBar,
2927
+ FilterDropdownFooter,
2565
2928
  InfoTip,
2566
2929
  LineChartPlaceholder,
2567
2930
  NO_RESULTS_BODY,
2568
2931
  NO_RESULTS_TITLE,
2932
+ NameListModal,
2569
2933
  NoResultsEmptyState,
2570
2934
  PRIMARY_ACTION_CLASS,
2935
+ Pager,
2571
2936
  PieChartPlaceholder,
2572
2937
  PublisherBreakdownList,
2573
2938
  QUIET_ACTION_CLASS,
@@ -2577,9 +2942,15 @@ export {
2577
2942
  SelectFilter,
2578
2943
  SelectedSection,
2579
2944
  SelectionPicker,
2945
+ SortButton,
2580
2946
  StackedColumnPlaceholder,
2581
2947
  Tooltip,
2948
+ WorkspaceAvatar,
2582
2949
  WorkspaceSelectedCard,
2583
- noResultsBody
2950
+ buttonVariants,
2951
+ chipTriggerClass,
2952
+ cycleSort,
2953
+ noResultsBody,
2954
+ sortProp
2584
2955
  };
2585
2956
  //# sourceMappingURL=index.js.map