@actioneer/ads 1.0.7 → 1.0.9

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.
Files changed (87) hide show
  1. package/DESIGN.md +119 -91
  2. package/README.md +19 -1
  3. package/components/ai-elements/chart.js +20 -20
  4. package/components/ai-elements/inline-citation.js +50 -50
  5. package/components/ai-elements/message.js +1 -1
  6. package/components/ai-elements/tool.js +5 -5
  7. package/components/alert.d.ts +3 -2
  8. package/components/alert.js +25 -25
  9. package/components/answer.js +1 -1
  10. package/components/assistant-ui/action-bar.js +1 -1
  11. package/components/assistant-ui/attachment.js +68 -49
  12. package/components/assistant-ui/composer-add-context.js +1 -1
  13. package/components/assistant-ui/composer-harness.js +5 -4
  14. package/components/assistant-ui/composer-trigger-popover.js +1 -1
  15. package/components/assistant-ui/composer.js +13 -13
  16. package/components/assistant-ui/message-timing.js +6 -6
  17. package/components/assistant-ui/selection-toolbar.js +5 -5
  18. package/components/assistant-ui/sources.js +15 -15
  19. package/components/assistant-ui/tooltip-icon-button.js +18 -14
  20. package/components/audio-waveform.d.ts +21 -8
  21. package/components/audio-waveform.js +371 -94
  22. package/components/autocomplete.js +9 -9
  23. package/components/breadcrumbs.js +36 -42
  24. package/components/button.d.ts +16 -1
  25. package/components/button.js +86 -73
  26. package/components/card.js +1 -1
  27. package/components/chart.d.ts +31 -4
  28. package/components/chart.js +900 -666
  29. package/components/chip.d.ts +2 -1
  30. package/components/chip.js +56 -51
  31. package/components/command-palette.js +12 -12
  32. package/components/connector-card.d.ts +1 -2
  33. package/components/connector-card.js +17 -17
  34. package/components/date-range-picker.js +1 -2
  35. package/components/drawer.js +9 -10
  36. package/components/dropdown.js +1 -1
  37. package/components/empty-state.d.ts +2 -0
  38. package/components/empty-state.js +17 -15
  39. package/components/field.js +9 -10
  40. package/components/file-dropzone.d.ts +6 -3
  41. package/components/file-dropzone.js +72 -60
  42. package/components/filters.d.ts +37 -0
  43. package/components/filters.js +129 -0
  44. package/components/index.d.ts +1 -0
  45. package/components/index.js +447 -444
  46. package/components/infinite-canvas.js +9 -9
  47. package/components/modal.d.ts +2 -1
  48. package/components/modal.js +102 -91
  49. package/components/page-body.js +8 -8
  50. package/components/popover.d.ts +6 -1
  51. package/components/popover.js +75 -65
  52. package/components/progress-circle.d.ts +1 -1
  53. package/components/progress-circle.js +57 -48
  54. package/components/segmented.d.ts +1 -0
  55. package/components/segmented.js +34 -36
  56. package/components/select.js +2 -2
  57. package/components/selection-action-bar.js +32 -31
  58. package/components/setting-row.js +10 -10
  59. package/components/sidebar.d.ts +15 -0
  60. package/components/sidebar.js +186 -149
  61. package/components/slider.js +5 -5
  62. package/components/stepper.d.ts +1 -1
  63. package/components/stepper.js +1 -1
  64. package/components/switch.js +10 -10
  65. package/components/table.d.ts +10 -2
  66. package/components/table.js +380 -366
  67. package/components/tabs.d.ts +6 -1
  68. package/components/tabs.js +32 -29
  69. package/components/timeline.js +3 -3
  70. package/components/toast.js +53 -56
  71. package/components/ui/button.js +4 -4
  72. package/components/ui/dialog.js +1 -1
  73. package/components/viz-charts.js +263 -221
  74. package/demo/components/add-to-artifact.js +3 -2
  75. package/examples/README.md +31 -0
  76. package/examples/agents-screen.tsx +477 -0
  77. package/examples/campaign-analysis-screen.tsx +138 -0
  78. package/examples/campaigns-screen.tsx +439 -0
  79. package/examples/settings-screen.tsx +90 -0
  80. package/examples/workspace-header.tsx +71 -0
  81. package/lib/icons.d.ts +8 -0
  82. package/lib/icons.js +171 -118
  83. package/llms.txt +93 -29
  84. package/manifest.json +1660 -113
  85. package/package.json +1 -1
  86. package/styles.css +55 -68
  87. package/theme.css +50 -59
