@mediaradar/design-components 0.9.0 → 0.11.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
@@ -2662,21 +2662,34 @@ function FilterDropdownFooter({
2662
2662
  import { useEffect as useEffect10, useState as useState11 } from "react";
2663
2663
  import { createPortal as createPortal5 } from "react-dom";
2664
2664
  import { jsx as jsx26, jsxs as jsxs24 } from "react/jsx-runtime";
2665
- 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
+ }) {
2666
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] }));
2667
2680
  useEffect10(() => {
2668
2681
  const onKey = (e) => e.key === "Escape" && onCancel();
2669
2682
  window.addEventListener("keydown", onKey);
2670
2683
  return () => window.removeEventListener("keydown", onKey);
2671
2684
  }, [onCancel]);
2672
2685
  return createPortal5(
2673
- // The scrim is a literal: @mediaradar/design-tokens has no scrim token yet.
2674
- // Same light/dark values as insights-studio-prototype's local --color-scrim;
2675
- // the dark one keys off data-theme, the attribute design-tokens' dark.css uses.
2686
+ // bg-scrim, not a literal: design-tokens gained a scrim token in 2.5.0.
2687
+ // No in-data-[theme=dark] override needed any more -- the token redefines
2688
+ // itself under that attribute in dark.css, so one class covers both themes.
2676
2689
  /* @__PURE__ */ jsx26(
2677
2690
  "div",
2678
2691
  {
2679
- 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",
2692
+ className: "fixed inset-0 z-70 flex items-center justify-center bg-scrim px-4",
2680
2693
  onMouseDown: (e) => e.target === e.currentTarget && onCancel(),
2681
2694
  children: /* @__PURE__ */ jsxs24(
2682
2695
  "div",
@@ -2688,7 +2701,7 @@ function NameListModal({ title, initialName, submitLabel, onCancel, onSubmit })
2688
2701
  children: [
2689
2702
  /* @__PURE__ */ jsx26("h2", { className: "text-[16px] font-semibold text-text-primary", children: title }),
2690
2703
  /* @__PURE__ */ jsxs24("div", { className: "mt-4 flex flex-col gap-1.5", children: [
2691
- /* @__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 }),
2692
2705
  /* @__PURE__ */ jsx26(
2693
2706
  "input",
2694
2707
  {
@@ -2696,14 +2709,18 @@ function NameListModal({ title, initialName, submitLabel, onCancel, onSubmit })
2696
2709
  autoFocus: true,
2697
2710
  value: name,
2698
2711
  onChange: (e) => setName(e.target.value),
2699
- placeholder: "e.g. Q4 prospecting targets",
2712
+ placeholder,
2700
2713
  className: "h-10 rounded-control border border-border-default bg-input px-3 text-sm outline-hidden focus:border-brand-primaryAction"
2701
2714
  }
2702
2715
  )
2703
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)) }),
2704
2721
  /* @__PURE__ */ jsxs24("div", { className: "mt-5 flex items-center justify-end gap-2", children: [
2705
2722
  /* @__PURE__ */ jsx26(Button, { variant: "secondary", onClick: onCancel, children: "Cancel" }),
2706
- /* @__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 })
2707
2724
  ] })
2708
2725
  ]
2709
2726
  }
@@ -3075,6 +3092,7 @@ import { useEffect as useEffect11, useMemo as useMemo4, useRef as useRef11, useS
3075
3092
  import { ChevronDown as ChevronDown10 } from "lucide-react";
3076
3093
  import { jsx as jsx34, jsxs as jsxs30 } from "react/jsx-runtime";
3077
3094
  var MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
3095
+ var QUARTER_ABBR = ["Q1", "Q2", "Q3", "Q4"];
3078
3096
  var PRESETS = ["Rolling 3 months", "Rolling 6 months", "Rolling 12 months"];
3079
3097
  var startOfToday = () => {
3080
3098
  const d = /* @__PURE__ */ new Date();
@@ -3123,6 +3141,7 @@ function MiniSelect({ value, onChange, options }) {
3123
3141
  }
3124
3142
  function DateRangePicker({
3125
3143
  defaultValue = "Rolling 6 months",
3144
+ granularity = "month",
3126
3145
  align = "left",
3127
3146
  presets = PRESETS,
3128
3147
  onChange,
@@ -3141,6 +3160,8 @@ function DateRangePicker({
3141
3160
  const [fromYr, setFromYr] = useState12(end.getFullYear() - 1);
3142
3161
  const [toMo, setToMo] = useState12(end.getMonth());
3143
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));
3144
3165
  useEffect11(() => {
3145
3166
  const [s, e] = resolveRange(value, end);
3146
3167
  onRangeChange?.(s, e);
@@ -3167,11 +3188,11 @@ function DateRangePicker({
3167
3188
  onChange?.(label);
3168
3189
  setOpen(false);
3169
3190
  };
3170
- 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;
3171
3192
  const valid = span >= 1;
3172
3193
  const applyCustom = () => {
3173
3194
  if (!valid) return;
3174
- 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}`;
3175
3196
  if (showFooter) setDraft(label);
3176
3197
  else commit(label);
3177
3198
  };
@@ -3210,14 +3231,16 @@ function DateRangePicker({
3210
3231
  /* @__PURE__ */ jsxs30("label", { className: "flex items-center justify-between gap-2 text-[12px] text-text-secondary", children: [
3211
3232
  "From",
3212
3233
  /* @__PURE__ */ jsxs30("span", { className: "flex gap-1", children: [
3213
- /* @__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 })) }),
3214
3236
  /* @__PURE__ */ jsx34(MiniSelect, { value: fromYr, onChange: setFromYr, options: yearOptions.map((y) => ({ label: String(y), value: y })) })
3215
3237
  ] })
3216
3238
  ] }),
3217
3239
  /* @__PURE__ */ jsxs30("label", { className: "flex items-center justify-between gap-2 text-[12px] text-text-secondary", children: [
3218
3240
  "To",
3219
3241
  /* @__PURE__ */ jsxs30("span", { className: "flex gap-1", children: [
3220
- /* @__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 })) }),
3221
3244
  /* @__PURE__ */ jsx34(MiniSelect, { value: toYr, onChange: setToYr, options: yearOptions.map((y) => ({ label: String(y), value: y })) })
3222
3245
  ] })
3223
3246
  ] }),
@@ -3246,6 +3269,8 @@ function MultiSelectFilter({
3246
3269
  caption,
3247
3270
  options = [],
3248
3271
  groups,
3272
+ counts,
3273
+ variant = "box",
3249
3274
  selected,
3250
3275
  onChange,
3251
3276
  allLabel = "All",
@@ -3303,14 +3328,18 @@ function MultiSelectFilter({
3303
3328
  return next;
3304
3329
  });
3305
3330
  const toggleGroup = (g) => setDraft((cur) => {
3306
- 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;
3307
3333
  const allSel = ids.every((id) => cur.includes(id));
3308
3334
  const next = allSel ? cur.filter((id) => !ids.includes(id)) : [.../* @__PURE__ */ new Set([...cur, ...ids])];
3309
3335
  if (next.length < min) return cur;
3310
3336
  return next;
3311
3337
  });
3338
+ const isAvailable = (id) => !counts || (counts[id] ?? 0) > 0 || draft.includes(id);
3339
+ const countFor = (id) => counts?.[id] ?? 0;
3312
3340
  const groupState = (g) => {
3313
- 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";
3314
3343
  const sel = ids.filter((id) => draft.includes(id)).length;
3315
3344
  return sel === 0 ? "none" : sel === ids.length ? "all" : "some";
3316
3345
  };
@@ -3318,10 +3347,12 @@ function MultiSelectFilter({
3318
3347
  const isAll = shown.length === 0 || fullIsAll && shown.length === leafOptions.length;
3319
3348
  const value = isAll ? allLabel : shown.length === 1 ? leafOptions.find((o) => o.id === shown[0])?.label ?? `${shown.length} selected` : `${shown.length} selected`;
3320
3349
  const ql = q.trim().toLowerCase();
3321
- 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)));
3322
3351
  const visibleGroups = groups ? groups.map((g) => {
3323
- if (!ql || g.label.toLowerCase().includes(ql)) return g;
3324
- 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));
3325
3356
  return kids.length ? { ...g, children: kids } : null;
3326
3357
  }).filter((g) => g !== null) : [];
3327
3358
  return /* @__PURE__ */ jsxs31("div", { className: cn("relative", fullWidth && "w-full"), ref, children: [
@@ -3331,7 +3362,7 @@ function MultiSelectFilter({
3331
3362
  type: "button",
3332
3363
  onClick: () => open ? close() : openTray(),
3333
3364
  "aria-expanded": open,
3334
- className: cn(chipTriggerClass(), fullWidth && "w-full justify-between"),
3365
+ className: cn(chipTriggerClass(variant), fullWidth && "w-full justify-between"),
3335
3366
  children: [
3336
3367
  /* @__PURE__ */ jsx35(ChipContent, { caption, value, icon }),
3337
3368
  /* @__PURE__ */ jsx35(
@@ -3382,7 +3413,8 @@ function MultiSelectFilter({
3382
3413
  className: "flex w-full items-center gap-2.5 rounded-control px-2 py-2 text-left transition-colors hover:bg-subtle",
3383
3414
  children: [
3384
3415
  /* @__PURE__ */ jsx35(TriCheck, { state }),
3385
- /* @__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) })
3386
3418
  ]
3387
3419
  }
3388
3420
  ),
@@ -3396,7 +3428,8 @@ function MultiSelectFilter({
3396
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",
3397
3429
  children: [
3398
3430
  /* @__PURE__ */ jsx35(TriCheck, { state: checked ? "all" : "none" }),
3399
- /* @__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) })
3400
3433
  ]
3401
3434
  },
3402
3435
  c.id
@@ -3422,7 +3455,8 @@ function MultiSelectFilter({
3422
3455
  children: checked && /* @__PURE__ */ jsx35(Check7, { size: 13, strokeWidth: 3 })
3423
3456
  }
3424
3457
  ),
3425
- /* @__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) })
3426
3460
  ]
3427
3461
  },
3428
3462
  o.id
@@ -3463,6 +3497,13 @@ function TriCheck({ state }) {
3463
3497
  }
3464
3498
  );
3465
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
+ }
3466
3507
 
3467
3508
  // src/Toggle.tsx
3468
3509
  import { jsx as jsx36 } from "react/jsx-runtime";