@cdx-ui/components 0.3.1 → 0.3.3

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 (94) hide show
  1. package/README.md +2 -2
  2. package/coverage.svg +1 -1
  3. package/docs/components/bottom-sheet/content.md +2 -2
  4. package/docs/components/content.md +2 -0
  5. package/docs/components/currency-input/content.md +94 -5
  6. package/docs/components/data-grid/content.md +125 -1
  7. package/docs/components/insight/content.md +644 -0
  8. package/docs/components/insight-stack/content.md +231 -0
  9. package/docs/foundations/styles/tokens/all-tokens/content.md +23 -3
  10. package/docs/llms.txt +4 -0
  11. package/docs/patterns/forms/content.md +255 -0
  12. package/docs/resources/release-notes/0.3.1/content.md +13 -0
  13. package/docs/resources/release-notes/0.3.2/content.md +17 -0
  14. package/docs/resources/release-notes/content.md +2 -0
  15. package/lib/commonjs/components/BottomSheet/index.js +63 -16
  16. package/lib/commonjs/components/BottomSheet/index.js.map +1 -1
  17. package/lib/commonjs/components/CurrencyInput/index.js +35 -10
  18. package/lib/commonjs/components/CurrencyInput/index.js.map +1 -1
  19. package/lib/commonjs/components/CurrencyInput/styles.js +40 -3
  20. package/lib/commonjs/components/CurrencyInput/styles.js.map +1 -1
  21. package/lib/commonjs/components/Insight/index.js +438 -0
  22. package/lib/commonjs/components/Insight/index.js.map +1 -0
  23. package/lib/commonjs/components/Insight/insetRadius.js +35 -0
  24. package/lib/commonjs/components/Insight/insetRadius.js.map +1 -0
  25. package/lib/commonjs/components/Insight/styles.js +186 -0
  26. package/lib/commonjs/components/Insight/styles.js.map +1 -0
  27. package/lib/commonjs/components/InsightStack/index.js +93 -0
  28. package/lib/commonjs/components/InsightStack/index.js.map +1 -0
  29. package/lib/commonjs/components/InsightStack/styles.js +61 -0
  30. package/lib/commonjs/components/InsightStack/styles.js.map +1 -0
  31. package/lib/commonjs/components/index.js +24 -0
  32. package/lib/commonjs/components/index.js.map +1 -1
  33. package/lib/commonjs/figma/CurrencyInput.figma.js +8 -8
  34. package/lib/commonjs/figma/CurrencyInput.figma.js.map +1 -1
  35. package/lib/commonjs/figma/Insight.figma.js +73 -0
  36. package/lib/commonjs/figma/Insight.figma.js.map +1 -0
  37. package/lib/commonjs/figma/InsightStack.figma.js +39 -0
  38. package/lib/commonjs/figma/InsightStack.figma.js.map +1 -0
  39. package/lib/module/components/BottomSheet/index.js +64 -17
  40. package/lib/module/components/BottomSheet/index.js.map +1 -1
  41. package/lib/module/components/CurrencyInput/index.js +37 -12
  42. package/lib/module/components/CurrencyInput/index.js.map +1 -1
  43. package/lib/module/components/CurrencyInput/styles.js +38 -3
  44. package/lib/module/components/CurrencyInput/styles.js.map +1 -1
  45. package/lib/module/components/Insight/index.js +433 -0
  46. package/lib/module/components/Insight/index.js.map +1 -0
  47. package/lib/module/components/Insight/insetRadius.js +30 -0
  48. package/lib/module/components/Insight/insetRadius.js.map +1 -0
  49. package/lib/module/components/Insight/styles.js +183 -0
  50. package/lib/module/components/Insight/styles.js.map +1 -0
  51. package/lib/module/components/InsightStack/index.js +89 -0
  52. package/lib/module/components/InsightStack/index.js.map +1 -0
  53. package/lib/module/components/InsightStack/styles.js +58 -0
  54. package/lib/module/components/InsightStack/styles.js.map +1 -0
  55. package/lib/module/components/index.js +2 -0
  56. package/lib/module/components/index.js.map +1 -1
  57. package/lib/module/figma/CurrencyInput.figma.js +8 -8
  58. package/lib/module/figma/CurrencyInput.figma.js.map +1 -1
  59. package/lib/module/figma/Insight.figma.js +67 -0
  60. package/lib/module/figma/Insight.figma.js.map +1 -0
  61. package/lib/module/figma/InsightStack.figma.js +33 -0
  62. package/lib/module/figma/InsightStack.figma.js.map +1 -0
  63. package/lib/typescript/components/BottomSheet/index.d.ts.map +1 -1
  64. package/lib/typescript/components/CurrencyInput/index.d.ts +4 -1
  65. package/lib/typescript/components/CurrencyInput/index.d.ts.map +1 -1
  66. package/lib/typescript/components/CurrencyInput/styles.d.ts +17 -6
  67. package/lib/typescript/components/CurrencyInput/styles.d.ts.map +1 -1
  68. package/lib/typescript/components/Insight/index.d.ts +87 -0
  69. package/lib/typescript/components/Insight/index.d.ts.map +1 -0
  70. package/lib/typescript/components/Insight/insetRadius.d.ts +8 -0
  71. package/lib/typescript/components/Insight/insetRadius.d.ts.map +1 -0
  72. package/lib/typescript/components/Insight/styles.d.ts +33 -0
  73. package/lib/typescript/components/Insight/styles.d.ts.map +1 -0
  74. package/lib/typescript/components/InsightStack/index.d.ts +21 -0
  75. package/lib/typescript/components/InsightStack/index.d.ts.map +1 -0
  76. package/lib/typescript/components/InsightStack/styles.d.ts +11 -0
  77. package/lib/typescript/components/InsightStack/styles.d.ts.map +1 -0
  78. package/lib/typescript/components/index.d.ts +2 -0
  79. package/lib/typescript/components/index.d.ts.map +1 -1
  80. package/lib/typescript/figma/CurrencyInput.figma.d.ts.map +1 -1
  81. package/lib/typescript/figma/Insight.figma.d.ts +8 -0
  82. package/lib/typescript/figma/Insight.figma.d.ts.map +1 -0
  83. package/lib/typescript/figma/InsightStack.figma.d.ts +8 -0
  84. package/lib/typescript/figma/InsightStack.figma.d.ts.map +1 -0
  85. package/package.json +5 -5
  86. package/src/components/BottomSheet/index.tsx +88 -13
  87. package/src/components/CurrencyInput/index.tsx +36 -12
  88. package/src/components/CurrencyInput/styles.ts +51 -4
  89. package/src/components/Insight/index.tsx +531 -0
  90. package/src/components/Insight/insetRadius.ts +30 -0
  91. package/src/components/Insight/styles.ts +193 -0
  92. package/src/components/InsightStack/index.tsx +86 -0
  93. package/src/components/InsightStack/styles.ts +61 -0
  94. package/src/components/index.ts +2 -0
