@mediaradar/design-components 0.5.1 → 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/README.md CHANGED
@@ -5,9 +5,8 @@ components genuinely common across consuming apps (`insights-studio-prototype`,
5
5
  `mr-clientapps-insights-studio`), published as a real npm package so there's
6
6
  structurally only one implementation instead of two that can silently drift.
7
7
 
8
- **Status: spike/pilot.** One component (`InfoTip`) so far, not yet published
9
- to the real npm registry, not yet consumed by `mr-clientapps-insights-studio`
10
- at all. See `gh-design-system`'s open spike PR for the current state.
8
+ Published to npm and consumed by `insights-studio-prototype`. See
9
+ `CHANGELOG.md` for what each version contains.
11
10
 
12
11
  ---
13
12
 
@@ -24,6 +23,16 @@ components, for example), forcing it into a shared package is a downgrade
24
23
  for whichever app had the better version, not an improvement. Leave it
25
24
  where it is.
26
25
 
26
+ **Exception: generic design-system primitives** (decided 2026-09-25).
27
+ Basic building blocks like `Button`, `Toggle`, `AISpark` and `Freshness`
28
+ belong here even without an `mr-clientapps` duplicate. Two reasons: shared
29
+ components are built on them (`FilterDropdownFooter` and `NameListModal`
30
+ can't move without `Button`), and the component catalog and style guide
31
+ are moving to read from this package alone, so a primitive left behind
32
+ would drop out of both. Page- or data-specific components with no
33
+ duplicate (`EmbeddedFrame`, `ParkedSection`) still stay in their app, and
34
+ out of the catalog.
35
+
27
36
  **The escape hatch — a genuine conflict discovered mid-migration.** Sometimes
28
37
  a component looks like a good candidate until reconciliation actually starts,
29
38
  and it turns out the two apps need behavior that's genuinely incompatible —
@@ -78,3 +87,35 @@ use Tailwind class names that only resolve correctly once a consuming app
78
87
  already has that package's Tailwind theme wired up. Declaring it as a peer
79
88
  (rather than not declaring it at all) means `npm install` at least warns if
80
89
  it's missing, instead of the component silently rendering unstyled.
90
+
91
+ The minimum version in that range is enforced, not guessed: this package's
92
+ CI (`npm run check:token-compat`) compiles the built bundle with real
93
+ Tailwind against the minimum allowed `@mediaradar/design-tokens` and the
94
+ latest published one, and fails if any class generates no CSS. If a
95
+ component needs a newer token, raise the minimum in the same PR.
96
+
97
+ ## Consuming app setup
98
+
99
+ Tailwind v4 skips `node_modules` when scanning for classes, so a class used
100
+ only inside this package compiles to nothing, with no build error. Point
101
+ Tailwind at the compiled bundle, after the app's last `@import`:
102
+
103
+ ```css
104
+ @source '../node_modules/@mediaradar/design-components/dist/index.js';
105
+ ```
106
+
107
+ (Tailwind v3 / `@config` apps: add
108
+ `./node_modules/@mediaradar/design-components/dist/*.{js,cjs}` to the
109
+ `content` array instead.)
110
+
111
+ Then verify it in CI, after the app's production build:
112
+
113
+ ```bash
114
+ npx design-components-check-coverage --css-dir dist # Vite
115
+ npx design-components-check-coverage --css-dir .next/static # Next.js
116
+ ```
117
+
118
+ It fails if any class the installed version of this package uses is
119
+ missing from the app's compiled CSS — whether because Tailwind isn't
120
+ scanning the package, or because the app's `@mediaradar/design-tokens` is
121
+ older than this package's peer range.
package/dist/index.cjs CHANGED
@@ -24,17 +24,23 @@ __export(index_exports, {
24
24
  AdvertiserQuickFilter: () => AdvertiserQuickFilter,
25
25
  AdvertiserSearchPrompt: () => AdvertiserSearchPrompt,
26
26
  BarChartPlaceholder: () => BarChartPlaceholder,
27
+ Button: () => Button,
27
28
  Card: () => Card,
28
29
  CheckBox: () => CheckBox,
30
+ ChipContent: () => ChipContent,
29
31
  DEFAULT_SEARCH_MATCH: () => DEFAULT_SEARCH_MATCH,
30
32
  DataScopeFilter: () => DataScopeFilter,
31
33
  DateRangeFilter: () => DateRangeFilter,
34
+ FilterChipBar: () => FilterChipBar,
35
+ FilterDropdownFooter: () => FilterDropdownFooter,
32
36
  InfoTip: () => InfoTip,
33
37
  LineChartPlaceholder: () => LineChartPlaceholder,
34
38
  NO_RESULTS_BODY: () => NO_RESULTS_BODY,
35
39
  NO_RESULTS_TITLE: () => NO_RESULTS_TITLE,
40
+ NameListModal: () => NameListModal,
36
41
  NoResultsEmptyState: () => NoResultsEmptyState,
37
42
  PRIMARY_ACTION_CLASS: () => PRIMARY_ACTION_CLASS,
43
+ Pager: () => Pager,
38
44
  PieChartPlaceholder: () => PieChartPlaceholder,
39
45
  PublisherBreakdownList: () => PublisherBreakdownList,
40
46
  QUIET_ACTION_CLASS: () => QUIET_ACTION_CLASS,
@@ -44,10 +50,16 @@ __export(index_exports, {
44
50
  SelectFilter: () => SelectFilter,
45
51
  SelectedSection: () => SelectedSection,
46
52
  SelectionPicker: () => SelectionPicker,
53
+ SortButton: () => SortButton,
47
54
  StackedColumnPlaceholder: () => StackedColumnPlaceholder,
48
55
  Tooltip: () => Tooltip,
56
+ WorkspaceAvatar: () => WorkspaceAvatar,
49
57
  WorkspaceSelectedCard: () => WorkspaceSelectedCard,
50
- noResultsBody: () => noResultsBody
58
+ buttonVariants: () => buttonVariants,
59
+ chipTriggerClass: () => chipTriggerClass,
60
+ cycleSort: () => cycleSort,
61
+ noResultsBody: () => noResultsBody,
62
+ sortProp: () => sortProp
51
63
  });
52
64
  module.exports = __toCommonJS(index_exports);
53
65
 
@@ -115,7 +127,7 @@ function InfoTip({ text, size = 11, label = "More information", trigger = "click
115
127
  {
116
128
  role: "tooltip",
117
129
  style: { position: "fixed", top: pos.top, left: pos.left, width: pos.width },
118
- className: "z-[90] rounded-control border border-border bg-surface px-3 py-2 text-[12px] font-normal not-italic normal-case leading-relaxed tracking-normal text-text-secondary shadow-overlay",
130
+ className: "z-[90] rounded-control border border-border-default bg-surface px-3 py-2 text-[12px] font-normal not-italic normal-case leading-relaxed tracking-normal text-text-secondary shadow-overlay",
119
131
  children: text
120
132
  }
121
133
  ),
@@ -1137,7 +1149,7 @@ function SectionRail({
1137
1149
  type: "button",
1138
1150
  onClick: () => scroll(-1),
1139
1151
  "aria-label": "Previous",
1140
- className: "hidden h-10 w-10 items-center justify-center rounded-control border border-border text-text-secondary transition-colors hover:bg-subtle hover:text-text-primary sm:flex lg:hidden",
1152
+ className: "hidden h-10 w-10 items-center justify-center rounded-control border border-border-default text-text-secondary transition-colors hover:bg-subtle hover:text-text-primary sm:flex lg:hidden",
1141
1153
  children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react7.ChevronLeft, { size: 16 })
1142
1154
  }
1143
1155
  ),
@@ -1147,7 +1159,7 @@ function SectionRail({
1147
1159
  type: "button",
1148
1160
  onClick: () => scroll(1),
1149
1161
  "aria-label": "Next",
1150
- className: "hidden h-10 w-10 items-center justify-center rounded-control border border-border text-text-secondary transition-colors hover:bg-subtle hover:text-text-primary sm:flex lg:hidden",
1162
+ className: "hidden h-10 w-10 items-center justify-center rounded-control border border-border-default text-text-secondary transition-colors hover:bg-subtle hover:text-text-primary sm:flex lg:hidden",
1151
1163
  children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react7.ChevronRight, { size: 16 })
1152
1164
  }
1153
1165
  )
@@ -1498,7 +1510,7 @@ function cx5(...parts) {
1498
1510
  return parts.filter(Boolean).join(" ");
1499
1511
  }
1500
1512
  function WorkspaceSelectedCard({ groups, testIdPrefix }) {
1501
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("aside", { className: "sticky top-4 flex min-w-0 flex-col gap-4 self-start rounded-card border border-border bg-surface px-5 pb-5 pt-4 shadow-raised sm:px-6 sm:pb-6", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex flex-col divide-y divide-border", children: groups.map((g) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SummaryGroupItem, { group: g, testIdPrefix }, g.id)) }) });
1513
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("aside", { className: "sticky top-4 flex min-w-0 flex-col gap-4 self-start rounded-card border border-border-default bg-surface px-5 pb-5 pt-4 shadow-raised sm:px-6 sm:pb-6", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex flex-col divide-y divide-border-default", children: groups.map((g) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SummaryGroupItem, { group: g, testIdPrefix }, g.id)) }) });
1502
1514
  }
1503
1515
  function SummaryGroupItem({
1504
1516
  group,
@@ -1543,7 +1555,7 @@ function SummaryGroupItem({
1543
1555
  group.items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1544
1556
  "div",
1545
1557
  {
1546
- className: "flex items-center justify-between gap-2 rounded-control border border-border bg-canvas px-2 py-1",
1558
+ className: "flex items-center justify-between gap-2 rounded-control border border-border-default bg-canvas px-2 py-1",
1547
1559
  children: [
1548
1560
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "min-w-0 flex-1", children: [
1549
1561
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "block truncate text-[12px] text-text-primary", children: item.name }),
@@ -1598,7 +1610,7 @@ function AxisFrame({
1598
1610
  "div",
1599
1611
  {
1600
1612
  className: "absolute inset-x-0 border-t border-dashed",
1601
- style: { top: `${pct}%`, borderColor: "var(--color-border-default)" }
1613
+ style: { top: `${pct}%`, borderColor: "var(--mr-color-border-default)" }
1602
1614
  },
1603
1615
  pct
1604
1616
  )),
@@ -1671,7 +1683,7 @@ function LineChartPlaceholder({ height = 280 }) {
1671
1683
  {
1672
1684
  points: toPath(pts, 0, LINE_FORECAST_FROM + 1),
1673
1685
  fill: "none",
1674
- stroke: "var(--color-border-strong)",
1686
+ stroke: "var(--mr-color-border-strong)",
1675
1687
  strokeWidth: "2",
1676
1688
  vectorEffect: "non-scaling-stroke",
1677
1689
  strokeLinecap: "round",
@@ -1683,7 +1695,7 @@ function LineChartPlaceholder({ height = 280 }) {
1683
1695
  {
1684
1696
  points: toPath(pts, LINE_FORECAST_FROM, pts.length),
1685
1697
  fill: "none",
1686
- stroke: "var(--color-border-strong)",
1698
+ stroke: "var(--mr-color-border-strong)",
1687
1699
  strokeWidth: "2",
1688
1700
  strokeDasharray: "5 4",
1689
1701
  vectorEffect: "non-scaling-stroke",
@@ -1700,7 +1712,7 @@ function LineChartPlaceholder({ height = 280 }) {
1700
1712
  style: {
1701
1713
  left: `${i / (pts.length - 1) * 100}%`,
1702
1714
  top: `${y}%`,
1703
- backgroundColor: "var(--color-border-strong)"
1715
+ backgroundColor: "var(--mr-color-border-strong)"
1704
1716
  }
1705
1717
  },
1706
1718
  `${s}-${i}`
@@ -1716,7 +1728,7 @@ function PieChartPlaceholder({ height = 220, legendRows = 8 }) {
1716
1728
  const r = 45;
1717
1729
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex w-full items-center gap-3", style: { height }, role: "presentation", "aria-hidden": "true", children: [
1718
1730
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "flex h-full items-center justify-center", style: { width: "60%" }, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("svg", { viewBox: "0 0 100 100", className: "h-full", style: { aspectRatio: "1 / 1" }, children: [
1719
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("circle", { cx: "50", cy: "50", r, fill: "var(--color-subtle)" }),
1731
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("circle", { cx: "50", cy: "50", r, fill: "var(--mr-color-subtle)" }),
1720
1732
  PIE_SEAMS.map((deg) => {
1721
1733
  const rad = (deg - 90) * Math.PI / 180;
1722
1734
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
@@ -1726,7 +1738,7 @@ function PieChartPlaceholder({ height = 220, legendRows = 8 }) {
1726
1738
  y1: "50",
1727
1739
  x2: 50 + r * Math.cos(rad),
1728
1740
  y2: 50 + r * Math.sin(rad),
1729
- stroke: "var(--color-border-default)",
1741
+ stroke: "var(--mr-color-border-default)",
1730
1742
  strokeWidth: "1"
1731
1743
  },
1732
1744
  deg
@@ -1758,7 +1770,7 @@ function StackedColumnPlaceholder({
1758
1770
  height: `${pct}%`,
1759
1771
  /* border-strong, not border-default: on a subtle fill the softer
1760
1772
  seam disappears and the stack reads as one undivided block. */
1761
- borderTop: i === 0 ? void 0 : "1px solid var(--color-border-strong)"
1773
+ borderTop: i === 0 ? void 0 : "1px solid var(--mr-color-border-strong)"
1762
1774
  }
1763
1775
  },
1764
1776
  i
@@ -2605,23 +2617,388 @@ function PublisherBreakdownList({ rows, defaultVisibleCount = 10, padded = true,
2605
2617
  ] }))
2606
2618
  ] });
2607
2619
  }
2620
+
2621
+ // src/Button.tsx
2622
+ var import_react13 = require("react");
2623
+ var import_react_slot = require("@radix-ui/react-slot");
2624
+ var import_class_variance_authority = require("class-variance-authority");
2625
+ var import_lucide_react15 = require("lucide-react");
2626
+
2627
+ // src/cn.ts
2628
+ var import_clsx = require("clsx");
2629
+ var import_tailwind_merge = require("tailwind-merge");
2630
+ function cn(...inputs) {
2631
+ return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
2632
+ }
2633
+
2634
+ // src/Button.tsx
2635
+ var import_jsx_runtime24 = require("react/jsx-runtime");
2636
+ var buttonVariants = (0, import_class_variance_authority.cva)(
2637
+ "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",
2638
+ {
2639
+ variants: {
2640
+ variant: {
2641
+ primary: "bg-brand-primaryAction text-brand-onprimary hover:brightness-110",
2642
+ secondary: "bg-surface text-text-primary border border-border-interactive hover:bg-subtle",
2643
+ ghost: "text-text-primary hover:bg-subtle",
2644
+ danger: "bg-status-danger text-status-onstatus hover:brightness-110",
2645
+ link: "text-text-link underline-offset-4 hover:underline px-0"
2646
+ },
2647
+ size: {
2648
+ sm: "h-8 px-3 text-sm",
2649
+ md: "h-10 px-4 text-sm",
2650
+ lg: "h-11 px-6 text-sm",
2651
+ icon: "h-9 w-9 p-0"
2652
+ }
2653
+ },
2654
+ defaultVariants: {
2655
+ variant: "primary",
2656
+ size: "md"
2657
+ }
2658
+ }
2659
+ );
2660
+ var Button = (0, import_react13.forwardRef)(
2661
+ ({ className, variant, size, asChild = false, loading = false, disabled, children, ...props }, ref) => {
2662
+ const Comp = asChild ? import_react_slot.Slot : "button";
2663
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
2664
+ Comp,
2665
+ {
2666
+ className: cn(buttonVariants({ variant, size, className })),
2667
+ ref,
2668
+ disabled: disabled || loading,
2669
+ "aria-busy": loading || void 0,
2670
+ ...props,
2671
+ children: [
2672
+ loading && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_lucide_react15.Loader2, { className: "size-4 shrink-0 animate-spin", "aria-hidden": "true" }),
2673
+ children
2674
+ ]
2675
+ }
2676
+ );
2677
+ }
2678
+ );
2679
+ Button.displayName = "Button";
2680
+
2681
+ // src/FilterDropdownFooter.tsx
2682
+ var import_jsx_runtime25 = require("react/jsx-runtime");
2683
+ function FilterDropdownFooter({
2684
+ onClear,
2685
+ onApply,
2686
+ clearLabel = "Clear all",
2687
+ applyLabel = "Apply"
2688
+ }) {
2689
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
2690
+ "div",
2691
+ {
2692
+ className: cn(
2693
+ "flex items-center gap-2 border-t border-border-default p-2",
2694
+ onClear ? "justify-between" : "justify-end"
2695
+ ),
2696
+ children: [
2697
+ onClear && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Button, { type: "button", variant: "ghost", size: "sm", onClick: onClear, children: clearLabel }),
2698
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Button, { type: "button", variant: "primary", size: "sm", onClick: onApply, children: applyLabel })
2699
+ ]
2700
+ }
2701
+ );
2702
+ }
2703
+
2704
+ // src/NameListModal.tsx
2705
+ var import_react14 = require("react");
2706
+ var import_react_dom5 = require("react-dom");
2707
+ var import_jsx_runtime26 = require("react/jsx-runtime");
2708
+ function NameListModal({ title, initialName, submitLabel, onCancel, onSubmit }) {
2709
+ const [name, setName] = (0, import_react14.useState)(initialName);
2710
+ (0, import_react14.useEffect)(() => {
2711
+ const onKey = (e) => e.key === "Escape" && onCancel();
2712
+ window.addEventListener("keydown", onKey);
2713
+ return () => window.removeEventListener("keydown", onKey);
2714
+ }, [onCancel]);
2715
+ return (0, import_react_dom5.createPortal)(
2716
+ // The scrim is a literal: @mediaradar/design-tokens has no scrim token yet.
2717
+ // Same light/dark values as insights-studio-prototype's local --color-scrim;
2718
+ // the dark one keys off data-theme, the attribute design-tokens' dark.css uses.
2719
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2720
+ "div",
2721
+ {
2722
+ 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",
2723
+ onMouseDown: (e) => e.target === e.currentTarget && onCancel(),
2724
+ children: /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
2725
+ "div",
2726
+ {
2727
+ role: "dialog",
2728
+ "aria-modal": "true",
2729
+ "aria-label": title,
2730
+ className: "w-full max-w-[420px] rounded-card border border-border-default bg-surface p-5 shadow-overlay",
2731
+ children: [
2732
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("h2", { className: "text-[16px] font-semibold text-text-primary", children: title }),
2733
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "mt-4 flex flex-col gap-1.5", children: [
2734
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("label", { htmlFor: "account-list-name", className: "text-[12px] font-medium text-text-secondary", children: "List name" }),
2735
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2736
+ "input",
2737
+ {
2738
+ id: "account-list-name",
2739
+ autoFocus: true,
2740
+ value: name,
2741
+ onChange: (e) => setName(e.target.value),
2742
+ placeholder: "e.g. Q4 prospecting targets",
2743
+ className: "h-10 rounded-control border border-border-default bg-input px-3 text-sm outline-hidden focus:border-brand-primaryAction"
2744
+ }
2745
+ )
2746
+ ] }),
2747
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "mt-5 flex items-center justify-end gap-2", children: [
2748
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Button, { variant: "secondary", onClick: onCancel, children: "Cancel" }),
2749
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Button, { disabled: !name.trim(), onClick: () => onSubmit(name.trim()), children: submitLabel })
2750
+ ] })
2751
+ ]
2752
+ }
2753
+ )
2754
+ }
2755
+ ),
2756
+ document.body
2757
+ );
2758
+ }
2759
+
2760
+ // src/ChipTrigger.tsx
2761
+ var import_jsx_runtime27 = require("react/jsx-runtime");
2762
+ function chipTriggerClass() {
2763
+ return cn(
2764
+ "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",
2765
+ // Sits on the input fill, with a stroke dark enough to still read as a
2766
+ // control against the warm canvas.
2767
+ "border-border-strong"
2768
+ );
2769
+ }
2770
+ function ChipContent({ caption, value, icon }) {
2771
+ const stack = /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { className: "flex flex-col gap-1 text-left leading-none", children: [
2772
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-text-tertiary", children: caption }),
2773
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "text-[12px] font-medium text-text-primary", children: value })
2774
+ ] });
2775
+ if (icon) {
2776
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { className: "flex items-center gap-2", children: [
2777
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "shrink-0 text-text-tertiary", children: icon }),
2778
+ stack
2779
+ ] });
2780
+ }
2781
+ return stack;
2782
+ }
2783
+
2784
+ // src/FilterChipBar.tsx
2785
+ var import_lucide_react16 = require("lucide-react");
2786
+ var import_jsx_runtime28 = require("react/jsx-runtime");
2787
+ function FilterChipBar({ chips, onRemove, onClearAll, parkedKeys }) {
2788
+ if (chips.length === 0) return null;
2789
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex flex-wrap items-center gap-1.5 border-t border-border-default pt-2.5", children: [
2790
+ chips.map((c) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
2791
+ Chip,
2792
+ {
2793
+ label: c.label,
2794
+ groupLabel: c.groupLabel,
2795
+ parked: !!parkedKeys?.has(c.key),
2796
+ onRemove: () => onRemove(c)
2797
+ },
2798
+ c.key
2799
+ )),
2800
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("button", { type: "button", onClick: onClearAll, className: "ml-1 text-[12px] font-medium text-text-link hover:underline", children: "Clear all" })
2801
+ ] });
2802
+ }
2803
+ function Chip({
2804
+ label,
2805
+ groupLabel,
2806
+ parked,
2807
+ onRemove
2808
+ }) {
2809
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { className: "group relative inline-flex", children: [
2810
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
2811
+ "span",
2812
+ {
2813
+ className: cn(
2814
+ "flex items-center gap-1 rounded-full px-2.5 py-1 text-[12px] font-medium ring-1 transition-colors",
2815
+ parked ? (
2816
+ // No fill, dashed edge, muted label: reads as "held, not acting on
2817
+ // anything" against the solid applied chips beside it.
2818
+ "border border-dashed border-border-default bg-transparent text-text-tertiary ring-transparent group-hover:text-text-secondary"
2819
+ ) : "bg-subtle text-text-secondary ring-transparent group-hover:bg-border-default group-hover:text-text-primary group-hover:ring-border-interactive"
2820
+ ),
2821
+ children: [
2822
+ parked && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_lucide_react16.CircleSlash, { size: 11, "aria-hidden": "true", className: "shrink-0 text-text-tertiary" }),
2823
+ label,
2824
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
2825
+ "button",
2826
+ {
2827
+ type: "button",
2828
+ onClick: onRemove,
2829
+ "aria-label": `Remove ${groupLabel}: ${label}`,
2830
+ className: "text-text-tertiary transition-colors hover:text-text-primary",
2831
+ children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_lucide_react16.X, { size: 12 })
2832
+ }
2833
+ )
2834
+ ]
2835
+ }
2836
+ ),
2837
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
2838
+ "span",
2839
+ {
2840
+ role: "tooltip",
2841
+ 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",
2842
+ children: parked ? "Not in this workspace" : groupLabel
2843
+ }
2844
+ )
2845
+ ] });
2846
+ }
2847
+
2848
+ // src/SortButton.tsx
2849
+ var import_lucide_react17 = require("lucide-react");
2850
+ var import_jsx_runtime29 = require("react/jsx-runtime");
2851
+ function SortButton({ label, sortKey, sort, onSort, disabled = false }) {
2852
+ const active = !disabled && sort.key === sortKey;
2853
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
2854
+ "button",
2855
+ {
2856
+ type: "button",
2857
+ disabled,
2858
+ onClick: () => onSort(sortKey),
2859
+ "aria-sort": active ? sort.dir === "asc" ? "ascending" : "descending" : "none",
2860
+ className: cn(
2861
+ // text-left overrides the UA default on <button>, which would centre a
2862
+ // label that wraps onto two lines, out of step with the column under it.
2863
+ "group inline-flex items-center gap-1 text-left uppercase tracking-wide transition-colors",
2864
+ !disabled && "hover:text-text-primary"
2865
+ ),
2866
+ children: [
2867
+ label,
2868
+ !disabled && (active ? sort.dir === "asc" ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_lucide_react17.ArrowUp, { size: 12, className: "shrink-0 text-brand-primaryAction" }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_lucide_react17.ArrowDown, { size: 12, className: "shrink-0 text-brand-primaryAction" }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
2869
+ import_lucide_react17.ArrowUpDown,
2870
+ {
2871
+ size: 12,
2872
+ className: "shrink-0 text-text-tertiary opacity-50 transition-opacity group-hover:opacity-100"
2873
+ }
2874
+ ))
2875
+ ]
2876
+ }
2877
+ );
2878
+ }
2879
+
2880
+ // src/tableSort.ts
2881
+ function cycleSort(current, key, firstDir = "desc") {
2882
+ if (!current || current.key !== key) return { key, dir: firstDir };
2883
+ if (current.dir === firstDir) return { key, dir: firstDir === "desc" ? "asc" : "desc" };
2884
+ return null;
2885
+ }
2886
+ function sortProp(sort) {
2887
+ return sort ?? { key: null, dir: "desc" };
2888
+ }
2889
+
2890
+ // src/Pager.tsx
2891
+ var import_lucide_react18 = require("lucide-react");
2892
+ var import_jsx_runtime30 = require("react/jsx-runtime");
2893
+ function pageWindow(current, total) {
2894
+ if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
2895
+ const keep = /* @__PURE__ */ new Set([1, total, current, current - 1, current + 1]);
2896
+ const pages = [...keep].filter((p) => p >= 1 && p <= total).sort((a, b) => a - b);
2897
+ const out = [];
2898
+ let prev = 0;
2899
+ for (const p of pages) {
2900
+ if (p - prev > 1) out.push(`gap-${p}`);
2901
+ out.push(p);
2902
+ prev = p;
2903
+ }
2904
+ return out;
2905
+ }
2906
+ function Pager({ page, totalPages, onPage }) {
2907
+ const btn = "flex h-8 min-w-8 items-center justify-center rounded-control px-2 text-[13px] transition-colors";
2908
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex items-center gap-1", children: [
2909
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
2910
+ "button",
2911
+ {
2912
+ type: "button",
2913
+ onClick: () => onPage(page - 1),
2914
+ disabled: page === 1,
2915
+ "aria-label": "Previous page",
2916
+ className: cn(btn, "text-text-secondary hover:bg-subtle hover:text-text-primary disabled:pointer-events-none disabled:opacity-30"),
2917
+ children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_lucide_react18.ChevronLeft, { size: 16 })
2918
+ }
2919
+ ),
2920
+ pageWindow(page, totalPages).map(
2921
+ (p) => typeof p === "string" ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { className: "px-1 text-[13px] text-text-tertiary", children: "\u2026" }, p) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
2922
+ "button",
2923
+ {
2924
+ type: "button",
2925
+ onClick: () => onPage(p),
2926
+ "aria-current": p === page ? "page" : void 0,
2927
+ className: cn(
2928
+ btn,
2929
+ p === page ? "bg-brand-primaryAction font-semibold text-brand-onprimary" : "text-text-secondary hover:bg-subtle hover:text-text-primary"
2930
+ ),
2931
+ children: p
2932
+ },
2933
+ p
2934
+ )
2935
+ ),
2936
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
2937
+ "button",
2938
+ {
2939
+ type: "button",
2940
+ onClick: () => onPage(page + 1),
2941
+ disabled: page === totalPages,
2942
+ "aria-label": "Next page",
2943
+ className: cn(btn, "text-text-secondary hover:bg-subtle hover:text-text-primary disabled:pointer-events-none disabled:opacity-30"),
2944
+ children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_lucide_react18.ChevronRight, { size: 16 })
2945
+ }
2946
+ )
2947
+ ] });
2948
+ }
2949
+
2950
+ // src/WorkspaceAvatar.tsx
2951
+ var import_jsx_runtime31 = require("react/jsx-runtime");
2952
+ function workspaceInitials(name) {
2953
+ const words = name.trim().split(/\s+/).filter(Boolean);
2954
+ if (words.length === 0) return "\xB7";
2955
+ if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
2956
+ return (words[0][0] + words[1][0]).toUpperCase();
2957
+ }
2958
+ var SIZES = {
2959
+ /** Matches a 15px lucide icon exactly, so a workspace name in a menu row
2960
+ * left-aligns with labels in icon-led rows above and below it. */
2961
+ xs: "h-[15px] w-[15px] rounded-[4px] text-[8px] tracking-tight",
2962
+ sm: "h-6 w-6 text-[10px]",
2963
+ md: "h-7 w-7 text-[11px]",
2964
+ lg: "h-11 w-11 text-[15px]"
2965
+ };
2966
+ function WorkspaceAvatar({ name, size = "md" }) {
2967
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
2968
+ "span",
2969
+ {
2970
+ "aria-hidden": "true",
2971
+ className: cn(
2972
+ "flex shrink-0 items-center justify-center rounded-control bg-subtle font-semibold leading-none text-text-secondary",
2973
+ SIZES[size]
2974
+ ),
2975
+ children: workspaceInitials(name ?? "All data")
2976
+ }
2977
+ );
2978
+ }
2608
2979
  // Annotate the CommonJS export names for ESM import in node:
