@mediaradar/design-components 0.8.1 → 0.10.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
@@ -390,9 +390,39 @@ function OptionRow({
390
390
 
391
391
  // src/CheckBox.tsx
392
392
  import { Check as Check2, Minus } from "lucide-react";
393
+
394
+ // src/cn.ts
395
+ import { clsx } from "clsx";
396
+ import { twMerge } from "tailwind-merge";
397
+ function cn(...inputs) {
398
+ return twMerge(clsx(inputs));
399
+ }
400
+
401
+ // src/CheckBox.tsx
393
402
  import { jsx as jsx4 } from "react/jsx-runtime";
394
- function CheckBox({ checked, indeterminate, onChange, ariaLabel, disabled, testId, title }) {
395
- const on = checked || indeterminate;
403
+ function boxClass(on, disabled) {
404
+ return cn(
405
+ "flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-[5px] border transition-colors",
406
+ on ? "border-transparent bg-brand-primaryAction text-white" : "border-border-interactive bg-input",
407
+ disabled && "cursor-not-allowed opacity-50"
408
+ );
409
+ }
410
+ function Glyph({ checked, indeterminate }) {
411
+ if (indeterminate) return /* @__PURE__ */ jsx4(Minus, { size: 13, strokeWidth: 3 });
412
+ return checked ? /* @__PURE__ */ jsx4(Check2, { size: 13, strokeWidth: 3 }) : null;
413
+ }
414
+ function CheckBox({
415
+ checked,
416
+ indeterminate,
417
+ onChange,
418
+ ariaLabel,
419
+ disabled,
420
+ stopPropagation = true,
421
+ testId,
422
+ title,
423
+ className
424
+ }) {
425
+ const on = checked || !!indeterminate;
396
426
  return /* @__PURE__ */ jsx4(
397
427
  "button",
398
428
  {
@@ -404,25 +434,26 @@ function CheckBox({ checked, indeterminate, onChange, ariaLabel, disabled, testI
404
434
  disabled,
405
435
  "data-testid": testId,
406
436
  onClick: (e) => {
407
- e.stopPropagation();
437
+ if (stopPropagation) e.stopPropagation();
408
438
  onChange();
409
439
  },
410
- className: [
411
- "flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-[5px] border transition-colors",
440
+ className: cn(
441
+ boxClass(on, disabled),
412
442
  "focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-border-focus",
413
- on ? "border-transparent bg-brand-primaryAction text-white" : "border-border-interactive bg-input hover:border-brand-primaryAction",
443
+ !on && "hover:border-brand-primaryAction",
414
444
  // Cancels the unchecked hover-border preview when disabled -- without
415
445
  // this, a disabled box still visually previews the "you can click
416
446
  // this" affordance on hover even though the click does nothing.
417
- // Found empirically reconciling this against insights-studio-
418
- // prototype's own (more complete) local CheckBox during its
419
- // migration -- that version already had this guard.
420
- disabled && "cursor-not-allowed opacity-50 hover:border-border-interactive"
421
- ].filter(Boolean).join(" "),
422
- children: indeterminate ? /* @__PURE__ */ jsx4(Minus, { size: 13, strokeWidth: 3 }) : checked ? /* @__PURE__ */ jsx4(Check2, { size: 13, strokeWidth: 3 }) : null
447
+ disabled && "hover:border-border-interactive",
448
+ className
449
+ ),
450
+ children: /* @__PURE__ */ jsx4(Glyph, { checked, indeterminate })
423
451
  }
424
452
  );
425
453
  }
454
+ function CheckBoxBox({ checked, indeterminate, disabled, className }) {
455
+ return /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", className: cn(boxClass(checked || !!indeterminate, disabled), className), children: /* @__PURE__ */ jsx4(Glyph, { checked, indeterminate }) });
456
+ }
426
457
 
427
458
  // src/DateRangeFilter.tsx
428
459
  import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState3 } from "react";
@@ -2558,15 +2589,6 @@ import { forwardRef as forwardRef2 } from "react";
2558
2589
  import { Slot } from "@radix-ui/react-slot";
2559
2590
  import { cva } from "class-variance-authority";
2560
2591
  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
2592
  import { jsx as jsx24, jsxs as jsxs22 } from "react/jsx-runtime";
2571
2593
  var buttonVariants = cva(
2572
2594
  "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",
@@ -2640,8 +2662,21 @@ function FilterDropdownFooter({
2640
2662
  import { useEffect as useEffect10, useState as useState11 } from "react";
2641
2663
  import { createPortal as createPortal5 } from "react-dom";
2642
2664
  import { jsx as jsx26, jsxs as jsxs24 } from "react/jsx-runtime";
2643
- function NameListModal({ title, initialName, submitLabel, onCancel, onSubmit }) {
2665
+ function NameListModal({
2666
+ title,
2667
+ initialName,
2668
+ submitLabel,
2669
+ onCancel,
2670
+ onSubmit,
2671
+ fieldLabel = "List name",
2672
+ placeholder = "e.g. Q4 prospecting targets",
2673
+ options
2674
+ }) {
2644
2675
  const [name, setName] = useState11(initialName);
2676
+ const [checked, setChecked] = useState11(
2677
+ () => Object.fromEntries((options ?? []).map((o) => [o.key, !!o.initial]))
2678
+ );
2679
+ const toggle = (key) => setChecked((c) => ({ ...c, [key]: !c[key] }));
2645
2680
  useEffect10(() => {
2646
2681
  const onKey = (e) => e.key === "Escape" && onCancel();
2647
2682
  window.addEventListener("keydown", onKey);
@@ -2666,7 +2701,7 @@ function NameListModal({ title, initialName, submitLabel, onCancel, onSubmit })
2666
2701
  children: [
2667
2702
  /* @__PURE__ */ jsx26("h2", { className: "text-[16px] font-semibold text-text-primary", children: title }),
2668
2703
  /* @__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" }),
2704
+ /* @__PURE__ */ jsx26("label", { htmlFor: "account-list-name", className: "text-[12px] font-medium text-text-secondary", children: fieldLabel }),
2670
2705
  /* @__PURE__ */ jsx26(
2671
2706
  "input",
2672
2707
  {
@@ -2674,14 +2709,18 @@ function NameListModal({ title, initialName, submitLabel, onCancel, onSubmit })
2674
2709
  autoFocus: true,
2675
2710
  value: name,
2676
2711
  onChange: (e) => setName(e.target.value),
2677
- placeholder: "e.g. Q4 prospecting targets",
2712
+ placeholder,
2678
2713
  className: "h-10 rounded-control border border-border-default bg-input px-3 text-sm outline-hidden focus:border-brand-primaryAction"
2679
2714
  }
2680
2715
  )
2681
2716
  ] }),
2717
+ options && options.length > 0 && /* @__PURE__ */ jsx26("div", { className: "mt-4 flex flex-col gap-2.5", children: options.map((o) => /* @__PURE__ */ jsxs24("label", { className: "flex cursor-pointer items-center gap-2.5 text-[13px] text-text-primary", children: [
2718
+ /* @__PURE__ */ jsx26(CheckBox, { checked: !!checked[o.key], onChange: () => toggle(o.key), ariaLabel: o.label }),
2719
+ o.label
2720
+ ] }, o.key)) }),
2682
2721
  /* @__PURE__ */ jsxs24("div", { className: "mt-5 flex items-center justify-end gap-2", children: [
2683
2722
  /* @__PURE__ */ jsx26(Button, { variant: "secondary", onClick: onCancel, children: "Cancel" }),
2684
- /* @__PURE__ */ jsx26(Button, { disabled: !name.trim(), onClick: () => onSubmit(name.trim()), children: submitLabel })
2723
+ /* @__PURE__ */ jsx26(Button, { disabled: !name.trim(), onClick: () => onSubmit(name.trim(), checked), children: submitLabel })
2685
2724
  ] })
2686
2725
  ]
2687
2726
  }
@@ -2694,12 +2733,13 @@ function NameListModal({ title, initialName, submitLabel, onCancel, onSubmit })
2694
2733
 
2695
2734
  // src/ChipTrigger.tsx
2696
2735
  import { jsx as jsx27, jsxs as jsxs25 } from "react/jsx-runtime";
2697
- function chipTriggerClass() {
2736
+ function chipTriggerClass(variant = "box") {
2698
2737
  return cn(
2699
2738
  "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"
2739
+ // Both variants sit on the input fill. `inline` carries the darker stroke
2740
+ // so it still reads as a control against the warm canvas with no boxed
2741
+ // shell behind it.
2742
+ variant === "inline" ? "border-border-strong" : "border-border-default"
2703
2743
  );
2704
2744
  }
2705
2745
  function ChipContent({ caption, value, icon }) {
@@ -3052,6 +3092,7 @@ import { useEffect as useEffect11, useMemo as useMemo4, useRef as useRef11, useS
3052
3092
  import { ChevronDown as ChevronDown10 } from "lucide-react";
3053
3093
  import { jsx as jsx34, jsxs as jsxs30 } from "react/jsx-runtime";
3054
3094
  var MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
3095
+ var QUARTER_ABBR = ["Q1", "Q2", "Q3", "Q4"];
3055
3096
  var PRESETS = ["Rolling 3 months", "Rolling 6 months", "Rolling 12 months"];
3056
3097
  var startOfToday = () => {
3057
3098
  const d = /* @__PURE__ */ new Date();
@@ -3100,6 +3141,7 @@ function MiniSelect({ value, onChange, options }) {
3100
3141
  }
3101
3142
  function DateRangePicker({
3102
3143
  defaultValue = "Rolling 6 months",
3144
+ granularity = "month",
3103
3145
  align = "left",
3104
3146
  presets = PRESETS,
3105
3147
  onChange,
@@ -3118,6 +3160,8 @@ function DateRangePicker({
3118
3160
  const [fromYr, setFromYr] = useState12(end.getFullYear() - 1);
3119
3161
  const [toMo, setToMo] = useState12(end.getMonth());
3120
3162
  const [toYr, setToYr] = useState12(end.getFullYear());
3163
+ const [fromQ, setFromQ] = useState12(Math.floor(end.getMonth() / 3));
3164
+ const [toQ, setToQ] = useState12(Math.floor(end.getMonth() / 3));
3121
3165
  useEffect11(() => {
3122
3166
  const [s, e] = resolveRange(value, end);
3123
3167
  onRangeChange?.(s, e);
@@ -3144,11 +3188,11 @@ function DateRangePicker({
3144
3188
  onChange?.(label);
3145
3189
  setOpen(false);
3146
3190
  };
3147
- const span = toYr * 12 + toMo - (fromYr * 12 + fromMo) + 1;
3191
+ const span = granularity === "year" ? toYr - fromYr + 1 : granularity === "quarter" ? toYr * 4 + toQ - (fromYr * 4 + fromQ) + 1 : toYr * 12 + toMo - (fromYr * 12 + fromMo) + 1;
3148
3192
  const valid = span >= 1;
3149
3193
  const applyCustom = () => {
3150
3194
  if (!valid) return;
3151
- const label = `${MONTH_ABBR[fromMo]} ${fromYr} \u2013 ${MONTH_ABBR[toMo]} ${toYr}`;
3195
+ const label = granularity === "year" ? `${fromYr} \u2013 ${toYr}` : granularity === "quarter" ? `${QUARTER_ABBR[fromQ]} ${fromYr} \u2013 ${QUARTER_ABBR[toQ]} ${toYr}` : `${MONTH_ABBR[fromMo]} ${fromYr} \u2013 ${MONTH_ABBR[toMo]} ${toYr}`;
3152
3196
  if (showFooter) setDraft(label);
3153
3197
  else commit(label);
3154
3198
  };
@@ -3187,14 +3231,16 @@ function DateRangePicker({
3187
3231
  /* @__PURE__ */ jsxs30("label", { className: "flex items-center justify-between gap-2 text-[12px] text-text-secondary", children: [
3188
3232
  "From",
3189
3233
  /* @__PURE__ */ jsxs30("span", { className: "flex gap-1", children: [
3190
- /* @__PURE__ */ jsx34(MiniSelect, { value: fromMo, onChange: setFromMo, options: MONTH_ABBR.map((mm, i) => ({ label: mm, value: i })) }),
3234
+ granularity === "month" && /* @__PURE__ */ jsx34(MiniSelect, { value: fromMo, onChange: setFromMo, options: MONTH_ABBR.map((mm, i) => ({ label: mm, value: i })) }),
3235
+ granularity === "quarter" && /* @__PURE__ */ jsx34(MiniSelect, { value: fromQ, onChange: setFromQ, options: QUARTER_ABBR.map((qq, i) => ({ label: qq, value: i })) }),
3191
3236
  /* @__PURE__ */ jsx34(MiniSelect, { value: fromYr, onChange: setFromYr, options: yearOptions.map((y) => ({ label: String(y), value: y })) })
3192
3237
  ] })
3193
3238
  ] }),
3194
3239
  /* @__PURE__ */ jsxs30("label", { className: "flex items-center justify-between gap-2 text-[12px] text-text-secondary", children: [
3195
3240
  "To",
3196
3241
  /* @__PURE__ */ jsxs30("span", { className: "flex gap-1", children: [
3197
- /* @__PURE__ */ jsx34(MiniSelect, { value: toMo, onChange: setToMo, options: MONTH_ABBR.map((mm, i) => ({ label: mm, value: i })) }),
3242
+ granularity === "month" && /* @__PURE__ */ jsx34(MiniSelect, { value: toMo, onChange: setToMo, options: MONTH_ABBR.map((mm, i) => ({ label: mm, value: i })) }),
3243
+ granularity === "quarter" && /* @__PURE__ */ jsx34(MiniSelect, { value: toQ, onChange: setToQ, options: QUARTER_ABBR.map((qq, i) => ({ label: qq, value: i })) }),
3198
3244
  /* @__PURE__ */ jsx34(MiniSelect, { value: toYr, onChange: setToYr, options: yearOptions.map((y) => ({ label: String(y), value: y })) })
3199
3245
  ] })
3200
3246
  ] }),
@@ -3223,6 +3269,8 @@ function MultiSelectFilter({
3223
3269
  caption,
3224
3270
  options = [],
3225
3271
  groups,
3272
+ counts,
3273
+ variant = "box",
3226
3274
  selected,
3227
3275
  onChange,
3228
3276
  allLabel = "All",
@@ -3280,14 +3328,18 @@ function MultiSelectFilter({
3280
3328
  return next;
3281
3329
  });
3282
3330
  const toggleGroup = (g) => setDraft((cur) => {
3283
- const ids = groupLeaves(g).map((c) => c.id);
3331
+ const ids = groupLeaves(g).map((c) => c.id).filter((id) => !counts || (counts[id] ?? 0) > 0 || cur.includes(id));
3332
+ if (!ids.length) return cur;
3284
3333
  const allSel = ids.every((id) => cur.includes(id));
3285
3334
  const next = allSel ? cur.filter((id) => !ids.includes(id)) : [.../* @__PURE__ */ new Set([...cur, ...ids])];
3286
3335
  if (next.length < min) return cur;
3287
3336
  return next;
3288
3337
  });
3338
+ const isAvailable = (id) => !counts || (counts[id] ?? 0) > 0 || draft.includes(id);
3339
+ const countFor = (id) => counts?.[id] ?? 0;
3289
3340
  const groupState = (g) => {
3290
- const ids = groupLeaves(g).map((c) => c.id);
3341
+ const ids = groupLeaves(g).map((c) => c.id).filter(isAvailable);
3342
+ if (!ids.length) return "none";
3291
3343
  const sel = ids.filter((id) => draft.includes(id)).length;
3292
3344
  return sel === 0 ? "none" : sel === ids.length ? "all" : "some";
3293
3345
  };
@@ -3295,10 +3347,12 @@ function MultiSelectFilter({
3295
3347
  const isAll = shown.length === 0 || fullIsAll && shown.length === leafOptions.length;
3296
3348
  const value = isAll ? allLabel : shown.length === 1 ? leafOptions.find((o) => o.id === shown[0])?.label ?? `${shown.length} selected` : `${shown.length} selected`;
3297
3349
  const ql = q.trim().toLowerCase();
3298
- const items = options.filter((o) => !ql || o.label.toLowerCase().includes(ql));
3350
+ const items = options.filter((o) => isAvailable(o.id) && (!ql || o.label.toLowerCase().includes(ql)));
3299
3351
  const visibleGroups = groups ? groups.map((g) => {
3300
- if (!ql || g.label.toLowerCase().includes(ql)) return g;
3301
- const kids = g.children.filter((c) => c.label.toLowerCase().includes(ql));
3352
+ const live = g.children.filter((c) => isAvailable(c.id));
3353
+ if (!live.length && !isAvailable(g.id)) return null;
3354
+ if (!ql || g.label.toLowerCase().includes(ql)) return { ...g, children: live };
3355
+ const kids = live.filter((c) => c.label.toLowerCase().includes(ql));
3302
3356
  return kids.length ? { ...g, children: kids } : null;
3303
3357
  }).filter((g) => g !== null) : [];
3304
3358
  return /* @__PURE__ */ jsxs31("div", { className: cn("relative", fullWidth && "w-full"), ref, children: [
@@ -3308,7 +3362,7 @@ function MultiSelectFilter({
3308
3362
  type: "button",
3309
3363
  onClick: () => open ? close() : openTray(),
3310
3364
  "aria-expanded": open,
3311
- className: cn(chipTriggerClass(), fullWidth && "w-full justify-between"),
3365
+ className: cn(chipTriggerClass(variant), fullWidth && "w-full justify-between"),
3312
3366
  children: [
3313
3367
  /* @__PURE__ */ jsx35(ChipContent, { caption, value, icon }),
3314
3368
  /* @__PURE__ */ jsx35(
@@ -3359,7 +3413,8 @@ function MultiSelectFilter({
3359
3413
  className: "flex w-full items-center gap-2.5 rounded-control px-2 py-2 text-left transition-colors hover:bg-subtle",
3360
3414
  children: [
3361
3415
  /* @__PURE__ */ jsx35(TriCheck, { state }),
3362
- /* @__PURE__ */ jsx35("span", { className: cn("flex-1 text-[14px] text-text-primary", !plainParent && "font-semibold"), children: g.label })
3416
+ /* @__PURE__ */ jsx35("span", { className: cn("flex-1 text-[14px] text-text-primary", !plainParent && "font-semibold"), children: g.label }),
3417
+ counts && /* @__PURE__ */ jsx35(OptionCount, { n: countFor(g.id) })
3363
3418
  ]
3364
3419
  }
3365
3420
  ),
@@ -3373,7 +3428,8 @@ function MultiSelectFilter({
3373
3428
  className: "flex w-full items-center gap-2.5 rounded-control py-2 pl-9 pr-2 text-left transition-colors hover:bg-subtle",
3374
3429
  children: [
3375
3430
  /* @__PURE__ */ jsx35(TriCheck, { state: checked ? "all" : "none" }),
3376
- /* @__PURE__ */ jsx35("span", { className: "flex-1 text-[14px] text-text-primary", children: c.label })
3431
+ /* @__PURE__ */ jsx35("span", { className: "flex-1 text-[14px] text-text-primary", children: c.label }),
3432
+ counts && /* @__PURE__ */ jsx35(OptionCount, { n: countFor(c.id) })
3377
3433
  ]
3378
3434
  },
3379
3435
  c.id
@@ -3399,7 +3455,8 @@ function MultiSelectFilter({
3399
3455
  children: checked && /* @__PURE__ */ jsx35(Check7, { size: 13, strokeWidth: 3 })
3400
3456
  }
3401
3457
  ),
3402
- /* @__PURE__ */ jsx35("span", { className: "flex-1 text-[14px] text-text-primary", children: o.label })
3458
+ /* @__PURE__ */ jsx35("span", { className: "flex-1 text-[14px] text-text-primary", children: o.label }),
3459
+ counts && /* @__PURE__ */ jsx35(OptionCount, { n: countFor(o.id) })
3403
3460
  ]
3404
3461
  },
3405
3462
  o.id
@@ -3440,6 +3497,13 @@ function TriCheck({ state }) {
3440
3497
  }
3441
3498
  );
3442
3499
  }
3500
+ function OptionCount({ n }) {
3501
+ return /* @__PURE__ */ jsxs31("span", { className: "shrink-0 text-[12px] tabular-nums text-text-tertiary", children: [
3502
+ "(",
3503
+ n.toLocaleString(),
3504
+ ")"
3505
+ ] });
3506
+ }
3443
3507
 
3444
3508
  // src/Toggle.tsx
3445
3509
  import { jsx as jsx36 } from "react/jsx-runtime";
@@ -3525,6 +3589,7 @@ export {
3525
3589
  Button,
3526
3590
  Card,
3527
3591
  CheckBox,
3592
+ CheckBoxBox,
3528
3593
  ChipContent,
3529
3594
  DEFAULT_SEARCH_MATCH,
3530
3595
  DataScopeFilter,