@@ -0,0 +1,644 @@
1
+ # Insight (https://design.candescent.com/docs/components/insight)
2
+
3
+ Resources: [Figma](https://www.figma.com/design/0lEVMVlLj8ZS4fgItr3pmd/%E2%AD%90-Forge-DS--Components?node-id=13158-18419), [Storybook](https://victorious-wave-0daadf00f.6.azurestaticapps.net/?path=/story/surfaces-insight--default)
4
+
5
+ A compound component with two layouts. A `summary` Insight is a single line of copy beside a Sparkle medallion, usually linking to a list of insights. A `detail` Insight on that list adds a category tag, a title, free content, and actions. Insight renders the medallion and the tag chip itself.
6
+
7
+ ```tsx
8
+ import { Insight } from '@cdx-ui/components';
9
+ ```
10
+
11
+ ## Basic
12
+
13
+ `layout` defaults to `summary`. Set `onPress` to make the whole card one press region. `onPress` exists only on the summary layout, where it is a type error on `detail`, and it is a plain callback: navigating to the insights screen is up to your app.
14
+
15
+ ```tsx
16
+ import { Box, Insight } from '@cdx-ui/components';
17
+
18
+ export default function InsightBasicDemo() {
19
+ return (
20
+ <Box className="w-full max-w-md">
21
+ <Insight onPress={() => {}}>
22
+ <Insight.Description>
23
+ Your spending this month is 12% lower than last month.
24
+ </Insight.Description>
25
+ </Insight>
26
+ </Box>
27
+ );
28
+ }
29
+ ```
30
+
31
+ ## Detail
32
+
33
+ Set `layout="detail"` for a static card. Write the category in `Insight.Tag`, the title and description in `Insight.Header`, free content such as a chart in `Insight.Content`, and actions in `Insight.Footer`. `Insight.Content`, `Insight.Footer`, and `onClose` are optional. A detail Insight without `Insight.Tag` logs a development warning.
34
+
35
+ The tag overhangs the card's top edge. The detail layout adds a top margin to make room for it, so keep that space above the card and don't set `overflow-hidden` on the Insight or on a container that clips it.
36
+
37
+ ```tsx
38
+ import { Box, Button, Insight, ProgressBar, Text } from '@cdx-ui/components';
39
+
40
+ export default function InsightDetailDemo() {
41
+ return (
42
+ <Box className="w-full max-w-md">
43
+ <Insight layout="detail" onClose={() => {}}>
44
+ <Insight.Tag>Spending</Insight.Tag>
45
+ <Insight.Header>
46
+ <Insight.Title>Dining is trending higher</Insight.Title>
47
+ <Insight.Description>
48
+ You have spent $320.00 of your $400.00 dining budget, with 10 days left this month.
49
+ </Insight.Description>
50
+ </Insight.Header>
51
+ <Insight.Content className="gap-1.5">
52
+ <Text size="sm" color="secondary">
53
+ 80% of budget used
54
+ </Text>
55
+ <ProgressBar value={80} max={100} accessibilityLabel="Dining budget used">
56
+ <ProgressBar.Indicator />
57
+ </ProgressBar>
58
+ </Insight.Content>
59
+ <Insight.Footer>
60
+ <Button variant="outline" size="small">
61
+ <Button.Label>View transactions</Button.Label>
62
+ </Button>
63
+ <Button size="small">
64
+ <Button.Label>Adjust budget</Button.Label>
65
+ </Button>
66
+ </Insight.Footer>
67
+ </Insight>
68
+ </Box>
69
+ );
70
+ }
71
+ ```
72
+
73
+ ## Custom content
74
+
75
+ `Insight.Content` takes any layout, such as budget bars, a short list of bills, or goal figures. It adds no padding or styling, so the card's `gap-4` spaces it from the header and footer. Keep the content to a glance: link to a full screen for anything longer.
76
+
77
+ ```tsx
78
+ import { Box, Button, Divider, Insight, ProgressBar, Text } from '@cdx-ui/components';
79
+
80
+ const categories = [
81
+ { name: 'Groceries', spent: '$412.30', percent: 82 },
82
+ { name: 'Dining', spent: '$268.75', percent: 54 },
83
+ { name: 'Transport', spent: '$96.40', percent: 19 },
84
+ ];
85
+
86
+ const bills = [
87
+ { name: 'Electric', due: 'Due Oct 17', amount: '$96.20' },
88
+ { name: 'Phone', due: 'Due Oct 18', amount: '$65.00' },
89
+ { name: 'Car insurance', due: 'Due Oct 20', amount: '$251.28' },
90
+ ];
91
+
92
+ export default function InsightCustomContentDemo() {
93
+ return (
94
+ <Box className="w-full max-w-md gap-6">
95
+ <Insight layout="detail">
96
+ <Insight.Tag>Spending</Insight.Tag>
97
+ <Insight.Header>
98
+ <Insight.Title>Your top categories this month</Insight.Title>
99
+ <Insight.Description>Groceries make up almost half of your spending.</Insight.Description>
100
+ </Insight.Header>
101
+ <Insight.Content className="gap-3">
102
+ {categories.map((category) => (
103
+ <Box key={category.name} className="gap-1.5">
104
+ <Box className="flex-row justify-between">
105
+ <Text size="sm">{category.name}</Text>
106
+ <Text size="sm" weight="semibold">
107
+ {category.spent}
108
+ </Text>
109
+ </Box>
110
+ <ProgressBar
111
+ value={category.percent}
112
+ max={100}
113
+ accessibilityLabel={`${category.name} budget used`}
114
+ >
115
+ <ProgressBar.Indicator />
116
+ </ProgressBar>
117
+ </Box>
118
+ ))}
119
+ </Insight.Content>
120
+ <Insight.Footer>
121
+ <Button variant="outline" size="small">
122
+ <Button.Label>See all spending</Button.Label>
123
+ </Button>
124
+ </Insight.Footer>
125
+ </Insight>
126
+ <Insight layout="detail" color="warning" onClose={() => {}}>
127
+ <Insight.Tag>Bills</Insight.Tag>
128
+ <Insight.Header>
129
+ <Insight.Title>3 bills are due this week</Insight.Title>
130
+ <Insight.Description>
131
+ They total $412.48, and your checking balance is $380.12.
132
+ </Insight.Description>
133
+ </Insight.Header>
134
+ <Insight.Content>
135
+ {bills.map((bill, index) => (
136
+ <Box key={bill.name}>
137
+ {index > 0 ? <Divider /> : null}
138
+ <Box className="flex-row items-center justify-between py-2">
139
+ <Box>
140
+ <Text size="sm" weight="medium">
141
+ {bill.name}
142
+ </Text>
143
+ <Text size="xs" color="secondary">
144
+ {bill.due}
145
+ </Text>
146
+ </Box>
147
+ <Text size="sm" weight="semibold">
148
+ {bill.amount}
149
+ </Text>
150
+ </Box>
151
+ </Box>
152
+ ))}
153
+ </Insight.Content>
154
+ <Insight.Footer>
155
+ <Button size="small">
156
+ <Button.Label>Transfer funds</Button.Label>
157
+ </Button>
158
+ </Insight.Footer>
159
+ </Insight>
160
+ <Insight layout="detail" aiGenerated>
161
+ <Insight.Tag>Goals</Insight.Tag>
162
+ <Insight.Header>
163
+ <Insight.Title>Your vacation fund is 64% there</Insight.Title>
164
+ <Insight.Description>
165
+ At your current pace, you will reach your goal three weeks early.
166
+ </Insight.Description>
167
+ </Insight.Header>
168
+ <Insight.Content className="gap-2">
169
+ <Box className="flex-row justify-between">
170
+ <Box>
171
+ <Text size="xs" color="secondary">
172
+ Saved
173
+ </Text>
174
+ <Text size="lg" weight="semibold">
175
+ $1,920.00
176
+ </Text>
177
+ </Box>
178
+ <Box className="items-end">
179
+ <Text size="xs" color="secondary">
180
+ Goal
181
+ </Text>
182
+ <Text size="lg" weight="semibold">
183
+ $3,000.00
184
+ </Text>
185
+ </Box>
186
+ </Box>
187
+ <ProgressBar value={64} max={100} accessibilityLabel="Vacation fund progress">
188
+ <ProgressBar.Indicator />
189
+ </ProgressBar>
190
+ </Insight.Content>
191
+ </Insight>
192
+ </Box>
193
+ );
194
+ }
195
+ ```
196
+
197
+ ## Colors
198
+
199
+ `color` tints the border, the medallion, and the tag. It is visual only, so any meaning it carries, such as a warning, must also be in the copy. FIs theme the border through the `stroke-info-subtle`, `stroke-warning-subtle`, and `stroke-danger-subtle` tokens. These tokens use the light values in dark mode until design supplies dark ones.
200
+
201
+ ```tsx
202
+ import { Box, Insight } from '@cdx-ui/components';
203
+
204
+ const examples = [
205
+ {
206
+ color: 'info',
207
+ tag: 'Income',
208
+ title: 'Your paycheck arrived early',
209
+ summary: 'Your paycheck usually arrives on Fridays.',
210
+ detail: '$2,450.00 was deposited to checking on Thursday.',
211
+ },
212
+ {
213
+ color: 'warning',
214
+ tag: 'Cash flow',
215
+ title: 'Your balance may run low',
216
+ summary: 'Your checking balance may run low before your next deposit.',
217
+ detail: 'Upcoming bills could bring checking below $50.00 before Friday.',
218
+ },
219
+ {
220
+ color: 'danger',
221
+ tag: 'Payment overdue',
222
+ title: 'Your card payment is overdue',
223
+ summary: 'Your credit card payment of $85.00 is overdue.',
224
+ detail: 'Pay $85.00 today to avoid a late fee.',
225
+ },
226
+ ] as const;
227
+
228
+ export default function InsightColorsDemo() {
229
+ return (
230
+ <Box className="w-full max-w-md gap-6">
231
+ <Box className="gap-3">
232
+ {examples.map((example) => (
233
+ <Insight key={example.color} color={example.color} onPress={() => {}}>
234
+ <Insight.Description>{example.summary}</Insight.Description>
235
+ </Insight>
236
+ ))}
237
+ </Box>
238
+ <Box className="gap-4">
239
+ {examples.map((example) => (
240
+ <Insight key={example.color} layout="detail" color={example.color}>
241
+ <Insight.Tag>{example.tag}</Insight.Tag>
242
+ <Insight.Header>
243
+ <Insight.Title>{example.title}</Insight.Title>
244
+ <Insight.Description>{example.detail}</Insight.Description>
245
+ </Insight.Header>
246
+ </Insight>
247
+ ))}
248
+ </Box>
249
+ </Box>
250
+ );
251
+ }
252
+ ```
253
+
254
+ ## AI-generated
255
+
256
+ `aiGenerated` swaps the `color` border for a left-to-right gradient with a soft glow. The gradient does not mirror in right-to-left layouts. FIs theme its two stops through the `stroke-ai-start` and `stroke-ai-end` tokens.
257
+
258
+ ```tsx
259
+ import { Box, Button, Insight } from '@cdx-ui/components';
260
+
261
+ export default function InsightAiGeneratedDemo() {
262
+ return (
263
+ <Box className="w-full max-w-md gap-4">
264
+ <Insight aiGenerated onPress={() => {}}>
265
+ <Insight.Description>
266
+ You could save $45.00 a month by moving two subscriptions to annual billing.
267
+ </Insight.Description>
268
+ </Insight>
269
+ <Insight aiGenerated color="warning" onPress={() => {}}>
270
+ <Insight.Description>
271
+ Your utility bills are 30% higher than this time last year.
272
+ </Insight.Description>
273
+ </Insight>
274
+ <Insight layout="detail" aiGenerated onClose={() => {}}>
275
+ <Insight.Tag>Savings</Insight.Tag>
276
+ <Insight.Header>
277
+ <Insight.Title>Grow your emergency fund</Insight.Title>
278
+ <Insight.Description>
279
+ Setting aside $150.00 each payday would reach your savings goal by June.
280
+ </Insight.Description>
281
+ </Insight.Header>
282
+ <Insight.Footer>
283
+ <Button size="small">
284
+ <Button.Label>Set up transfer</Button.Label>
285
+ </Button>
286
+ </Insight.Footer>
287
+ </Insight>
288
+ </Box>
289
+ );
290
+ }
291
+ ```
292
+
293
+ ## Dismissible
294
+
295
+ Set `onClose` to add a dismiss button at the top-end corner. On a pressable summary it sits beside the press region, not inside it. Insight does not remove itself: remove the item from your data in `onClose`, and move focus as described under Accessibility. Here, dismissing removes the insight from the list and moves focus to the next insight, or to the button after the list when none is left.
296
+
297
+ ```tsx
298
+ import { useRef, useState, type ComponentRef } from 'react';
299
+ import { Box, Button, Insight } from '@cdx-ui/components';
300
+
301
+ const insights = [
302
+ { id: 'subscriptions', copy: 'You have three subscriptions renewing in the next seven days.' },
303
+ { id: 'dining', copy: 'Your dining spending is 20% higher than last month.' },
304
+ { id: 'savings', copy: 'You could save $45.00 a month with annual billing.' },
305
+ ];
306
+
307
+ export default function InsightDismissibleDemo() {
308
+ const [visible, setVisible] = useState(insights);
309
+ const insightRefs = useRef(new Map<string, ComponentRef<typeof Insight>>());
310
+ const restoreRef = useRef<ComponentRef<typeof Button>>(null);
311
+
312
+ const dismiss = (id: string) => {
313
+ const index = visible.findIndex((insight) => insight.id === id);
314
+ const next = visible[index + 1] ?? visible[index - 1];
315
+ setVisible((current) => current.filter((insight) => insight.id !== id));
316
+ if (next) {
317
+ insightRefs.current.get(next.id)?.focus();
318
+ } else {
319
+ restoreRef.current?.focus();
320
+ }
321
+ };
322
+
323
+ return (
324
+ <Box className="w-full max-w-md gap-3">
325
+ {visible.map((insight) => (
326
+ <Insight
327
+ key={insight.id}
328
+ ref={(node) => {
329
+ if (node) {
330
+ insightRefs.current.set(insight.id, node);
331
+ } else {
332
+ insightRefs.current.delete(insight.id);
333
+ }
334
+ }}
335
+ onPress={() => {}}
336
+ onClose={() => dismiss(insight.id)}
337
+ >
338
+ <Insight.Description>{insight.copy}</Insight.Description>
339
+ </Insight>
340
+ ))}
341
+ <Box className="items-start">
342
+ <Button
343
+ ref={restoreRef}
344
+ variant="outline"
345
+ size="small"
346
+ onPress={() => setVisible(insights)}
347
+ >
348
+ <Button.Label>Restore insights</Button.Label>
349
+ </Button>
350
+ </Box>
351
+ </Box>
352
+ );
353
+ }
354
+ ```
355
+
356
+ ## Text wrapping
357
+
358
+ Long copy wraps and the card grows downward, while the medallion stays aligned with the first line. With `onClose`, the text leaves room for the dismiss button. Set `numberOfLines` on `Insight.Description` to truncate, and pass the full copy as the Insight's `accessibilityLabel` so screen readers still read all of it.
359
+
360
+ ```tsx
361
+ import { Box, Insight } from '@cdx-ui/components';
362
+
363
+ const longCopy =
364
+ 'You spent $1,240.00 on dining out over the last three months, which is $310.00 more than the three months before and the highest it has been this year.';
365
+
366
+ export default function InsightTextWrappingDemo() {
367
+ return (
368
+ <Box className="w-full max-w-md gap-4">
369
+ <Insight onPress={() => {}}>
370
+ <Insight.Description>{longCopy}</Insight.Description>
371
+ </Insight>
372
+ <Insight onPress={() => {}} onClose={() => {}}>
373
+ <Insight.Description>{longCopy}</Insight.Description>
374
+ </Insight>
375
+ <Insight onPress={() => {}} accessibilityLabel={longCopy}>
376
+ <Insight.Description numberOfLines={2}>{longCopy}</Insight.Description>
377
+ </Insight>
378
+ <Insight layout="detail" onClose={() => {}}>
379
+ <Insight.Tag>Spending</Insight.Tag>
380
+ <Insight.Header>
381
+ <Insight.Title>
382
+ Dining out is your fastest-growing category for the third month in a row
383
+ </Insight.Title>
384
+ <Insight.Description>{longCopy}</Insight.Description>
385
+ </Insight.Header>
386
+ </Insight>
387
+ </Box>
388
+ );
389
+ }
390
+ ```
391
+
392
+ ## List
393
+
394
+ The screen a summary Insight links to lists detail Insights, mixing colors, AI-generated cards, and cards with or without content and actions. The detail layout's top margin already makes room for each tag, so a plain `gap` between cards is enough.
395
+
396
+ ```tsx
397
+ import { Box, Button, Heading, Insight, ProgressBar, Text } from '@cdx-ui/components';
398
+
399
+ export default function InsightListDemo() {
400
+ return (
401
+ <Box className="w-full max-w-md gap-4">
402
+ <Box className="gap-0.5">
403
+ <Heading size="md">Insights</Heading>
404
+ <Text size="sm" color="secondary">
405
+ Updated today at 8:00 AM
406
+ </Text>
407
+ </Box>
408
+ <Insight layout="detail" aiGenerated>
409
+ <Insight.Tag>Subscriptions</Insight.Tag>
410
+ <Insight.Header>
411
+ <Insight.Title>Save $45.00 a month</Insight.Title>
412
+ <Insight.Description>
413
+ Two of your streaming subscriptions offer a cheaper annual plan.
414
+ </Insight.Description>
415
+ </Insight.Header>
416
+ <Insight.Footer>
417
+ <Button variant="ghost" size="small">
418
+ <Button.Label>Not now</Button.Label>
419
+ </Button>
420
+ <Button size="small">
421
+ <Button.Label>Review plans</Button.Label>
422
+ </Button>
423
+ </Insight.Footer>
424
+ </Insight>
425
+ <Insight layout="detail" color="warning">
426
+ <Insight.Tag>Cash flow</Insight.Tag>
427
+ <Insight.Header>
428
+ <Insight.Title>Your balance may run low</Insight.Title>
429
+ <Insight.Description>
430
+ Upcoming bills could bring checking below $50.00 before your Friday deposit.
431
+ </Insight.Description>
432
+ </Insight.Header>
433
+ <Insight.Footer>
434
+ <Button variant="outline" size="small">
435
+ <Button.Label>View forecast</Button.Label>
436
+ </Button>
437
+ </Insight.Footer>
438
+ </Insight>
439
+ <Insight layout="detail">
440
+ <Insight.Tag>Spending</Insight.Tag>
441
+ <Insight.Header>
442
+ <Insight.Title>Dining is trending higher</Insight.Title>
443
+ <Insight.Description>
444
+ You have spent $320.00 of your $400.00 dining budget, with 10 days left this month.
445
+ </Insight.Description>
446
+ </Insight.Header>
447
+ <Insight.Content>
448
+ <ProgressBar value={80} max={100} accessibilityLabel="Dining budget used">
449
+ <ProgressBar.Indicator />
450
+ </ProgressBar>
451
+ </Insight.Content>
452
+ </Insight>
453
+ <Insight layout="detail">
454
+ <Insight.Tag>Income</Insight.Tag>
455
+ <Insight.Header>
456
+ <Insight.Title>Your paycheck arrived early</Insight.Title>
457
+ <Insight.Description>
458
+ $2,450.00 was deposited to checking on Thursday.
459
+ </Insight.Description>
460
+ </Insight.Header>
461
+ </Insight>
462
+ </Box>
463
+ );
464
+ }
465
+ ```
466
+
467
+ ## States
468
+
469
+ A summary without `onPress` is static and has no role. With `onPress`, it scales on press and shows a focus ring on keyboard focus on web. `isDisabled` dims the whole card and suppresses `onPress`; it does not disable the dismiss button.
470
+
471
+ ```tsx
472
+ import { Box, Insight, Text } from '@cdx-ui/components';
473
+
474
+ export default function InsightStatesDemo() {
475
+ return (
476
+ <Box className="w-full max-w-md gap-4">
477
+ <Box className="gap-2">
478
+ <Text size="sm" color="secondary">
479
+ Static
480
+ </Text>
481
+ <Insight>
482
+ <Insight.Description>Your paycheck usually arrives on Fridays.</Insight.Description>
483
+ </Insight>
484
+ </Box>
485
+ <Box className="gap-2">
486
+ <Text size="sm" color="secondary">
487
+ Pressable
488
+ </Text>
489
+ <Insight onPress={() => {}}>
490
+ <Insight.Description>
491
+ Your spending this month is 12% lower than last month.
492
+ </Insight.Description>
493
+ </Insight>
494
+ </Box>
495
+ <Box className="gap-2">
496
+ <Text size="sm" color="secondary">
497
+ Disabled
498
+ </Text>
499
+ <Insight onPress={() => {}} isDisabled>
500
+ <Insight.Description>
501
+ Insights are unavailable while your accounts sync.
502
+ </Insight.Description>
503
+ </Insight>
504
+ </Box>
505
+ </Box>
506
+ );
507
+ }
508
+ ```
509
+
510
+ ## API
511
+
512
+ ### Insight
513
+
514
+ Root card. `className` and `style` apply to the outer frame that paints the border. `layout` defaults to `summary` and is a discriminated union: `onPress`, `isDisabled`, `onPressIn`, `onPressOut`, `onHoverIn`, and `onHoverOut` type-check only with `layout="summary"`.
515
+
516
+ | Prop | Type | Default | Description |
517
+ | --- | --- | --- | --- |
518
+ | `className?` | `string` | — | — |
519
+ | `children?` | `ReactNode` | — | — |
520
+ | `onPressIn?` | `((event: GestureResponderEvent) => void) \| null` | — | Called when a touch is engaged before `onPress`. |
521
+ | `onPressOut?` | `((event: GestureResponderEvent) => void) \| null` | — | Called when a touch is released before `onPress`. |
522
+ | `onHoverIn?` | `((event: MouseEvent) => void) \| null` | — | Called when the hover is activated to provide visual feedback. |
523
+ | `onHoverOut?` | `((event: MouseEvent) => void) \| null` | — | Called when the hover is deactivated to undo visual feedback. |
524
+ | `onPress?` | `((event: GestureResponderEvent) => void) \| null` | — | When set, the root renders a press region with `role="button"`. |
525
+ | `isDisabled?` | `boolean` | — | Suppresses `onPress` and marks the press region disabled. |
526
+ | `asChild?` | `boolean` | — | Merge props onto a single child instead of rendering `View` or `Pressable`. |
527
+ | `color?` | `'info' \| 'warning' \| 'danger'` | `'info'` | Tints the border, the summary medallion, and the detail tag. Visual only: any meaning it carries must also be in the text. |
528
+ | `aiGenerated?` | `boolean` | `false` | Swaps the `color` border for the AI gradient border. |
529
+ | `onClose?` | `(() => void)` | — | Renders a dismiss button at the top-end corner. In `summary`, it sits beside the press region, not inside it. |
530
+ | `closeAccessibilityLabel?` | `string` | `'Dismiss insight'` | Accessible name for the dismiss button. |
531
+ | `layout?` | `'summary' \| 'detail'` | — | — |
532
+
533
+ Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
534
+
535
+ Pressing a summary Insight with `onPress` scales the card to 0.98 and back. The scale is skipped when the device's reduce motion setting is on, or when `ForgeDesignProvider` sets `animation: 'disable-all'`.
536
+
537
+ | Attribute | Values | Description |
538
+ | -------------------- | ----------------- | ------------------------------------ |
539
+ | `data-hover` | `true` \| `false` | Hover (web, when pressable) |
540
+ | `data-active` | `true` \| `false` | Pressed (when pressable) |
541
+ | `data-focus` | `true` \| `false` | Focused (when pressable) |
542
+ | `data-focus-visible` | `true` \| `false` | Keyboard focus (web, when pressable) |
543
+ | `data-disabled` | `true` \| `false` | Disabled (when pressable) |
544
+
545
+ ## Sub-components
546
+
547
+ ### Insight.Tag
548
+
549
+ The detail layout's category text. Insight renders it as a Sparkle chip on the card's top edge, tinted by `color`, wherever it appears among the children.
550
+
551
+ | Prop | Type | Default | Description |
552
+ | --- | --- | --- | --- |
553
+ | `className?` | `string` | — | Classes for the positioned tag wrapper. |
554
+ | `children?` | `ReactNode` | — | The insight's category, read before the title. |
555
+ | `asChild?` | `boolean` | — | Merges the label props onto a single text child instead of rendering `Text`. |
556
+
557
+ Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
558
+
559
+ ### Insight.Header
560
+
561
+ Groups the title and description in the detail layout.
562
+
563
+ | Prop | Type | Default | Description |
564
+ | --- | --- | --- | --- |
565
+ | `className?` | `string` | — | — |
566
+ | `children?` | `ReactNode` | — | — |
567
+ | `asChild?` | `boolean` | — | — |
568
+
569
+ Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
570
+
571
+ ### Insight.Title
572
+
573
+ Heading text. Leaves room for the dismiss button when `onClose` is set.
574
+
575
+ | Prop | Type | Default | Description |
576
+ | --- | --- | --- | --- |
577
+ | `className?` | `string` | — | — |
578
+ | `children?` | `ReactNode` | — | — |
579
+ | `asChild?` | `boolean` | — | — |
580
+
581
+ Inherits all [`TextProps`](https://reactnative.dev/docs/text#props).
582
+
583
+ ### Insight.Description
584
+
585
+ Body text. In the summary layout it is the card's copy. Insight does not truncate it; set `numberOfLines` if you need to.
586
+
587
+ | Prop | Type | Default | Description |
588
+ | --- | --- | --- | --- |
589
+ | `className?` | `string` | — | — |
590
+ | `children?` | `ReactNode` | — | — |
591
+ | `asChild?` | `boolean` | — | — |
592
+
593
+ Inherits all [`TextProps`](https://reactnative.dev/docs/text#props).
594
+
595
+ ### Insight.Content
596
+
597
+ Free content slot, such as a chart, in the detail layout. It adds no styling of its own.
598
+
599
+ | Prop | Type | Default | Description |
600
+ | --- | --- | --- | --- |
601
+ | `className?` | `string` | — | — |
602
+ | `children?` | `ReactNode` | — | — |
603
+ | `asChild?` | `boolean` | — | — |
604
+
605
+ Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
606
+
607
+ ### Insight.Footer
608
+
609
+ End-aligned row of actions. Buttons keep their natural width, and Insight does not pass its `color` to them.
610
+
611
+ | Prop | Type | Default | Description |
612
+ | --- | --- | --- | --- |
613
+ | `className?` | `string` | — | — |
614
+ | `children?` | `ReactNode` | — | — |
615
+ | `asChild?` | `boolean` | — | — |
616
+
617
+ Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
618
+
619
+ ## Anatomy
620
+
621
+ | `data-slot` | Element |
622
+ | --------------------- | ------------------------------------------------ |
623
+ | `insight-frame` | Outer frame that paints the border |
624
+ | `insight` | Card surface; carries the interaction attributes |
625
+ | `insight-icon` | Summary Sparkle medallion (internal, decorative) |
626
+ | `insight-tag` | Detail category chip |
627
+ | `insight-header` | Title and description group |
628
+ | `insight-title` | Title |
629
+ | `insight-description` | Description |
630
+ | `insight-content` | Free content |
631
+ | `insight-footer` | Actions row |
632
+ | `insight-close` | Dismiss button, rendered when `onClose` is set |
633
+
634
+ ## Accessibility
635
+
636
+ | Attribute | Behavior |
637
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
638
+ | **Summary** | With `onPress`, the press region has the button role and takes its name from its text. Set `accessibilityLabel` when the text alone doesn't describe it. The Sparkle medallion is hidden from assistive tech. |
639
+ | **Detail** | No root role. `Insight.Title` has the heading role. `Insight.Tag` is plain text and is always read before the title, wherever it is written among the children. |
640
+ | **Dismiss button** | A separate focus stop. On a pressable summary it follows the press region and is never nested inside it; on a detail card it follows the content. Its label defaults to `Dismiss insight`; set `closeAccessibilityLabel` to change it. |
641
+ | **Dismissing** | Consumer-owned. Insight calls `onClose` and does not remove itself. After you remove the item, move focus to a sensible target, such as the next insight or the list heading, or it is lost. On a summary with `onPress`, `ref` points at the press region, so calling `focus()` on the next insight's `ref` moves focus to it. On a static Insight, `ref` points at its non-focusable surface. |
642
+ | **Live region** | None. Insights render with the screen, often several at once, so none of them is announced. Use [`Alert`](https://design.candescent.com/docs/components/alert/content.md) or [`Toast`](https://design.candescent.com/docs/components/toast/content.md) for messages that must be announced. |
643
+ | **`color`** | Visual only. Any meaning it carries must also be in the tag, title, or description. |
644
+ | **`aiGenerated`** | Visual only. It changes the border and adds nothing to the accessibility tree. |