2609
2980
  0 && (module.exports = {
2610
2981
  AdvertiserNoResults,
2611
2982
  AdvertiserQuickFilter,
2612
2983
  AdvertiserSearchPrompt,
2613
2984
  BarChartPlaceholder,
2985
+ Button,
2614
2986
  Card,
2615
2987
  CheckBox,
2988
+ ChipContent,
2616
2989
  DEFAULT_SEARCH_MATCH,
2617
2990
  DataScopeFilter,
2618
2991
  DateRangeFilter,
2992
+ FilterChipBar,
2993
+ FilterDropdownFooter,
2619
2994
  InfoTip,
2620
2995
  LineChartPlaceholder,
2621
2996
  NO_RESULTS_BODY,
2622
2997
  NO_RESULTS_TITLE,
2998
+ NameListModal,
2623
2999
  NoResultsEmptyState,
2624
3000
  PRIMARY_ACTION_CLASS,
3001
+ Pager,
2625
3002
  PieChartPlaceholder,
2626
3003
  PublisherBreakdownList,
2627
3004
  QUIET_ACTION_CLASS,
@@ -2631,9 +3008,15 @@ function PublisherBreakdownList({ rows, defaultVisibleCount = 10, padded = true,
2631
3008
  SelectFilter,
2632
3009
  SelectedSection,
2633
3010
  SelectionPicker,
3011
+ SortButton,
2634
3012
  StackedColumnPlaceholder,
2635
3013
  Tooltip,
3014
+ WorkspaceAvatar,
2636
3015
  WorkspaceSelectedCard,
2637
- noResultsBody
3016
+ buttonVariants,
3017
+ chipTriggerClass,
3018
+ cycleSort,
3019
+ noResultsBody,
3020
+ sortProp
2638
3021
  });
2639
3022
  //# sourceMappingURL=index.cjs.map