@mediaradar/design-components 0.1.0 → 0.5.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.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as react from 'react';
2
+ import { ReactNode, RefObject } from 'react';
2
3
 
3
4
  /**
4
5
  * @pattern InfoTip
@@ -35,4 +36,1146 @@ interface InfoTipProps {
35
36
  }
36
37
  declare function InfoTip({ text, size, label, trigger }: InfoTipProps): react.JSX.Element;
37
38
 
38
- export { InfoTip, type InfoTipProps };
39
+ /**
40
+ * @pattern AdvertiserQuickFilter
41
+ * @description Search-first quick filter. Mirrors the standard filter-chip
42
+ * shape (two-line caption + value + chevron) but never preloads the option
43
+ * list -- users must search first, since the advertiser pool is effectively
44
+ * endless. Selections are pinned in a "Selected (n)" section at the top so
45
+ * they're never lost when the user runs another search. Named for its
46
+ * original (and still primary) use, but the copy props make it reusable for
47
+ * any dimension that's also search-first in its own filter tray, so the
48
+ * dimension reads the same in the quick bar as it does there.
49
+ * @tokens rounded-control, rounded-card, border-border-default, border-border-strong,
50
+ * border-border-interactive, border-border-focus, bg-surface, bg-input,
51
+ * shadow-overlay, text-text-primary, text-text-secondary, text-text-tertiary,
52
+ * text-text-link, bg-brand-primaryAction, text-brand-onprimary,
53
+ * hover:bg-subtle, hover:text-text-primary, placeholder:text-text-tertiary
54
+ * @dependencies lucide-react
55
+ * @behavior The search input auto-focuses the moment the tray opens.
56
+ * Selections buffer in a local draft while open. Without `showFooter`,
57
+ * closing by any means (outside click, Escape, re-click) commits the draft
58
+ * to `onChange`. With `showFooter`, only the Apply button commits -- every
59
+ * other close path discards the draft, so the trigger label never previews
60
+ * an unapplied change. The pinned "selected" section persists across
61
+ * searches and collapses when the search is cleared.
62
+ * @usage
63
+ * ```tsx
64
+ * <AdvertiserQuickFilter options={advertisers} selected={selected} onChange={setSelected} />
65
+ * <AdvertiserQuickFilter options={advertisers} selected={selected} onChange={setSelected} showFooter clearValue={defaults} />
66
+ * ```
67
+ */
68
+ interface AdvertiserQuickFilterProps {
69
+ /** Small uppercase label above the value. */
70
+ caption?: string;
71
+ options: {
72
+ id: string;
73
+ label: string;
74
+ }[];
75
+ selected: string[];
76
+ onChange: (next: string[]) => void;
77
+ align?: "left" | "right";
78
+ /** Optional leading icon rendered before the caption/value stack. */
79
+ icon?: React.ReactNode;
80
+ /**
81
+ * Deferred mode: checkbox edits stage in a draft; only the fixed
82
+ * Apply/Clear All footer commits or discards them (no live commit on
83
+ * close). Defaults to false -- live commit-on-close -- so an existing
84
+ * caller's behavior never changes just by picking up a new version of
85
+ * this component.
86
+ */
87
+ showFooter?: boolean;
88
+ /** What "Clear all" resets the draft to in footer mode (the page's
89
+ * default selection). */
90
+ clearValue?: string[];
91
+ /** Selected ids the active workspace can't resolve. They stay in the
92
+ * selection -- still committed back, still removable -- but they narrow
93
+ * nothing, so the trigger label leaves them out of the count. */
94
+ parked?: string[];
95
+ /** Search box placeholder. Override alongside the other copy props when
96
+ * the filter isn't advertisers, so the empty and no-results states name
97
+ * the right dimension. */
98
+ searchPlaceholder?: string;
99
+ /** Completes "Start typing … to filter." in the pre-search prompt. */
100
+ promptLabel?: string;
101
+ /** The actionable next step under a no-results message. */
102
+ noResultsHint?: string;
103
+ /** The clear link in footerless mode (unused when `showFooter` is set). */
104
+ clearLabel?: string;
105
+ /**
106
+ * Optional prefix for `data-testid` attributes on the trigger, popover,
107
+ * search input, selected-section toggle, select-all-visible row, each
108
+ * option row (`${testIdPrefix}-option-<slugified-id>`), and the
109
+ * Apply/Clear All footer. Omit entirely for no test-id wiring at all --
110
+ * added specifically so an existing Playwright suite keyed off these ids
111
+ * (mr-clientapps' `AdvertisersPicker`) doesn't lose its locators when
112
+ * swapped over to this shared component.
113
+ */
114
+ testIdPrefix?: string;
115
+ }
116
+ declare function AdvertiserQuickFilter({ caption, options, selected, onChange, align, icon, showFooter, clearValue, parked, searchPlaceholder, promptLabel, noResultsHint, clearLabel, testIdPrefix, }: AdvertiserQuickFilterProps): react.JSX.Element;
117
+
118
+ /**
119
+ * @pattern AdvertiserSearchPrompt
120
+ * @description Pre-search empty state for a search-first picker — shown the
121
+ * moment the tray opens, before anything has been typed. Used for option
122
+ * pools too large to preload (the advertiser list is effectively endless),
123
+ * where the caller must search before any options can be listed at all.
124
+ * Named for its original (and still primary) use, but the `label` prop makes
125
+ * it reusable for any other search-first dimension (a league picker, say).
126
+ * @tokens bg-subtle, text-text-tertiary
127
+ * @dependencies lucide-react
128
+ * @usage
129
+ * ```tsx
130
+ * <AdvertiserSearchPrompt />
131
+ * <AdvertiserSearchPrompt label="a league name" />
132
+ * ```
133
+ */
134
+ interface AdvertiserSearchPromptProps {
135
+ /** Completes "Start typing … to filter." */
136
+ label?: string;
137
+ }
138
+ declare function AdvertiserSearchPrompt({ label }: AdvertiserSearchPromptProps): react.JSX.Element;
139
+
140
+ /**
141
+ * @pattern CheckBox
142
+ * @description Tri-state checkbox rendered as a `<button>` (not a native
143
+ * `<input type="checkbox">`), so it can carry an indeterminate "some but not
144
+ * all" state without the native element's limited styling hooks. Designed to
145
+ * be dropped into a row that already has its own click-to-toggle handler
146
+ * (a common list/table pattern) without double-firing.
147
+ * @tokens bg-brand-primaryAction, border-border-interactive, bg-input,
148
+ * hover:border-brand-primaryAction, focus-visible:outline-border-focus
149
+ * @dependencies lucide-react
150
+ * @behavior `aria-checked` is `"mixed"` when indeterminate, otherwise the
151
+ * plain boolean. Indeterminate renders a dash (Minus) instead of a
152
+ * checkmark; checked renders a checkmark; neither renders nothing. The
153
+ * unchecked state previews the brand action color on hover, matching the
154
+ * color the box fills with once checked -- the hover state is a preview of
155
+ * the click's effect, not an unrelated highlight. `onClick` calls
156
+ * `stopPropagation` before firing `onChange`, so nesting this inside a row
157
+ * that toggles on its own click can't fire the toggle twice.
158
+ * @usage
159
+ * ```tsx
160
+ * <CheckBox checked={selected} ariaLabel="Select row" onChange={toggle} />
161
+ * <CheckBox checked={allSelected} indeterminate={someSelected} ariaLabel="Select all" onChange={toggleAll} />
162
+ * <CheckBox checked={false} disabled ariaLabel="Select row" title="Max 5 rows -- remove one first" onChange={toggle} />
163
+ * ```
164
+ */
165
+ interface CheckBoxProps {
166
+ checked: boolean;
167
+ /**
168
+ * Some-but-not-all state for a "select all" checkbox -- shows a dash
169
+ * instead of a check, and reports `aria-checked="mixed"` instead of the
170
+ * plain boolean.
171
+ */
172
+ indeterminate?: boolean;
173
+ onChange: () => void;
174
+ /** Accessible label -- required since the control carries no visible text of its own. */
175
+ ariaLabel: string;
176
+ disabled?: boolean;
177
+ /** Forwarded as `data-testid`, for Playwright/test selectors. */
178
+ testId?: string;
179
+ /**
180
+ * Native tooltip -- e.g. explaining why a disabled box is disabled. Forwarded
181
+ * directly to the underlying `<button>`, same as a plain HTML `title` would
182
+ * be on any other disabled control.
183
+ */
184
+ title?: string;
185
+ }
186
+ declare function CheckBox({ checked, indeterminate, onChange, ariaLabel, disabled, testId, title }: CheckBoxProps): react.JSX.Element;
187
+
188
+ /**
189
+ * @pattern DateRangeFilter
190
+ * @description Date-range control -- the two-line chip (caption + value) with
191
+ * a chevron that opens a two-pane picker: preset shortcuts on the left, a
192
+ * dual-month calendar with range highlighting on the right, Start/End fields,
193
+ * and Cancel/Apply. Shared by Creative Intel and the Account Review.
194
+ * @tokens rounded-control, rounded-card, border-border-default, border-border-strong,
195
+ * bg-surface, bg-input, bg-subtle, bg-ai-tint, bg-brand-primaryAction,
196
+ * shadow-overlay, text-text-primary, text-text-secondary, text-text-tertiary,
197
+ * hover:bg-subtle, hover:text-text-primary, focus-visible:outline-border-focus
198
+ * @dependencies lucide-react
199
+ * @behavior Escape and outside-click both close the popup. Opening always
200
+ * resets the draft (shortcut/start/end/visible month) to the last committed
201
+ * values, so a previous unsaved edit never lingers into the next open. Only
202
+ * the Apply button commits -- Cancel, Escape, and outside-click all discard
203
+ * the draft silently.
204
+ * @usage
205
+ * ```tsx
206
+ * <DateRangeFilter caption="Date Range" defaultValue="Rolling 6 months" onChange={setRange} />
207
+ * <DateRangeFilter presetSet="creativeIntel" defaultValue="Last 7 days" onChange={setRange} />
208
+ * <DateRangeFilter presetSet="creativeIntel" value={preset} range={[start, end]} onChange={setPreset} onRangeChange={(s, e) => setRange({ start: s, end: e })} />
209
+ * ```
210
+ */
211
+ interface DateRangeFilterProps {
212
+ /** Static label shown above the value when `showDates` is false. */
213
+ caption?: string;
214
+ /** Initial preset (matched case-insensitively against the active preset set) or, if unmatched, treated as a custom label. Ignored once `value`/`range` are given -- see those. */
215
+ defaultValue?: string;
216
+ /**
217
+ * Controlled mode: the parent owns the committed preset/range instead of
218
+ * this component tracking it internally. Give both `value` and `range`
219
+ * together to enable it -- omit both (the default) for the original
220
+ * uncontrolled behavior, where `defaultValue` seeds the initial state and
221
+ * `onChange`/`onRangeChange` are fire-and-forget notifications. In
222
+ * controlled mode the chip's displayed value/dates always reflect these
223
+ * props directly, so the parent must call `onChange`/`onRangeChange` and
224
+ * feed the result back for the chip to update after Apply -- the same
225
+ * contract as a controlled `<input value onChange>`.
226
+ */
227
+ value?: string;
228
+ /** Controlled mode's resolved range -- see `value`. */
229
+ range?: [Date, Date];
230
+ /** Which edge of the trigger the popup aligns to. */
231
+ align?: "left" | "right";
232
+ /** Fired on Apply with the chip's new display value. */
233
+ onChange?: (value: string) => void;
234
+ /** Fired on Apply (and once on mount, uncontrolled only) with the resolved start/end dates -- e.g. to drive dependent table columns. */
235
+ onRangeChange?: (start: Date, end: Date) => void;
236
+ /** Show the resolved start-end dates in the chip instead of the preset name. */
237
+ showDates?: boolean;
238
+ /** Use an abbreviated date format (e.g. "Dec 1, 2025 - Jun 24, 2026") for the dates line. */
239
+ shortDates?: boolean;
240
+ /** Optional leading icon rendered before the caption/value stack. */
241
+ icon?: React.ReactNode;
242
+ /** 'creativeIntel' swaps in the day-level shortcut list (Today/Yesterday/Last N days/All available history) used by that dashboard. */
243
+ presetSet?: "default" | "creativeIntel";
244
+ /** Prefix for data-testid attributes on the trigger, popover, each preset option, and Cancel/Apply. Omit for no test-id wiring. */
245
+ testIdPrefix?: string;
246
+ }
247
+ declare function DateRangeFilter({ caption, defaultValue, value: controlledValue, range: controlledRange, align, onChange, onRangeChange, showDates, shortDates, icon, presetSet, testIdPrefix, }: DateRangeFilterProps): react.JSX.Element;
248
+
249
+ /**
250
+ * @pattern SearchBar
251
+ * @description Filter-bar keyword search. 'default' is a plain keyword
252
+ * input; 'filterable' adds an attached match-type dropdown sharing one
253
+ * bordered control, mirroring the legacy Creative Intel search + match-type
254
+ * modal (Any Word/Exact Match/Contextual/Ad Code), including the Or/And
255
+ * sub-choice nested under Any Word.
256
+ * @tokens rounded-control, border-border-strong, bg-input, text-text-primary,
257
+ * text-text-tertiary, hover:bg-subtle, bg-brand-primaryAction, bg-subtle,
258
+ * bg-surface, border-brand-primaryAction, border-border-default,
259
+ * border-border-interactive, focus-visible:outline-border-focus,
260
+ * hover:text-text-primary, placeholder:text-text-tertiary, rounded-card,
261
+ * rounded-full, shadow-overlay, text-brand-primaryAction, text-text-secondary
262
+ * @dependencies lucide-react
263
+ * @behavior The match-type dropdown ('filterable' variant) closes on Escape
264
+ * and outside click. Each mode row is a real `radio` role, with a nested
265
+ * Or/And sub-choice under Any Word and its own click-to-toggle info tooltip.
266
+ * The text input carries an `aria-label` derived from `placeholder` — the
267
+ * wrapping `<label>` has no visible text content (just an icon and the
268
+ * input), so it grants the input no accessible name on its own.
269
+ * @usage
270
+ * ```tsx
271
+ * <SearchBar value={query} onChange={setQuery} />
272
+ * <SearchBar variant="filterable" value={query} onChange={setQuery} matchValue={matchValue} onMatchChange={setMatchValue} />
273
+ * ```
274
+ */
275
+ type SearchBarVariant = "default" | "filterable";
276
+ type SearchMatchMode = "anyWord" | "exactMatch" | "contextual" | "adCode";
277
+ interface SearchMatchValue {
278
+ mode: SearchMatchMode;
279
+ /** Only meaningful when mode === 'anyWord'. */
280
+ wordJoin: "Or" | "And";
281
+ }
282
+ declare const DEFAULT_SEARCH_MATCH: SearchMatchValue;
283
+ interface SearchBarProps {
284
+ /** Input placeholder text. Also used as the input's `aria-label`. */
285
+ placeholder?: string;
286
+ value?: string;
287
+ onChange?: (v: string) => void;
288
+ /** 'default' is a plain keyword input; 'filterable' adds the match-type dropdown. */
289
+ variant?: SearchBarVariant;
290
+ /** The 'filterable' variant's dropdown state. */
291
+ matchValue?: SearchMatchValue;
292
+ onMatchChange?: (v: SearchMatchValue) => void;
293
+ className?: string;
294
+ /**
295
+ * Optional prefix for `data-testid` attributes on the input, the
296
+ * match-type trigger/popover, each mode row, and the Or/And radio pair
297
+ * (e.g. `${testIdPrefix}-search`, `${testIdPrefix}-mode-trigger`,
298
+ * `${testIdPrefix}-mode-anyWord`, `${testIdPrefix}-word-join-or`). Omit
299
+ * entirely for no test-id wiring at all -- added specifically so an
300
+ * existing Playwright suite keyed off these ids (mr-clientapps'
301
+ * `KeywordSearchBar`) doesn't lose its locators when swapped over to
302
+ * this shared component.
303
+ */
304
+ testIdPrefix?: string;
305
+ }
306
+ declare function SearchBar({ placeholder, value, onChange, variant, matchValue, onMatchChange, className, testIdPrefix, }: SearchBarProps): react.JSX.Element;
307
+
308
+ /**
309
+ * @pattern SectionRail
310
+ * @description Generic titled section wrapper for a row of cards — a
311
+ * horizontal scroll rail on mobile/tablet (snap, bleeds to edges), a
312
+ * left-aligned 4-column grid on desktop. Card width is handled here, not on
313
+ * the card itself, so cards stay layout-agnostic; it works with any
314
+ * `children`. Real call sites vary: a fixed 4-item row that fills the
315
+ * desktop grid exactly, or a variable-length list with no "view all" link
316
+ * at all — both need to keep working without the row assuming a count.
317
+ * @tokens text-text-primary, text-text-secondary, text-text-link,
318
+ * rounded-control, rounded-sm, border-border, focus-visible:outline-solid,
319
+ * focus-visible:outline-border-focus, hover:bg-subtle, hover:text-text-primary,
320
+ * tracking-tight, font-serif
321
+ * @dependencies lucide-react
322
+ * @behavior Card width lives on the row, not the card: a fixed 268px per
323
+ * card in rail mode, capped at 320px per column in the lg+ grid, so cards
324
+ * stay layout-agnostic. The "view all" link only renders when both
325
+ * `viewAllLabel` and `viewAllHref` are given — either alone hides it, rather
326
+ * than falling back to a dead `href="#"`. Prev/next arrows are hidden below
327
+ * `sm` (swipe is the native affordance there) and at `lg`+ (the grid shows
328
+ * every card, nothing left to scroll to). `viewAllHref` renders as a plain
329
+ * anchor rather than a framework router Link — this package has no router
330
+ * peer dependency, and the two consuming apps disagree on one (react-router-dom
331
+ * vs next/link); a plain anchor is the one thing that works unmodified in both.
332
+ * @usage
333
+ * ```tsx
334
+ * <SectionRail title="Sports" subtitle="Your tracked leagues" viewAllLabel="View all" viewAllHref="/sports">
335
+ * {items.map((i) => <Card key={i.id}>{i.name}</Card>)}
336
+ * </SectionRail>
337
+ * ```
338
+ */
339
+ interface SectionRailProps {
340
+ /** Section heading. */
341
+ title: string;
342
+ /** Use the serif display treatment instead of the default sans heading. */
343
+ titleSerif?: boolean;
344
+ /** Supporting line under the title. */
345
+ subtitle: string;
346
+ /** Label for the "view all" link. Has no effect unless `viewAllHref` is also given. */
347
+ viewAllLabel?: string;
348
+ /** Route the "view all" link points to. Has no effect unless `viewAllLabel` is also given. */
349
+ viewAllHref?: string;
350
+ /** The row's cards. Any content works — width is handled by the row, not the card. */
351
+ children: React.ReactNode;
352
+ }
353
+ declare function SectionRail({ title, titleSerif, subtitle, viewAllLabel, viewAllHref, children, }: SectionRailProps): react.JSX.Element;
354
+
355
+ /**
356
+ * @pattern DataScopeFilter
357
+ * @description "Data" scope chip -- toggles the filter bar between All data
358
+ * and Final-only data (data that's finalized across the selected media
359
+ * formats, for stable report-ready numbers). One boolean, one control,
360
+ * shared across every dashboard that has this data-finality concept
361
+ * (Publisher Intel, Opportunities, Sports).
362
+ * @tokens rounded-control, border-border-strong, bg-input, hover:bg-subtle,
363
+ * hover:text-text-primary, focus-visible:outline-border-focus, rounded-card,
364
+ * border-border-default, bg-surface, shadow-overlay, text-text-primary,
365
+ * text-text-secondary, text-text-tertiary, text-brand-primaryAction
366
+ * @dependencies lucide-react
367
+ * @behavior Escape and outside-click both close the popover. The "Final
368
+ * only" option's info tooltip is a real InfoTip in hover mode, rendered as a
369
+ * sibling of the option button rather than nested inside it -- InfoTip
370
+ * renders its own `<button>`, and a button nested inside another button is
371
+ * invalid HTML that breaks click targeting (confirmed against how
372
+ * mr-clientapps-insights-studio's own equivalent, `ChipOption`, already
373
+ * avoids exactly this by rendering its InfoTip as a sibling, not a child).
374
+ * @usage
375
+ * ```tsx
376
+ * <DataScopeFilter value={finalOnly} onChange={setFinalOnly} />
377
+ * ```
378
+ */
379
+ interface DataScopeFilterProps {
380
+ value: boolean;
381
+ onChange: (v: boolean) => void;
382
+ /** Which edge of the trigger the popover aligns to. Both known consumers
383
+ * anchor this at the right edge of a filter bar, hence the default. */
384
+ align?: "left" | "right";
385
+ }
386
+ declare function DataScopeFilter({ value, onChange, align }: DataScopeFilterProps): react.JSX.Element;
387
+
388
+ /**
389
+ * @pattern SelectFilter
390
+ * @description Single-select filter chip -- the caption-over-value trigger
391
+ * shape shared with AdvertiserQuickFilter/DateRangeFilter, opening a
392
+ * lightweight listbox of plain string options. Portal-rendered to
393
+ * `document.body` so the popover is never clipped by a scrollable ancestor
394
+ * (e.g. inside a modal) -- unlike this package's other chip popovers, none
395
+ * of which are known to sit inside a clipping container today.
396
+ * @tokens rounded-control, rounded-card, border-border-default,
397
+ * border-border-strong, bg-input, bg-surface, shadow-overlay,
398
+ * text-text-primary, text-text-secondary,
399
+ * text-text-tertiary, hover:bg-subtle, hover:text-text-primary,
400
+ * text-brand-primaryAction, focus-visible:outline-border-focus
401
+ * @dependencies lucide-react
402
+ * @behavior The popover is portal-rendered to document.body, so its own DOM
403
+ * node isn't inside the trigger. The outside-click check tests both the
404
+ * trigger and the popover explicitly for that reason -- treating the
405
+ * popover as "outside" would close it on every option click before
406
+ * `onChange` ever fires, so nothing would ever get selected. Selecting an
407
+ * option commits immediately and closes -- there's no separate draft/Apply
408
+ * step; Escape and outside-click just close without side effects.
409
+ * @usage
410
+ * ```tsx
411
+ * <SelectFilter caption="Rank" value={rankFilter} onChange={setRankFilter} options={['All', 'VP / Director', 'Manager', 'Coordinator']} />
412
+ * <SelectFilter caption="Group by" value={groupBy} onChange={setGroupBy} options={['Parent', 'Advertiser', 'Brand']} fullWidth testIdPrefix="dropdown-group-by" />
413
+ * ```
414
+ */
415
+ interface SelectFilterProps {
416
+ caption: string;
417
+ value: string;
418
+ options: string[];
419
+ onChange: (v: string) => void;
420
+ /** Which edge of the trigger the popover aligns to. */
421
+ align?: "left" | "right";
422
+ /** Stretch the trigger (and the popover, when true) to fill its container. */
423
+ fullWidth?: boolean;
424
+ /** Extra classes merged onto the trigger button -- e.g. a background
425
+ * override for a filter bar that isn't sitting on a filter-tray canvas. */
426
+ className?: string;
427
+ /**
428
+ * Optional prefix for `data-testid` attributes on the trigger
429
+ * (`${testIdPrefix}-trigger`), the popover (`${testIdPrefix}-popover`),
430
+ * and each option (`${testIdPrefix}-option-<slugified-label>`). Omit
431
+ * entirely for no test-id wiring at all -- added specifically so an
432
+ * existing Playwright suite keyed off hardcoded ids (mr-clientapps'
433
+ * `GroupBySelect`, e.g. `dropdown-group-by-option-parent`) can be re-keyed
434
+ * to this shared component's ids rather than losing its locators outright.
435
+ */
436
+ testIdPrefix?: string;
437
+ }
438
+ declare function SelectFilter({ caption, value, options, onChange, align, fullWidth, className, testIdPrefix, }: SelectFilterProps): react.JSX.Element;
439
+
440
+ /**
441
+ * @pattern Card
442
+ * @description Layout primitive for grouping content in a bordered, elevated
443
+ * surface. Width is controlled by the parent container, not the Card itself,
444
+ * so it works unchanged as a fixed-width rail item or a grid cell. Applies an
445
+ * interactive hover treatment (deeper shadow) unless `staticCard` is set. An
446
+ * optional `title` (+ `subtitle`/`action`) renders a standard header row and
447
+ * switches the card into a padded content layout, replacing the header/body
448
+ * shell mr-clientapps-insights-studio's own `CardShell` used to hand-roll.
449
+ * @tokens rounded-card, border-border-default, bg-surface, shadow-raised,
450
+ * hover:shadow-overlay, p-card-padding, text-text-primary, text-text-tertiary
451
+ * @behavior Without `title`, children render directly as the card's only
452
+ * content -- no padding is applied, so callers that need a full-bleed image
453
+ * or gradient flush against the card edge (the common case in
454
+ * insights-studio-prototype's rail/grid cards) stay in control of their own
455
+ * inner layout, exactly as before this component grew a `title` prop. With
456
+ * `title`, the header and children are wrapped together in the card's
457
+ * standard padding, matching `CardShell`'s layout so it can be swapped in
458
+ * directly. `staticCard` drops the hover-shadow transition for non-clickable
459
+ * content (a KPI tile, a read-only summary) -- there's nothing to respond to.
460
+ * @variant interactive - Default. Clickable/hoverable content -- deeper
461
+ * shadow on hover signals it responds to the cursor.
462
+ * @variant static - Set `staticCard`. Non-clickable content -- no hover
463
+ * treatment.
464
+ * @usage
465
+ * ```tsx
466
+ * <Card>
467
+ * <h3>Title</h3>
468
+ * <p>Body content</p>
469
+ * </Card>
470
+ * <Card staticCard>Non-interactive content</Card>
471
+ * <Card title="Spend trend" subtitle="Last 90 days" action={<Link to="/spend">View all</Link>}>
472
+ * <SpendChart />
473
+ * </Card>
474
+ * ```
475
+ */
476
+ interface CardProps extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
477
+ /** Disable interactive hover treatment (when card isn't clickable). */
478
+ staticCard?: boolean;
479
+ /**
480
+ * Optional header title. Omit it (the default) to keep Card a bare,
481
+ * unpadded surface -- set it to get a title/subtitle/action header row
482
+ * plus the card's standard content padding, matching `CardShell`.
483
+ */
484
+ title?: string;
485
+ /** Shown under `title` in a muted tertiary color. Ignored without `title`. */
486
+ subtitle?: string;
487
+ /** Trailing header content, e.g. a "View all" link. Ignored without `title`. */
488
+ action?: ReactNode;
489
+ }
490
+ declare const Card: react.ForwardRefExoticComponent<CardProps & react.RefAttributes<HTMLElement>>;
491
+
492
+ /**
493
+ * The one "no results" message, shared across every filtered table/list that
494
+ * can end up empty for the same two reasons -- see the component doc below
495
+ * for why they're deliberately not distinguished.
496
+ */
497
+ declare const NO_RESULTS_TITLE = "No results match your filters";
498
+ declare const NO_RESULTS_BODY = "Your filters may be too narrow, conflict with each other, or have no activity in this period.";
499
+ /**
500
+ * The body for the active workspace: named while one is active, since its scope
501
+ * bounds the result as much as the filters do. Under "All data" (no name) the
502
+ * generic line stands alone.
503
+ */
504
+ declare function noResultsBody(workspaceName?: string | null): string;
505
+ /** One caller-supplied recovery action, rendered after the two named handlers. */
506
+ interface NoResultsAction {
507
+ label: string;
508
+ onClick: () => void;
509
+ /** Rendered as the filled button; every other action renders as the quiet link/button. */
510
+ primary?: boolean;
511
+ }
512
+ declare const PRIMARY_ACTION_CLASS = "rounded-control bg-brand-primaryAction px-3.5 py-2 text-[13px] font-semibold text-white transition-colors hover:opacity-90";
513
+ declare const QUIET_ACTION_CLASS = "rounded-control text-[13px] font-medium text-text-link transition-colors hover:underline focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-border-focus";
514
+ /**
515
+ * @pattern NoResultsEmptyState
516
+ * @description Guided empty state for a query that returned nothing.
517
+ * Deliberately one state for two causes, because the user can't tell them
518
+ * apart and the fix is the same either way -- loosen the filters or widen the
519
+ * date range: (1) a filter combination that can't match anything, or (2) a
520
+ * valid combination with no activity in the selected period. So the copy
521
+ * names both possibilities instead of asserting one. Renders as a bare
522
+ * stack -- no border, background, or fixed height -- so it can sit directly
523
+ * in a colSpan table cell (or a card, or a chart body) that already owns
524
+ * that chrome, without imposing its own.
525
+ * @tokens bg-subtle, text-text-tertiary, text-base, text-text-primary,
526
+ * text-text-secondary, rounded-control, bg-brand-primaryAction, text-text-link,
527
+ * focus-visible:outline-border-focus
528
+ * @dependencies lucide-react
529
+ * @behavior The Clear button shows when `onClearFilters` is given AND
530
+ * `hasFilters` isn't explicitly `false` -- so a caller can either pass the
531
+ * handler conditionally (only when there's something to clear) or always
532
+ * pass it and gate visibility with `hasFilters` separately; both existing
533
+ * call patterns work unchanged. The Edit button shows whenever
534
+ * `onEditFilters` is given. `actions` appends further caller-defined
535
+ * recovery buttons (e.g. "Switch workspace") after the two named ones, for
536
+ * empty states whose fix isn't about filters at all -- primary gets the
537
+ * filled treatment, everything else the quiet one.
538
+ * @usage
539
+ * ```tsx
540
+ * <NoResultsEmptyState hasFilters={hasFilters} onClearFilters={clearFilters} onEditFilters={openFilterTray} />
541
+ * <NoResultsEmptyState
542
+ * title="Properties not set up in this workspace"
543
+ * body="Switch to a workspace that has them defined or create a new one."
544
+ * actions={[{ label: 'Switch workspace', onClick: openSwitcher, primary: true }, { label: 'Create workspace', onClick: createNew }]}
545
+ * />
546
+ * ```
547
+ */
548
+ interface NoResultsEmptyStateProps {
549
+ /** @default NO_RESULTS_TITLE */
550
+ title?: string;
551
+ /** Replaces the default body wholesale, workspace name included. */
552
+ body?: string;
553
+ /** The active workspace's name, or nothing under "All data". Shapes the default body only -- ignored when `body` is given. */
554
+ workspaceName?: string | null;
555
+ /**
556
+ * Explicit override for whether the Clear button can show. Omit to derive
557
+ * it from whether `onClearFilters` was passed; pass `false` to force it
558
+ * hidden even though a handler was given (e.g. the handler exists but
559
+ * nothing is currently applied).
560
+ */
561
+ hasFilters?: boolean;
562
+ onClearFilters?: () => void;
563
+ onEditFilters?: () => void;
564
+ /** Extra recovery actions, rendered in order after Clear/Edit. */
565
+ actions?: NoResultsAction[];
566
+ /**
567
+ * Optional prefix for `data-testid` attributes on the container
568
+ * (`testIdPrefix` itself), the Clear button (`${testIdPrefix}-clear`), and
569
+ * the Edit button (`${testIdPrefix}-edit`). Omit for no test-id wiring.
570
+ * Pass `"no-results"` to reproduce mr-clientapps-insights-studio's current
571
+ * fixed ids (`no-results`, `no-results-clear`, `no-results-edit`) exactly.
572
+ */
573
+ testIdPrefix?: string;
574
+ }
575
+ declare function NoResultsEmptyState({ title, body, workspaceName, hasFilters, onClearFilters, onEditFilters, actions, testIdPrefix, }: NoResultsEmptyStateProps): react.JSX.Element;
576
+
577
+ interface SummaryGroupItemValue {
578
+ id: string;
579
+ name: string;
580
+ /**
581
+ * Secondary line under the name. Takes a node rather than a plain string
582
+ * so a caller can render something richer than static text there -- e.g.
583
+ * mr-clientapps-insights-studio's Account Lists group swaps this for a
584
+ * "No accounts assigned -- click here to add accounts" link when a list is
585
+ * empty, which a plain `string` field couldn't hold.
586
+ */
587
+ meta?: React.ReactNode;
588
+ }
589
+ interface SummaryGroup {
590
+ id: string;
591
+ label: string;
592
+ items: SummaryGroupItemValue[];
593
+ onRemove: (id: string) => void;
594
+ onRemoveAll: () => void;
595
+ /** Nested sub-sections rendered, indented, beneath this group's own items. */
596
+ subGroups?: SummaryGroup[];
597
+ }
598
+ /**
599
+ * @pattern WorkspaceSelectedCard
600
+ * @description Sticky recap card listing what each section's picker has
601
+ * added, as collapsible groups with per-item remove and "Remove all" --
602
+ * generic over the `SummaryGroup` shape, no domain coupling. Reconciled from
603
+ * two implementations: insights-studio-prototype's generic `SummaryGroup[]`
604
+ * (kept, since it cleanly covers mr-clientapps-insights-studio's own usage
605
+ * too -- that app's fixed `sets`/`accountLists` prop pair is just this same
606
+ * shape with the group list hardcoded at the call site instead of passed
607
+ * in) and mr-clientapps-insights-studio's `SelectedSummary`, whose
608
+ * `wasEmpty`-ref auto-expand tracking turned out to be the correct one (see
609
+ * `@behavior`) and whose `shadow-1` -- not a real `@mediaradar/design-tokens`
610
+ * class, confirmed against the built theme -- pointed at a real, deliberately
611
+ * omitted elevation this card was missing; the local `shadow-1`/`shadow-2`
612
+ * naming maps to this package's `shadow-raised`/`shadow-overlay` respectively
613
+ * (raised: a static card like this one; overlay: a floating popover/menu),
614
+ * not a 1:1 name swap.
615
+ * @tokens rounded-card, border-border, bg-surface, shadow-raised,
616
+ * text-text-secondary, text-text-primary, rounded-full, bg-subtle,
617
+ * rounded-control, bg-canvas, hover:bg-subtle, hover:text-text-danger,
618
+ * text-text-link, divide-border
619
+ * @dependencies lucide-react
620
+ * @behavior A group auto-expands only on the empty-to-nonempty transition
621
+ * (including via a nested sub-group's items) and auto-collapses when
622
+ * emptied; manual open/close toggles in between are preserved. This is
623
+ * mr-clientapps-insights-studio's `wasEmpty`-ref logic, not
624
+ * insights-studio-prototype's original `prevCount`-comparison one --
625
+ * verified the two differ: the original re-opened a group on ANY item
626
+ * increase, including one added to an already-open or already
627
+ * manually-collapsed nonempty group, silently undoing the user's own
628
+ * collapse. Sub-groups render indented beneath their parent's own items.
629
+ * @usage
630
+ * ```tsx
631
+ * <WorkspaceSelectedCard groups={[{ id: 'advertisers', label: 'Advertisers', items: selected, onRemove: removeOne, onRemoveAll: clearAll }]} />
632
+ * ```
633
+ */
634
+ interface WorkspaceSelectedCardProps {
635
+ groups: SummaryGroup[];
636
+ /**
637
+ * Optional prefix for `data-testid` attributes on each group's toggle
638
+ * (`${testIdPrefix}-group-<group.id>`) and its "Remove all" button
639
+ * (`${testIdPrefix}-remove-all-<group.id>`), applied recursively to
640
+ * sub-groups too. Omit for no test-id wiring.
641
+ *
642
+ * NOTE: this does not reproduce mr-clientapps-insights-studio's current
643
+ * fixed ids byte-for-byte -- its container carries a separate, unrelated
644
+ * `workspace-selected-summary` id (this component has no single container
645
+ * element those groups sit inside, by design, so there's nothing to attach
646
+ * an equivalent id to) while its groups use a *different* prefix,
647
+ * `workspace-summary-group-<type>` / `workspace-summary-remove-all-<type>`.
648
+ * A consumer with existing Playwright locators keyed off those exact
649
+ * strings needs to update them, or the caller needs its own id mapping --
650
+ * not attempted here, same as `DateRangeFilter`'s documented preset-slug
651
+ * mismatch.
652
+ */
653
+ testIdPrefix?: string;
654
+ }
655
+ declare function WorkspaceSelectedCard({ groups, testIdPrefix }: WorkspaceSelectedCardProps): react.JSX.Element;
656
+
657
+ /**
658
+ * @pattern BarChartPlaceholder
659
+ * @description Neutral, dataless stand-in for a ranked-bar chart --
660
+ * descending bar profile under a blank-labelled axis frame (dashed
661
+ * gridlines, blank y/x-tick boxes), so an unsatisfiable filter still reads
662
+ * as "a chart belongs here" rather than a failed load. No real value ever
663
+ * appears (no channel/league names, media types, dollar amounts,
664
+ * percentages, or series colors), and the shape holds the card's layout
665
+ * steady so nothing jumps when real data arrives.
666
+ * @tokens bg-subtle, border-border-default, rounded-xs
667
+ * @behavior Purely presentational -- `role="presentation"` and
668
+ * `aria-hidden="true"` throughout, since none of the boxes carry real
669
+ * content for assistive tech to announce. The bar count and profile are a
670
+ * fixed shape, not driven by `height`.
671
+ * @usage
672
+ * ```tsx
673
+ * <BarChartPlaceholder />
674
+ * <BarChartPlaceholder height={180} />
675
+ * ```
676
+ */
677
+ interface BarChartPlaceholderProps {
678
+ /** Chart height in pixels. */
679
+ height?: number;
680
+ }
681
+ declare function BarChartPlaceholder({ height }: BarChartPlaceholderProps): react.JSX.Element;
682
+
683
+ /**
684
+ * @pattern LineChartPlaceholder
685
+ * @description Neutral, dataless stand-in for a multi-series trend chart --
686
+ * four grey paths over the shared axis frame, each with a solid-then-dashed
687
+ * handoff for the live chart's forecast tail, and a point at every sample.
688
+ * Points are positioned rather than drawn in the SVG: the plot stretches to
689
+ * the card, and a stretched `<circle>` would go oval. Same no-results use
690
+ * as `BarChartPlaceholder` -- no real value ever appears, and the shape
691
+ * holds the card's layout steady so nothing jumps when real data arrives.
692
+ * @tokens border-border-strong, bg-subtle, border-border-default, rounded-xs
693
+ * @behavior Purely presentational -- `role="presentation"` and
694
+ * `aria-hidden="true"` throughout (inherited from `AxisFrame`). The series
695
+ * count and shapes are fixed; `height` is the only variable.
696
+ * @usage
697
+ * ```tsx
698
+ * <LineChartPlaceholder />
699
+ * <LineChartPlaceholder height={200} />
700
+ * ```
701
+ */
702
+ interface LineChartPlaceholderProps {
703
+ /** Chart height in pixels. */
704
+ height?: number;
705
+ }
706
+ declare function LineChartPlaceholder({ height }: LineChartPlaceholderProps): react.JSX.Element;
707
+
708
+ /**
709
+ * @pattern PieChartPlaceholder
710
+ * @description Neutral, dataless stand-in for a pie chart -- a grey disc
711
+ * seamed where slices would fall, beside a blank legend. No axis, matching
712
+ * the live pie it stands in for. Same no-results use as
713
+ * `BarChartPlaceholder`: an unsatisfiable filter reads as "a chart belongs
714
+ * here" rather than a failed load, with no real value (names, media types,
715
+ * dollar amounts, percentages, series colors) ever appearing.
716
+ * @tokens bg-subtle, border-border-default, rounded-xs
717
+ * @behavior Purely presentational -- `role="presentation"` and
718
+ * `aria-hidden="true"` throughout. `legendRows` controls the blank legend
719
+ * only; the disc's seam count is a fixed shape.
720
+ * @usage
721
+ * ```tsx
722
+ * <PieChartPlaceholder />
723
+ * <PieChartPlaceholder legendRows={4} />
724
+ * ```
725
+ */
726
+ interface PieChartPlaceholderProps {
727
+ /** Chart height in pixels. */
728
+ height?: number;
729
+ /** Number of blank legend rows to render beside the disc. */
730
+ legendRows?: number;
731
+ }
732
+ declare function PieChartPlaceholder({ height, legendRows }: PieChartPlaceholderProps): react.JSX.Element;
733
+
734
+ /**
735
+ * @pattern StackedColumnPlaceholder
736
+ * @description Neutral, dataless stand-in for a 100% stacked column -- a
737
+ * single banded column beside a blank legend. `axis` follows the live card
738
+ * it stands in for: a share-of-total chart plotting on a 0-100% axis passes
739
+ * `axis`, a bare stack with no scale (e.g. a share-of-voice column) omits
740
+ * it. Same no-results use as `BarChartPlaceholder` -- no real value ever
741
+ * appears, and the shape holds the card's layout steady.
742
+ * @tokens bg-subtle, border-border-strong, border-border-default, rounded-control
743
+ * @behavior Purely presentational -- `role="presentation"` and
744
+ * `aria-hidden="true"` throughout. Band seams use `border-border-strong`
745
+ * rather than the softer `border-border-default` gridline color: on a
746
+ * `bg-subtle` fill the default-strength seam disappears and the stack reads
747
+ * as one undivided block. Legend rows are reversed (bottom-up) when `axis`
748
+ * is off, matching a bare stack's usual bottom-anchored reading order.
749
+ * @usage
750
+ * ```tsx
751
+ * <StackedColumnPlaceholder />
752
+ * <StackedColumnPlaceholder axis />
753
+ * ```
754
+ */
755
+ interface StackedColumnPlaceholderProps {
756
+ /** Chart height in pixels. */
757
+ height?: number;
758
+ /** Column width in pixels. */
759
+ width?: number;
760
+ /** Number of bands (and matching blank legend rows). */
761
+ legendRows?: number;
762
+ /** Wrap the column in a 0-100% axis frame, matching a live share-of-total chart. */
763
+ axis?: boolean;
764
+ }
765
+ declare function StackedColumnPlaceholder({ height, width, legendRows, axis, }: StackedColumnPlaceholderProps): react.JSX.Element;
766
+
767
+ /**
768
+ * @pattern Tooltip
769
+ * @description Portalled hover/focus tooltip. Two ways to attach a trigger:
770
+ * wrap `children` (the default -- an inline trigger, e.g. a truncated table
771
+ * cell or a chart legend entry), or pass `anchorRef` for an element that
772
+ * already needs its own ref, or where wrapping in an extra inline box would
773
+ * disturb layout (an icon-only collapsed nav rail, for example) -- the two
774
+ * are mutually exclusive ways of specifying the trigger, and `children` is
775
+ * ignored when `anchorRef` is set. Positioned `fixed` from the trigger's
776
+ * bounding rect and rendered through a portal to `document.body`, so it
777
+ * escapes both `overflow` clipping inside scrollable panels and the broken
778
+ * `fixed` containing block a transformed ancestor creates.
779
+ * @tokens rounded-control, border-border-default, bg-surface, shadow-overlay,
780
+ * text-text-primary, bg-text-primary, text-surface
781
+ * @variant surface - Default. Themed surface/border bubble -- matches the
782
+ * Recharts chart tooltips this was originally built for.
783
+ * @variant inverted - Solid dark pill (no border) -- for a tooltip sitting
784
+ * over a nav rail or other icon-only surface, where the themed surface bubble
785
+ * doesn't read as clearly against a busy background.
786
+ * @behavior Shows on hover and on keyboard focus (so it stays reachable
787
+ * without a pointer) after `delay` ms (0 = immediate, the default). Hides
788
+ * immediately on mouseleave/blur, cancelling a pending delayed show if it
789
+ * hasn't fired yet -- except in `interactive` mode, which delays the hide
790
+ * briefly so the pointer can travel from the trigger into the tooltip's own
791
+ * content. Pass `whenTruncated` to only surface it once the trigger's content
792
+ * is actually clipped by ellipsis (`scrollWidth > clientWidth`). `placement`
793
+ * 'top'/'bottom' centre (or `align="start"` left-align) on the trigger;
794
+ * 'right' centres vertically beside the anchor and 'bottom-end' hangs below,
795
+ * right-aligned to the anchor's right edge -- both meant for `anchorRef`
796
+ * anchors near the viewport's right edge, where a centred box would run
797
+ * off-screen or read awkwardly beside a narrow icon.
798
+ * @usage
799
+ * ```tsx
800
+ * <Tooltip label="Full advertiser name" whenTruncated>
801
+ * <span className="truncate">{name}</span>
802
+ * </Tooltip>
803
+ *
804
+ * const ref = useRef<HTMLButtonElement>(null);
805
+ * <button ref={ref}>Sports</button>
806
+ * <Tooltip label="Sports" anchorRef={ref} placement="right" delay={500} variant="inverted" />
807
+ * ```
808
+ */
809
+ interface TooltipProps {
810
+ /** Tooltip content shown on hover/focus. */
811
+ label: ReactNode;
812
+ /** The trigger content, wrapped inline. Omit when using `anchorRef`. */
813
+ children?: ReactNode;
814
+ /**
815
+ * Attach to an existing element instead of wrapping `children` in a
816
+ * trigger span -- see the pattern description. When set, `children` /
817
+ * `className` / `whenTruncated` all still apply to that element (its
818
+ * `scrollWidth`/`clientWidth` drive `whenTruncated`), just without this
819
+ * component rendering a wrapping element of its own.
820
+ */
821
+ anchorRef?: RefObject<HTMLElement | null>;
822
+ /** Classes for the inline trigger wrapper (e.g. `truncate`, `min-w-0`). Ignored with `anchorRef`. */
823
+ className?: string;
824
+ /**
825
+ * Only reveal the tooltip when the trigger's content is actually clipped by
826
+ * ellipsis (`scrollWidth > clientWidth`). Off by default -- the tooltip always
827
+ * shows.
828
+ */
829
+ whenTruncated?: boolean;
830
+ /**
831
+ * When true, the tooltip accepts pointer events so it can contain links and
832
+ * other interactive content.
833
+ */
834
+ interactive?: boolean;
835
+ /**
836
+ * Which side of the trigger to render on. 'top' (default) / 'bottom'
837
+ * centre on the trigger (or left-align with `align="start"`). 'right' /
838
+ * 'bottom-end' are for an `anchorRef` anchor -- see the pattern description.
839
+ */
840
+ placement?: "top" | "bottom" | "right" | "bottom-end";
841
+ /**
842
+ * How the box lines up with the trigger horizontally, for 'top'/'bottom'
843
+ * placement only. "center" (the default) centres it; "start" puts its left
844
+ * edge on the trigger so it grows rightward, which reads better for a small
845
+ * icon sitting beside the text it describes -- a centred box there hangs
846
+ * out over the content to the left.
847
+ */
848
+ align?: "center" | "start";
849
+ /**
850
+ * Hover delay in ms before the tooltip appears. 0 (default) shows
851
+ * immediately. A collapsed nav rail passes 500 so a fast mouse pass over
852
+ * several icons doesn't flash a tooltip per icon.
853
+ */
854
+ delay?: number;
855
+ /** 'surface' (default) or 'inverted' -- see the pattern description. */
856
+ variant?: "surface" | "inverted";
857
+ /** Optional prefix for a `data-testid` on the floating tooltip bubble. */
858
+ testIdPrefix?: string;
859
+ }
860
+ declare function Tooltip({ label, children, anchorRef, className, whenTruncated, interactive, placement, align, delay, variant, testIdPrefix, }: TooltipProps): react.JSX.Element | null;
861
+
862
+ /**
863
+ * @pattern AdvertiserNoResults
864
+ * @description Shown when a search-first picker's search returns nothing --
865
+ * gives an actionable next step rather than just a bare "no results"
866
+ * message. Named for its original (and still primary) use across
867
+ * `insights-studio-prototype`'s three advertiser-search filter trays
868
+ * (Creative Intel, Publisher, Sports), but the `hint` prop makes it reusable
869
+ * for any other search-first dimension. Distinct from the more general
870
+ * `NoResultsEmptyState` (a whole-board "no results match your filters"
871
+ * state) -- this one is scoped to a single search box returning nothing,
872
+ * inline within a picker's option list.
873
+ * @tokens text-text-primary, text-text-tertiary
874
+ * @usage
875
+ * ```tsx
876
+ * <AdvertiserNoResults query={query} />
877
+ * <AdvertiserNoResults query={query} hint="Try a different spelling, or switch the Agency Type." />
878
+ * ```
879
+ */
880
+ interface AdvertiserNoResultsProps {
881
+ /** The search term that returned nothing. */
882
+ query: string;
883
+ /** The actionable next step under the message. */
884
+ hint?: string;
885
+ }
886
+ declare function AdvertiserNoResults({ query, hint, }: AdvertiserNoResultsProps): react.JSX.Element;
887
+
888
+ /**
889
+ * @pattern SelectAllVisible
890
+ * @description "Select all visible" row for a search-first picker's result
891
+ * list -- only render it once the caller decides the visible result set is
892
+ * long enough to warrant it (`insights-studio-prototype`'s three advertiser-
893
+ * search filter trays all gate it behind a 5-result threshold; mr-clientapps'
894
+ * `ScopedSearchSection` uses the identical threshold and copy for its own
895
+ * scoped search sections). Communicates how many are visible and toggles
896
+ * between "Select all" and "Deselect" based on `allSelected`.
897
+ * @tokens rounded-control, text-text-link, hover:bg-subtle
898
+ * @dependencies lucide-react
899
+ * @usage
900
+ * ```tsx
901
+ * {results.length > 5 && (
902
+ * <SelectAllVisible count={results.length} allSelected={allSelected} onToggle={toggleAll} />
903
+ * )}
904
+ * ```
905
+ */
906
+ interface SelectAllVisibleProps {
907
+ /** Number of currently visible results. */
908
+ count: number;
909
+ /** Whether every visible result is already selected. */
910
+ allSelected: boolean;
911
+ onToggle: () => void;
912
+ /** Optional prefix for a `data-testid` on the row. */
913
+ testIdPrefix?: string;
914
+ }
915
+ declare function SelectAllVisible({ count, allSelected, onToggle, testIdPrefix }: SelectAllVisibleProps): react.JSX.Element;
916
+
917
+ /**
918
+ * @pattern SelectedSection
919
+ * @description Collapsible "Selected (n)" container for a search-first
920
+ * picker. Selections can pile up and crowd out the live search results, so
921
+ * the user can fold them away to refocus on the search. Controlled so the
922
+ * parent can auto-collapse it (e.g. when the search is cleared, or jointly
923
+ * with a sibling "parked" section) -- `insights-studio-prototype`'s three
924
+ * advertiser-search filter trays all drive `open` this way. The caller
925
+ * renders each selected row itself as `children` rather than this component
926
+ * owning row markup: the three real call sites render genuinely different
927
+ * row content (plain rows, opacity-dimmed "parked" rows, per-tray custom
928
+ * columns), so a fixed row shape would have forced a compromise none of them
929
+ * need. Self-hides at `count === 0` so a caller can render it unconditionally.
930
+ * @tokens rounded-control, text-text-tertiary, hover:text-text-primary
931
+ * @dependencies lucide-react
932
+ * @usage
933
+ * ```tsx
934
+ * <SelectedSection count={selected.length} open={open} onToggle={() => setOpen((o) => !o)}>
935
+ * {selected.map((s) => <Row key={s.id} item={s} />)}
936
+ * </SelectedSection>
937
+ * ```
938
+ */
939
+ interface SelectedSectionProps {
940
+ count: number;
941
+ open: boolean;
942
+ onToggle: () => void;
943
+ /** The caller renders each selected row itself. */
944
+ children: React.ReactNode;
945
+ /** Optional prefix for a `data-testid` on the toggle button. */
946
+ testIdPrefix?: string;
947
+ }
948
+ declare function SelectedSection({ count, open, onToggle, children, testIdPrefix }: SelectedSectionProps): react.JSX.Element | null;
949
+
950
+ interface PickerRow {
951
+ id: string;
952
+ name: string;
953
+ children?: PickerRow[];
954
+ }
955
+ interface PickerColumn<T extends PickerRow> {
956
+ key: string;
957
+ header: string;
958
+ render: (row: T) => ReactNode;
959
+ className?: string;
960
+ }
961
+ interface PickerFilter<T extends PickerRow> {
962
+ label: string;
963
+ options: string[];
964
+ /** Returns the row's value for this filter (matched against options). */
965
+ value: (row: T) => string | undefined;
966
+ }
967
+ /** The subset of an account-list record this component actually renders in
968
+ * its "Account lists" tab -- a caller's own richer type (with members,
969
+ * createdBy, etc.) is passed in as-is; only these fields are read. */
970
+ interface SelectionPickerAccountList {
971
+ id: string;
972
+ name: string;
973
+ /** Number of accounts currently in the list. */
974
+ count: number;
975
+ /** Entity types represented in the list, for a quick descriptive line. */
976
+ entityTypes: string[];
977
+ updatedAt: string;
978
+ }
979
+ interface SelectionPickerProps<T extends PickerRow> {
980
+ /** Full catalog to search/filter over client-side. Ignored when `results`
981
+ * is given instead -- that's the async/paginated mode, see `results`. */
982
+ catalog?: T[];
983
+ selected: T[];
984
+ onChange: (next: T[]) => void;
985
+ columns: PickerColumn<T>[];
986
+ /** First filter is rendered as the primary entity-type group. */
987
+ filters: PickerFilter<T>[];
988
+ searchPlaceholder?: string;
989
+ /** Optional mock drill-down: returns child rows to reveal under a row. */
990
+ getChildren?: (row: T) => T[] | undefined;
991
+ helper?: string;
992
+ /** Trailing Info/drill-down column. Defaults to shown. */
993
+ showInfoColumn?: boolean;
994
+ /** Filter options pre-selected on mount, keyed by filter label. */
995
+ defaultFilters?: Record<string, string[]>;
996
+ /** When true, results appear only once the user types a query -- active
997
+ * filters alone don't reveal the table (used where a filter is pre-selected). */
998
+ requireQuery?: boolean;
999
+ /** When true, the full catalog shows immediately without a search -- for
1000
+ * short, browsable lists (e.g. US states). */
1001
+ showResultsWithoutSearch?: boolean;
1002
+ /** When provided, a "Search / Account lists" source toggle appears and the
1003
+ * Account lists tab lets the user pull in whole uploaded lists. */
1004
+ accountLists?: SelectionPickerAccountList[];
1005
+ selectedListIds?: string[];
1006
+ onToggleList?: (id: string) => void;
1007
+ /** Rows that are already committed elsewhere and can't be toggled here --
1008
+ * they render dimmed with no checkbox at all (an "Add" picker shouldn't
1009
+ * offer to un-add something it isn't responsible for). Select-all skips
1010
+ * them, so it can't strip them out from under the caller. */
1011
+ lockedIds?: ReadonlySet<string>;
1012
+ /**
1013
+ * Externally-computed, already-filtered rows -- for a caller whose search
1014
+ * is server-side/async (loading, pagination) instead of a full in-memory
1015
+ * `catalog` filtered client-side. When given, this replaces `catalog`'s
1016
+ * own search/filter matching entirely: the component renders exactly
1017
+ * these rows, still gated by the same "has the user searched or filtered
1018
+ * yet" reveal rule as catalog mode. Pair with `onQueryChange` so the
1019
+ * caller knows what to search for, and with
1020
+ * `loading`/`error`/`hasMore`/`onLoadMore` for the rest of a typical async
1021
+ * search's states. Omit (the default) to keep the original catalog +
1022
+ * client-side search/filter behavior -- an existing caller's behavior
1023
+ * never changes just by picking up a new version of this component.
1024
+ */
1025
+ results?: T[];
1026
+ /** Fires on every keystroke in the search box with the new query text --
1027
+ * for a caller in `results` mode that needs to know what to search for.
1028
+ * Fire-and-forget; this component still owns the input's value either way. */
1029
+ onQueryChange?: (query: string) => void;
1030
+ /** Shows a centered spinner in place of rows. Only meaningful in `results` mode. */
1031
+ loading?: boolean;
1032
+ /** Shows a "couldn't load" message (see `errorMessage`) in place of rows. */
1033
+ error?: boolean;
1034
+ /** @default "Couldn't load options. Please try again." */
1035
+ errorMessage?: string;
1036
+ /** Shows a "Load more" button below the results when another page is available. */
1037
+ hasMore?: boolean;
1038
+ /** Disables the Load more button and shows a small spinner in it while a page is already in flight. */
1039
+ loadingMore?: boolean;
1040
+ onLoadMore?: () => void;
1041
+ /** Extra toolbar content rendered after the built-in filter dropdowns --
1042
+ * e.g. a source/level multi-select the caller owns. A plain composition
1043
+ * slot; this component has no opinion on what it renders. */
1044
+ toolbarExtra?: ReactNode;
1045
+ /**
1046
+ * Optional prefix for `data-testid` attributes on the search input
1047
+ * (`${testIdPrefix}-search`), the source toggle buttons
1048
+ * (`${testIdPrefix}-source-search` / `-lists`), each filter dropdown
1049
+ * trigger (`${testIdPrefix}-filter-<slug>`) and its options, the
1050
+ * select-all checkbox (`${testIdPrefix}-select-all`), each row
1051
+ * (`${testIdPrefix}-row-<slug>`), the Load more button
1052
+ * (`${testIdPrefix}-load-more`), and each Account lists row/count
1053
+ * (`${testIdPrefix}-list-<id>` / `${testIdPrefix}-list-count`). Omit for
1054
+ * no test-id wiring.
1055
+ */
1056
+ testIdPrefix?: string;
1057
+ }
1058
+ /**
1059
+ * @pattern SelectionPicker
1060
+ * @description Generic search-and-filter picker for building a selection out
1061
+ * of a larger catalog -- a search box plus filter dropdowns above a checkbox
1062
+ * table, with an optional "Search / Account lists" source toggle for pulling
1063
+ * in whole pre-built lists at once. The table stays empty until the user
1064
+ * actually searches or filters (no pre-loaded catalog dump), and checking a
1065
+ * row commits immediately rather than staging into a separate "Add
1066
+ * selected" step. Supports two data modes: a full in-memory `catalog`
1067
+ * filtered client-side (the default), or externally-computed `results` for a
1068
+ * caller whose search is server-side/async, with `loading`/`error`/
1069
+ * `hasMore`/`onLoadMore` covering that mode's states.
1070
+ * @tokens rounded-control, rounded-card, border-border-default, bg-input,
1071
+ * bg-canvas, bg-surface, shadow-raised, shadow-overlay, text-text-primary,
1072
+ * text-text-secondary, text-text-tertiary, text-text-link,
1073
+ * text-brand-primaryAction, border-brand-primaryAction, bg-brand-primaryAction,
1074
+ * bg-brand-primarySoft, bg-subtle, rounded-full, hover:bg-subtle,
1075
+ * hover:bg-canvas, hover:text-text-primary, placeholder:text-text-tertiary,
1076
+ * cursor-default
1077
+ * @dependencies lucide-react, this package's own CheckBox
1078
+ * @behavior "Select all" is scoped to the current search/filter results (or
1079
+ * the currently-loaded page(s) of `results`), not the whole catalog, and
1080
+ * skips lockedIds throughout so it can't toggle rows it isn't responsible
1081
+ * for. Filter dropdowns are portal-rendered and close on outside-click,
1082
+ * Escape, or scroll. Long option lists (>8) get an in-dropdown search box.
1083
+ * Row and select-all checkboxes use this package's own `CheckBox`, wired
1084
+ * with both an `onChange` and the row's own `onClick` -- `CheckBox` stops
1085
+ * click propagation before firing `onChange`, which is exactly what lets it
1086
+ * sit inside a row that toggles on its own click without double-firing.
1087
+ * @usage
1088
+ * ```tsx
1089
+ * <SelectionPicker
1090
+ * catalog={advertisers}
1091
+ * selected={selected}
1092
+ * onChange={setSelected}
1093
+ * columns={[{ key: 'name', header: 'Advertiser', render: (r) => r.name }]}
1094
+ * filters={[{ label: 'Category', options: categories, value: (r) => r.category }]}
1095
+ * />
1096
+ * <SelectionPicker
1097
+ * catalog={[]}
1098
+ * results={searchResults}
1099
+ * onQueryChange={setQuery}
1100
+ * loading={status === 'loading'}
1101
+ * error={status === 'error'}
1102
+ * hasMore={hasMore}
1103
+ * loadingMore={loadingMore}
1104
+ * onLoadMore={loadMore}
1105
+ * selected={selected}
1106
+ * onChange={setSelected}
1107
+ * columns={columns}
1108
+ * filters={[]}
1109
+ * toolbarExtra={<LevelMultiSelect checked={levels} onToggle={toggleLevel} options={levelOptions} />}
1110
+ * />
1111
+ * ```
1112
+ */
1113
+ declare function SelectionPicker<T extends PickerRow>({ catalog, selected, onChange, columns, filters, searchPlaceholder, getChildren, helper, showInfoColumn, defaultFilters, requireQuery, showResultsWithoutSearch, accountLists, selectedListIds, onToggleList, lockedIds, results, onQueryChange, loading, error, errorMessage, hasMore, loadingMore, onLoadMore, toolbarExtra, testIdPrefix, }: SelectionPickerProps<T>): react.JSX.Element;
1114
+
1115
+ interface PubBreak {
1116
+ brand: string;
1117
+ owner: string;
1118
+ tags: string[];
1119
+ spendN: number;
1120
+ pct: number;
1121
+ yoy: number | "NEW";
1122
+ }
1123
+ interface PublisherBreakdownListProps {
1124
+ rows: PubBreak[];
1125
+ /** @default 10 */
1126
+ defaultVisibleCount?: number;
1127
+ /** Set false when the container already supplies its own padding. @default true */
1128
+ padded?: boolean;
1129
+ /** When provided, the truncated-list footer becomes a plain link instead of
1130
+ * the inline "see all" expand/collapse behavior. */
1131
+ viewAllHref?: string;
1132
+ /**
1133
+ * Custom per-row logo/avatar rendering -- e.g. a real logo-fetching hook or
1134
+ * a brand-name-to-domain favicon lookup. Defaults to a plain initials
1135
+ * monogram so the component works with zero configuration; this package
1136
+ * intentionally doesn't ship a brand -> logo/domain lookup table, since
1137
+ * that's fixture/business data, not a design-system concern.
1138
+ */
1139
+ renderLogo?: (row: PubBreak) => ReactNode;
1140
+ }
1141
+ /**
1142
+ * @pattern PublisherBreakdownList
1143
+ * @description Flat Current/Previous/YoY spend list, prop-driven end to end
1144
+ * -- no data fetching of its own. Row padding is configurable so it can sit
1145
+ * flush inside a bordered card or inline within an already-padded container.
1146
+ * mr-clientapps-insights-studio's analogous "Top publishers" UI
1147
+ * (`PublisherBreakdown.tsx`) was evaluated for reconciliation and
1148
+ * deliberately NOT merged into this component -- see the escape-hatch note
1149
+ * below. `renderLogo` lets a caller plug in real per-row logos; without it,
1150
+ * rows show a plain initials monogram.
1151
+ * @tokens text-text-primary, text-text-tertiary, text-text-secondary,
1152
+ * text-text-link, text-text-warning, text-text-success, text-text-danger,
1153
+ * border-border-default, bg-surface, bg-subtle, rounded-control, rounded-full,
1154
+ * shadow-overlay
1155
+ * @behavior Truncates to `defaultVisibleCount` rows with a "See all (N more)"
1156
+ * / "Show less" toggle, unless `viewAllHref` is given, in which case the
1157
+ * footer becomes a plain link out instead (e.g. to a full breakdown page).
1158
+ *
1159
+ * Escape hatch: mr-clientapps-insights-studio's own publisher breakdown
1160
+ * (`domains/reviews/src/components/media-spend/PublisherBreakdown.tsx`) is a
1161
+ * genuinely different component, not just a differently-styled copy of this
1162
+ * one -- a three-level Owner/Publisher/Property accordion, each level
1163
+ * independently paginated against a live API (`usePublisherBreakdown`/
1164
+ * `usePublisherBreakdownChildren`), with per-row logo fetching via a batched
1165
+ * hook. None of that -- the recursive expand/collapse tree, the live
1166
+ * fetching, the level toggle -- has an equivalent in this flat,
1167
+ * fully-synchronous, prop-driven list, and bolting it on would mean either
1168
+ * building the accordion machinery into every consumer of this component
1169
+ * (including ones that only ever wanted a flat list, like the Opportunities
1170
+ * battle card) or branching internally on which "mode" is active -- exactly
1171
+ * the kind of forked-with-extra-indirection outcome this package's README
1172
+ * calls out as not actually shared. Per that guidance, this component stays
1173
+ * the flat-list case only; mr-clientapps' accordion stays where it is, un-migrated.
1174
+ * @usage
1175
+ * ```tsx
1176
+ * <PublisherBreakdownList rows={publishers} defaultVisibleCount={10} />
1177
+ * ```
1178
+ */
1179
+ declare function PublisherBreakdownList({ rows, defaultVisibleCount, padded, viewAllHref, renderLogo }: PublisherBreakdownListProps): react.JSX.Element;
1180
+
1181
+ export { AdvertiserNoResults, type AdvertiserNoResultsProps, AdvertiserQuickFilter, type AdvertiserQuickFilterProps, AdvertiserSearchPrompt, type AdvertiserSearchPromptProps, BarChartPlaceholder, type BarChartPlaceholderProps, Card, type CardProps, CheckBox, type CheckBoxProps, DEFAULT_SEARCH_MATCH, DataScopeFilter, type DataScopeFilterProps, DateRangeFilter, type DateRangeFilterProps, InfoTip, type InfoTipProps, LineChartPlaceholder, type LineChartPlaceholderProps, NO_RESULTS_BODY, NO_RESULTS_TITLE, type NoResultsAction, NoResultsEmptyState, type NoResultsEmptyStateProps, PRIMARY_ACTION_CLASS, type PickerColumn, type PickerFilter, type PickerRow, PieChartPlaceholder, type PieChartPlaceholderProps, type PubBreak, PublisherBreakdownList, type PublisherBreakdownListProps, QUIET_ACTION_CLASS, SearchBar, type SearchBarProps, type SearchBarVariant, type SearchMatchMode, type SearchMatchValue, SectionRail, type SectionRailProps, SelectAllVisible, type SelectAllVisibleProps, SelectFilter, type SelectFilterProps, SelectedSection, type SelectedSectionProps, SelectionPicker, type SelectionPickerAccountList, type SelectionPickerProps, StackedColumnPlaceholder, type StackedColumnPlaceholderProps, type SummaryGroup, type SummaryGroupItemValue, Tooltip, type TooltipProps, WorkspaceSelectedCard, type WorkspaceSelectedCardProps, noResultsBody };