@@ -0,0 +1,439 @@
1
+ /**
2
+ * Campaigns screen — the ADS collection-screen recipe, built only from ADS exports.
3
+ *
4
+ * Follows principles `collection-screens`, `record-detail-panels`,
5
+ * `status-language` and `tabs-search-opposed` (see `get_principles`):
6
+ *
7
+ * - One white panel: BodyPageHeader (title + one primary action), then the
8
+ * control row — Tabs with counts leading, SearchDisclosure then Filters as
9
+ * icon triggers trailing — then the Table.
10
+ * - The rows fill the panel: the page size is derived from the height that is
11
+ * left, so Pagination always sits on the panel's bottom edge.
12
+ * - Row checkboxes appear on hover (the header checkbox is always visible).
13
+ * - Clicking a row opens its detail panel beside the list and marks the row
14
+ * with `isActive` — no radio, no modal.
15
+ * - The detail panel: kicker, title, status Chip + context under the title,
16
+ * then Performance (stat cards + chart card) and Details (label–value rows).
17
+ * - New campaign opens as a Modal over the list (never a separate screen): one
18
+ * scrolling form — Campaign name, Caller numbers, Audience — with Cancel and
19
+ * Create campaign together in the footer.
20
+ *
21
+ * In a full app this screen sits inside the shell, next to the Sidebar
22
+ * (put the customer's logo in its brand slot with SidebarWorkspace).
23
+ * Copy it and replace CAMPAIGNS with your own records.
24
+ */
25
+ import * as React from "react";
26
+ import {
27
+ Avatar,
28
+ BodyPageHeader,
29
+ Button,
30
+ Checkbox,
31
+ Chip,
32
+ Drawer,
33
+ DrawerBody,
34
+ DrawerFooter,
35
+ DrawerHeader,
36
+ DrawerTitle,
37
+ FileDropzone,
38
+ Filters,
39
+ InputField,
40
+ LineChart,
41
+ Modal,
42
+ ModalBody,
43
+ ModalFooter,
44
+ ModalHeader,
45
+ ModalTitle,
46
+ Pagination,
47
+ SearchDisclosure,
48
+ SegmentedControl,
49
+ Tab,
50
+ TabList,
51
+ Table,
52
+ TableBody,
53
+ TableCell,
54
+ TableColumn,
55
+ TableHeader,
56
+ TableRow,
57
+ Tabs,
58
+ type FilterGroup,
59
+ } from "@actioneer/ads";
60
+
61
+ /** Business-language statuses, one tone per meaning (principle `status-language`). */
62
+ export type CampaignStatus = "Active" | "Scheduled" | "Paused" | "Completed" | "Draft" | "Failed";
63
+
64
+ export const STATUS_COLOR: Record<CampaignStatus, "info" | "warning" | "success" | "default" | "danger"> = {
65
+ Active: "info",
66
+ Scheduled: "info",
67
+ Paused: "warning",
68
+ Completed: "success",
69
+ Draft: "default",
70
+ Failed: "danger",
71
+ };
72
+
73
+ export interface CampaignRecord {
74
+ id: string;
75
+ name: string;
76
+ status: CampaignStatus;
77
+ leads: number;
78
+ connectRate: string;
79
+ booked: number;
80
+ owner: string;
81
+ started: string;
82
+ context: string;
83
+ callingWindows: number;
84
+ /** Connected calls for the last seven days. */
85
+ week: number[];
86
+ cohorts?: CampaignRecord[];
87
+ }
88
+
89
+ const week = (...values: number[]) => values;
90
+
91
+ export const CAMPAIGNS: CampaignRecord[] = [
92
+ {
93
+ id: "q4", name: "Q4 renewal reminders", status: "Active", leads: 2400, connectRate: "44%", booked: 286, owner: "Divya Shetty", started: "Oct 22", context: "Running since Oct 22", callingWindows: 2, week: week(210, 260, 240, 310, 290, 180, 150),
94
+ cohorts: [
95
+ { id: "q4-blr", name: "Bengaluru cohort", status: "Active", leads: 1500, connectRate: "47%", booked: 188, owner: "Divya Shetty", started: "Oct 22", context: "Running since Oct 22", callingWindows: 2, week: week(140, 170, 160, 210, 190, 120, 100) },
96
+ { id: "q4-bom", name: "Mumbai cohort", status: "Paused", leads: 900, connectRate: "39%", booked: 98, owner: "Kiran Menon", started: "Oct 22", context: "Paused today at 11:40", callingWindows: 2, week: week(70, 90, 80, 100, 100, 60, 0) },
97
+ ],
98
+ },
99
+ { id: "emi", name: "Loan EMI nudges", status: "Completed", leads: 5120, connectRate: "41%", booked: 612, owner: "Vikram Nair", started: "Oct 14", context: "Finished Oct 21", callingWindows: 3, week: week(420, 510, 480, 620, 580, 310, 260) },
100
+ { id: "kyc", name: "KYC re-verification", status: "Active", leads: 3260, connectRate: "38%", booked: 402, owner: "Rohan Iyer", started: "Oct 20", context: "Running since Oct 20", callingWindows: 2, week: week(260, 300, 320, 340, 330, 280, 310) },
101
+ { id: "lapse", name: "Policy lapse win-back", status: "Draft", leads: 780, connectRate: "—", booked: 0, owner: "Asha Rao", started: "—", context: "Not launched", callingWindows: 1, week: week(0, 0, 0, 0, 0, 0, 0) },
102
+ { id: "card", name: "Card activation follow-up", status: "Scheduled", leads: 1840, connectRate: "—", booked: 0, owner: "Meera Pillai", started: "Oct 28", context: "Starts Oct 28 at 10:00", callingWindows: 2, week: week(0, 0, 0, 0, 0, 0, 0) },
103
+ { id: "payment", name: "Payment failure outreach", status: "Completed", leads: 1120, connectRate: "45%", booked: 151, owner: "Arjun Das", started: "Oct 12", context: "Finished Oct 19", callingWindows: 2, week: week(160, 190, 170, 210, 200, 70, 34) },
104
+ { id: "festive", name: "Festive offer callbacks", status: "Paused", leads: 640, connectRate: "36%", booked: 52, owner: "Neha Kapoor", started: "Oct 18", context: "Paused yesterday at 18:10", callingWindows: 1, week: week(60, 80, 70, 90, 88, 0, 0) },
105
+ { id: "abandoned", name: "Abandoned application recovery", status: "Active", leads: 4050, connectRate: "40%", booked: 377, owner: "Sanjay Rao", started: "Oct 18", context: "Running since Oct 18", callingWindows: 3, week: week(340, 380, 400, 420, 410, 330, 310) },
106
+ { id: "renewal-check", name: "Insurance renewal check-in", status: "Failed", leads: 2210, connectRate: "—", booked: 0, owner: "Kiran Menon", started: "Oct 16", context: "CRM writeback failed on Oct 16", callingWindows: 2, week: week(0, 0, 0, 0, 0, 0, 0) },
107
+ { id: "credit-limit", name: "Credit limit upgrade offer", status: "Completed", leads: 3480, connectRate: "43%", booked: 298, owner: "Divya Shetty", started: "Oct 08", context: "Finished Oct 15", callingWindows: 3, week: week(300, 320, 310, 360, 340, 120, 80) },
108
+ { id: "survey", name: "Home loan top-up survey", status: "Active", leads: 2730, connectRate: "35%", booked: 120, owner: "Asha Rao", started: "Oct 24", context: "Running since Oct 24", callingWindows: 1, week: week(0, 0, 120, 180, 210, 190, 160) },
109
+ { id: "welcome", name: "New account welcome calls", status: "Scheduled", leads: 960, connectRate: "—", booked: 0, owner: "Meera Pillai", started: "Nov 02", context: "Starts Nov 2 at 09:30", callingWindows: 1, week: week(0, 0, 0, 0, 0, 0, 0) },
110
+ ];
111
+
112
+ /** Status tabs use the same words as the status chips. */
113
+ const TABS: Array<{ value: "all" | CampaignStatus; label: string }> = [
114
+ { value: "all", label: "All" },
115
+ { value: "Draft", label: "Draft" },
116
+ { value: "Active", label: "Active" },
117
+ { value: "Paused", label: "Paused" },
118
+ { value: "Completed", label: "Completed" },
119
+ ];
120
+
121
+ const INITIAL_FILTERS: FilterGroup[] = [
122
+ {
123
+ id: "owner",
124
+ label: "Owner",
125
+ value: "all",
126
+ options: [{ value: "all", label: "All owners" }, ...Array.from(new Set(CAMPAIGNS.map((c) => c.owner))).map((owner) => ({ value: owner, label: owner }))],
127
+ },
128
+ {
129
+ id: "windows",
130
+ label: "Calling windows",
131
+ value: "all",
132
+ options: [
133
+ { value: "all", label: "Any number" },
134
+ { value: "1", label: "1 window" },
135
+ { value: "2", label: "2 windows" },
136
+ { value: "3", label: "3 windows" },
137
+ ],
138
+ },
139
+ ];
140
+
141
+ const ROW_HEIGHT = 64;
142
+ /** Header row + footer (pagination) heights inside the table surface. */
143
+ const TABLE_CHROME = 37 + 53;
144
+
145
+ /** Derive the page size from the height left in the panel, so rows fill it. */
146
+ function useRowsThatFit(ref: React.RefObject<HTMLDivElement | null>) {
147
+ const [rows, setRows] = React.useState(8);
148
+ React.useLayoutEffect(() => {
149
+ const node = ref.current;
150
+ if (!node) return;
151
+ const update = () => setRows(Math.max(3, Math.floor((node.clientHeight - TABLE_CHROME) / ROW_HEIGHT)));
152
+ update();
153
+ const observer = new ResizeObserver(update);
154
+ observer.observe(node);
155
+ return () => observer.disconnect();
156
+ }, [ref]);
157
+ return rows;
158
+ }
159
+
160
+ function findCampaign(id: string | null): CampaignRecord | undefined {
161
+ if (!id) return undefined;
162
+ for (const campaign of CAMPAIGNS) {
163
+ if (campaign.id === id) return campaign;
164
+ const cohort = campaign.cohorts?.find((child) => child.id === id);
165
+ if (cohort) return cohort;
166
+ }
167
+ return undefined;
168
+ }
169
+
170
+ function CampaignCells({ campaign }: { campaign: CampaignRecord }) {
171
+ return (
172
+ <>
173
+ <TableCell>{campaign.name}</TableCell>
174
+ <TableCell>
175
+ <Chip color={STATUS_COLOR[campaign.status]}>{campaign.status}</Chip>
176
+ </TableCell>
177
+ <TableCell align="right" className="tabular-nums">{campaign.leads.toLocaleString("en-IN")}</TableCell>
178
+ <TableCell>
179
+ <span className="flex items-center gap-2">
180
+ <Avatar name={campaign.owner} size="sm" />
181
+ {campaign.owner}
182
+ </span>
183
+ </TableCell>
184
+ </>
185
+ );
186
+ }
187
+
188
+ /** Selection-driven detail panel (principle `record-detail-panels`). */
189
+ export function CampaignDetail({ campaign, onClose }: { campaign: CampaignRecord | undefined; onClose: () => void }) {
190
+ return (
191
+ <Drawer open={Boolean(campaign)} onOpenChange={(open) => !open && onClose()} aria-label="Campaign details">
192
+ {campaign && (
193
+ <>
194
+ <DrawerHeader>
195
+ <p className="text-xs uppercase tracking-wide text-secondary">Campaign</p>
196
+ <DrawerTitle>{campaign.name}</DrawerTitle>
197
+ {/* Status lives with the title, so it never floats on its own. */}
198
+ <div className="mt-1 flex items-center gap-2">
199
+ <Chip color={STATUS_COLOR[campaign.status]}>{campaign.status}</Chip>
200
+ <span className="text-s text-secondary">{campaign.context}</span>
201
+ </div>
202
+ </DrawerHeader>
203
+ <DrawerBody className="flex flex-col gap-8 overflow-y-auto">
204
+ <section aria-label="Performance" className="flex flex-col gap-2">
205
+ <h3 className="mb-1 text-m font-medium text-foreground">Performance</h3>
206
+ <dl className="grid grid-cols-3 gap-2">
207
+ {[
208
+ { label: "Leads", value: campaign.leads.toLocaleString("en-IN") },
209
+ { label: "Connect rate", value: campaign.connectRate },
210
+ { label: "Booked", value: campaign.booked.toLocaleString("en-IN") },
211
+ ].map((stat) => (
212
+ <div key={stat.label} className="min-w-0 rounded-l border border-tertiary p-3">
213
+ <dt className="truncate text-s text-secondary">{stat.label}</dt>
214
+ <dd className="mt-0.5 text-xl tabular-nums text-foreground">{stat.value}</dd>
215
+ </div>
216
+ ))}
217
+ </dl>
218
+ <div className="rounded-l border border-tertiary p-4">
219
+ <LineChart
220
+ title={<span className="text-s font-normal text-secondary">Connected calls · last 7 days</span>}
221
+ data={campaign.week.map((calls, index) => ({ day: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][index], calls }))}
222
+ categoryKey="day"
223
+ series={["calls"]}
224
+ seriesLabels={{ calls: "Connected" }}
225
+ legend={false}
226
+ showYAxis={false}
227
+ height={160}
228
+ />
229
+ </div>
230
+ </section>
231
+ <section aria-label="Details" className="flex flex-col gap-1">
232
+ <h3 className="text-m font-medium text-foreground">Details</h3>
233
+ <dl>
234
+ {[
235
+ { label: "Owner", value: campaign.owner },
236
+ { label: "Started", value: campaign.started },
237
+ { label: "Calling windows", value: String(campaign.callingWindows) },
238
+ ].map((row) => (
239
+ <div key={row.label} className="flex min-h-10 items-center justify-between gap-3 border-b border-tertiary last:border-0">
240
+ <dt className="text-m text-secondary">{row.label}</dt>
241
+ <dd className="text-m text-foreground">{row.value}</dd>
242
+ </div>
243
+ ))}
244
+ </dl>
245
+ </section>
246
+ </DrawerBody>
247
+ <DrawerFooter>
248
+ <Button variant="outline">Duplicate</Button>
249
+ <Button>Open full view</Button>
250
+ </DrawerFooter>
251
+ </>
252
+ )}
253
+ </Drawer>
254
+ );
255
+ }
256
+
257
+ const SAVED_AUDIENCES = [
258
+ { id: "0019b431", name: "CSV audience 0019b431", size: 6 },
259
+ { id: "b190b191", name: "CSV audience b190b191", size: 868 },
260
+ { id: "5ee46ae6", name: "CSV audience 5ee46ae6", size: 162 },
261
+ { id: "9dc3d0b1", name: "CSV audience 9dc3d0b1", size: 188 },
262
+ ];
263
+
264
+ /** Create task launched from the collection: a Modal over the list (principle `minimal-information`). */
265
+ export function NewCampaignModal({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
266
+ const [name, setName] = React.useState("");
267
+ const [useNumber, setUseNumber] = React.useState(false);
268
+ const [audience, setAudience] = React.useState("csv");
269
+ const [query, setQuery] = React.useState("");
270
+ const audiences = SAVED_AUDIENCES.filter((item) => item.name.toLowerCase().includes(query.trim().toLowerCase()));
271
+
272
+ return (
273
+ <Modal open={open} onOpenChange={onOpenChange} height="fixed">
274
+ <ModalHeader>
275
+ <ModalTitle>New campaign</ModalTitle>
276
+ </ModalHeader>
277
+ <ModalBody className="flex flex-col gap-6">
278
+ <InputField label="Campaign name" value={name} onChange={(event) => setName(event.target.value)} placeholder="First-EMI onboarding" />
279
+ <div className="flex flex-col gap-2">
280
+ <p className="text-m font-medium text-foreground">Caller numbers</p>
281
+ <div className="flex min-h-12 items-center gap-3 rounded-m border border-input px-3">
282
+ <Checkbox aria-label="+91 22 6987 0900" checked={useNumber} onChange={(event) => setUseNumber(event.target.checked)} />
283
+ <span className="flex-1 text-m tabular-nums text-foreground">+91 22 6987 0900</span>
284
+ <Chip>Plivo</Chip>
285
+ </div>
286
+ <p className="text-s text-secondary">Optional. Tick none to call from any enabled number; you can change this later, even while it is calling.</p>
287
+ </div>
288
+ <div className="flex flex-col gap-3">
289
+ <p className="text-m font-medium text-foreground">Audience</p>
290
+ <SegmentedControl
291
+ aria-label="Audience source"
292
+ options={[{ value: "csv", label: "CSV upload" }, { value: "empty", label: "Empty audience" }]}
293
+ value={audience}
294
+ onChange={setAudience}
295
+ />
296
+ {audience === "csv" && (
297
+ <>
298
+ <InputField aria-label="Search audiences" type="search" placeholder="Search audiences" value={query} onChange={(event) => setQuery(event.target.value)} />
299
+ <FileDropzone accept=".csv,.xlsx" hint="CSV or first-sheet .xlsx with a phone column; name and other columns are optional" />
300
+ <ul aria-label="Saved audiences" className="rounded-m border border-input px-3">
301
+ {audiences.map((item) => (
302
+ <li key={item.id} className="flex min-h-10 items-center justify-between gap-3 border-b border-tertiary text-m last:border-0">
303
+ <span className="text-foreground">{item.name}</span>
304
+ <span className="tabular-nums text-secondary">{item.size.toLocaleString("en-IN")}</span>
305
+ </li>
306
+ ))}
307
+ </ul>
308
+ </>
309
+ )}
310
+ </div>
311
+ </ModalBody>
312
+ <ModalFooter>
313
+ <Button variant="outline" onClick={() => onOpenChange(false)}>Cancel</Button>
314
+ <Button disabled={!name.trim()} onClick={() => onOpenChange(false)}>Create campaign</Button>
315
+ </ModalFooter>
316
+ </Modal>
317
+ );
318
+ }
319
+
320
+ export function CampaignsScreen() {
321
+ const [tab, setTab] = React.useState<"all" | CampaignStatus>("all");
322
+ const [search, setSearch] = React.useState("");
323
+ const [searchOpen, setSearchOpen] = React.useState(false);
324
+ const [filters, setFilters] = React.useState(INITIAL_FILTERS);
325
+ const [selectedKeys, setSelectedKeys] = React.useState<Set<string>>(new Set());
326
+ const [openId, setOpenId] = React.useState<string | null>(null);
327
+ const [page, setPage] = React.useState(1);
328
+ const [creating, setCreating] = React.useState(false);
329
+ const rowsRef = React.useRef<HTMLDivElement>(null);
330
+ const pageSize = useRowsThatFit(rowsRef);
331
+
332
+ const owner = filters.find((group) => group.id === "owner")?.value ?? "all";
333
+ const windows = filters.find((group) => group.id === "windows")?.value ?? "all";
334
+ const query = search.trim().toLowerCase();
335
+ const visible = CAMPAIGNS.filter(
336
+ (c) =>
337
+ (tab === "all" || c.status === tab) &&
338
+ (owner === "all" || c.owner === owner) &&
339
+ (windows === "all" || String(c.callingWindows) === windows) &&
340
+ (!query || c.name.toLowerCase().includes(query)),
341
+ );
342
+ const pages = Math.max(1, Math.ceil(visible.length / pageSize));
343
+ const current = Math.min(page, pages);
344
+ const pageRows = visible.slice((current - 1) * pageSize, current * pageSize);
345
+ const countFor = (value: "all" | CampaignStatus) =>
346
+ value === "all" ? CAMPAIGNS.length : CAMPAIGNS.filter((c) => c.status === value).length;
347
+
348
+ return (
349
+ <div className="flex h-screen gap-3 bg-background p-3 text-foreground">
350
+ <section aria-label="Voice campaigns" className="flex min-w-0 flex-1 flex-col overflow-hidden rounded-l bg-card">
351
+ <BodyPageHeader title="Voice campaigns" actions={<Button onClick={() => setCreating(true)}>New campaign</Button>} />
352
+
353
+ {/* Tabs lead; search and filters trail as icon triggers. */}
354
+ <div className="flex items-center justify-between gap-4 px-6">
355
+ <Tabs className="min-w-0 flex-1" value={tab} onValueChange={(value) => { setTab(value as typeof tab); setPage(1); }}>
356
+ <TabList aria-label="Filter campaigns by status">
357
+ {TABS.map((item) => (
358
+ <Tab key={item.value} value={item.value} count={countFor(item.value)}>
359
+ {item.label}
360
+ </Tab>
361
+ ))}
362
+ </TabList>
363
+ </Tabs>
364
+ <div className="flex shrink-0 items-center gap-2">
365
+ <SearchDisclosure
366
+ value={search}
367
+ onValueChange={(value) => { setSearch(value); setPage(1); }}
368
+ open={searchOpen}
369
+ onOpenChange={setSearchOpen}
370
+ placeholder="Search campaigns"
371
+ triggerLabel="Search campaigns"
372
+ />
373
+ <Filters
374
+ triggerLabel="Filter campaigns"
375
+ groups={filters}
376
+ onChange={(id, value) => { setFilters((groups) => groups.map((group) => (group.id === id ? { ...group, value } : group))); setPage(1); }}
377
+ />
378
+ </div>
379
+ </div>
380
+
381
+ {/* The rows fill what is left of the panel; pagination sits at the bottom. */}
382
+ <div ref={rowsRef} className="min-h-0 flex-1 px-6 pt-4 pb-6">
383
+ <Table
384
+ aria-label="Voice campaigns"
385
+ selectionMode="multiple"
386
+ selectedKeys={selectedKeys}
387
+ onSelectionChange={setSelectedKeys}
388
+ footer={
389
+ <Pagination
390
+ className="w-full"
391
+ total={pages}
392
+ page={current}
393
+ onChange={setPage}
394
+ summary={`${visible.length === 0 ? 0 : (current - 1) * pageSize + 1}–${Math.min(current * pageSize, visible.length)} of ${visible.length} campaigns`}
395
+ />
396
+ }
397
+ >
398
+ <TableHeader>
399
+ <TableRow>
400
+ <TableColumn>Campaign</TableColumn>
401
+ <TableColumn>Status</TableColumn>
402
+ <TableColumn align="right">Leads</TableColumn>
403
+ <TableColumn>Owner</TableColumn>
404
+ </TableRow>
405
+ </TableHeader>
406
+ <TableBody>
407
+ {pageRows.map((campaign) => (
408
+ <TableRow
409
+ key={campaign.id}
410
+ rowKey={campaign.id}
411
+ aria-label={`Open ${campaign.name}`}
412
+ isActive={openId === campaign.id}
413
+ onClick={() => setOpenId((id) => (id === campaign.id ? null : campaign.id))}
414
+ childRows={campaign.cohorts?.map((cohort) => (
415
+ <TableRow
416
+ key={cohort.id}
417
+ rowKey={cohort.id}
418
+ aria-label={`Open ${cohort.name}`}
419
+ isActive={openId === cohort.id}
420
+ onClick={() => setOpenId((id) => (id === cohort.id ? null : cohort.id))}
421
+ >
422
+ <CampaignCells campaign={cohort} />
423
+ </TableRow>
424
+ ))}
425
+ defaultExpanded={Boolean(campaign.cohorts)}
426
+ >
427
+ <CampaignCells campaign={campaign} />
428
+ </TableRow>
429
+ ))}
430
+ </TableBody>
431
+ </Table>
432
+ </div>
433
+ </section>
434
+
435
+ <CampaignDetail campaign={findCampaign(openId)} onClose={() => setOpenId(null)} />
436
+ <NewCampaignModal open={creating} onOpenChange={setCreating} />
437
+ </div>
438
+ );
439
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Settings screen — a settings card built only from ADS exports.
3
+ *
4
+ * What it shows: Card header with a status Badge, an InputField, a SettingList
5
+ * of SettingRows whose trailing control is a Switch or Button, and a footer
6
+ * with one primary action.
7
+ */
8
+ import * as React from "react";
9
+ import {
10
+ Badge,
11
+ Button,
12
+ Card,
13
+ CardBody,
14
+ CardDescription,
15
+ CardFooter,
16
+ CardHeader,
17
+ CardTitle,
18
+ InputField,
19
+ SettingList,
20
+ SettingRow,
21
+ Switch,
22
+ } from "@actioneer/ads";
23
+ import {
24
+ Database,
25
+ Settings,
26
+ User,
27
+ } from "@actioneer/ads/lib/icons";
28
+
29
+ export function SettingsScreen() {
30
+ const [digest, setDigest] = React.useState(false);
31
+ const [sharing, setSharing] = React.useState(true);
32
+
33
+ return (
34
+ <Card className="mx-auto w-full max-w-2xl">
35
+ <CardHeader>
36
+ <div className="flex items-start justify-between gap-4">
37
+ <div className="flex min-w-0 flex-col gap-1">
38
+ <CardTitle>Workspace preferences</CardTitle>
39
+ <CardDescription>
40
+ A canonical settings composition: context first, grouped controls second, one clear save action.
41
+ </CardDescription>
42
+ </div>
43
+ <Badge variant="dot" color="success">Saved</Badge>
44
+ </div>
45
+ </CardHeader>
46
+
47
+ <CardBody className="flex flex-col gap-5">
48
+ <InputField
49
+ label="Workspace name"
50
+ defaultValue="Actioneer research"
51
+ description="Shown to everyone in this workspace."
52
+ />
53
+
54
+ <SettingList aria-label="Workspace settings">
55
+ <SettingRow
56
+ icon={<User />}
57
+ label="Weekly digest"
58
+ description="Receive a concise summary every Monday."
59
+ action={
60
+ <Switch checked={digest} onCheckedChange={setDigest}>
61
+ Weekly digest
62
+ </Switch>
63
+ }
64
+ />
65
+ <SettingRow
66
+ icon={<Database />}
67
+ label="Source sharing"
68
+ description="Allow teammates to reuse connected sources."
69
+ action={
70
+ <Switch checked={sharing} onCheckedChange={setSharing}>
71
+ Source sharing
72
+ </Switch>
73
+ }
74
+ />
75
+ <SettingRow
76
+ icon={<Settings />}
77
+ label="Advanced controls"
78
+ description="Manage retention and export permissions."
79
+ action={<Button variant="ghost" size="sm">Manage</Button>}
80
+ />
81
+ </SettingList>
82
+ </CardBody>
83
+
84
+ <CardFooter className="justify-end gap-2">
85
+ <Button variant="ghost">Cancel</Button>
86
+ <Button>Save changes</Button>
87
+ </CardFooter>
88
+ </Card>
89
+ );
90
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Workspace header — a page header and tabbed body built only from ADS exports.
3
+ *
4
+ * What it shows: title with a status Badge, trailing page actions, and Tabs
5
+ * with counts whose panels hold Cards.
6
+ */
7
+ import {
8
+ Badge,
9
+ Button,
10
+ Card,
11
+ CardBody,
12
+ Tab,
13
+ TabList,
14
+ TabPanel,
15
+ Tabs,
16
+ } from "@actioneer/ads";
17
+ import {
18
+ ArrowUpRight,
19
+ Plus,
20
+ } from "@actioneer/ads/lib/icons";
21
+
22
+ export function WorkspaceHeaderScreen() {
23
+ return (
24
+ <section className="mx-auto flex w-full max-w-4xl flex-col gap-6">
25
+ <header className="flex flex-col items-start gap-5 sm:flex-row sm:justify-between">
26
+ <div className="flex w-full min-w-0 flex-1 flex-col gap-2">
27
+ <div className="flex flex-wrap items-center gap-2">
28
+ <h1 className="text-2xl font-medium text-foreground">Research workspace</h1>
29
+ <Badge variant="dot" color="success">Connected</Badge>
30
+ </div>
31
+ <p className="max-w-2xl text-base text-muted-foreground">
32
+ Keep status beside the title, supporting context below, and primary actions at the far edge.
33
+ </p>
34
+ </div>
35
+ <div className="flex shrink-0 items-center gap-2">
36
+ <Button variant="outline" endContent={<ArrowUpRight aria-hidden />}>Share</Button>
37
+ <Button startContent={<Plus aria-hidden />}>New board</Button>
38
+ </div>
39
+ </header>
40
+
41
+ <Tabs defaultValue="boards">
42
+ <TabList aria-label="Workspace sections">
43
+ <Tab value="boards" count={12}>Boards</Tab>
44
+ <Tab value="sources" count={6}>Sources</Tab>
45
+ <Tab value="members" count={8}>Members</Tab>
46
+ </TabList>
47
+ <TabPanel value="boards">
48
+ <Card>
49
+ <CardBody>
50
+ <p className="text-base text-foreground">Boards keep ongoing analysis visible and reusable.</p>
51
+ </CardBody>
52
+ </Card>
53
+ </TabPanel>
54
+ <TabPanel value="sources">
55
+ <Card>
56
+ <CardBody>
57
+ <p className="text-base text-foreground">Six connected sources are available to this workspace.</p>
58
+ </CardBody>
59
+ </Card>
60
+ </TabPanel>
61
+ <TabPanel value="members">
62
+ <Card>
63
+ <CardBody>
64
+ <p className="text-base text-foreground">Eight members can collaborate here.</p>
65
+ </CardBody>
66
+ </Card>
67
+ </TabPanel>
68
+ </Tabs>
69
+ </section>
70
+ );
71
+ }
package/lib/icons.d.ts CHANGED
@@ -21,6 +21,14 @@ export type IconProps = Omit<React.ComponentProps<PhosphorIcon>, "ref"> & {
21
21
  /** Legacy Hugeicons prop — `>= 2` maps to weight="bold", else ignored. */
22
22
  strokeWidth?: number;
23
23
  };
24
+ /**
25
+ * Icon-only controls (Button `size="icon*"`, TooltipIconButton) provide this
26
+ * context. The control turns its icon teal on hover and draws no background.
27
+ * Only the info icon also swaps to its `fill` weight (a hidden twin that
28
+ * `[data-icon-hover-fill]:hover` in theme.css reveals); every other icon keeps
29
+ * its regular outline on hover.
30
+ */
31
+ export declare const IconHoverFillContext: React.Context<boolean>;
24
32
  export declare const ArrowDown: React.ForwardRefExoticComponent<Omit<import('@phosphor-icons/react').IconProps, "ref"> & {
25
33
  /** Legacy Hugeicons prop — `>= 2` maps to weight="bold", else ignored. */
26
34
  strokeWidth?: number;