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