@cdx-ui/components 0.3.2 → 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 (66) hide show
  1. package/README.md +2 -2
  2. package/coverage.svg +1 -1
  3. package/docs/components/content.md +2 -0
  4. package/docs/components/data-grid/content.md +125 -1
  5. package/docs/components/insight/content.md +644 -0
  6. package/docs/components/insight-stack/content.md +231 -0
  7. package/docs/foundations/styles/tokens/all-tokens/content.md +23 -3
  8. package/docs/llms.txt +3 -0
  9. package/docs/patterns/forms/content.md +255 -0
  10. package/docs/resources/release-notes/0.3.2/content.md +17 -0
  11. package/docs/resources/release-notes/content.md +1 -0
  12. package/lib/commonjs/components/Insight/index.js +438 -0
  13. package/lib/commonjs/components/Insight/index.js.map +1 -0
  14. package/lib/commonjs/components/Insight/insetRadius.js +35 -0
  15. package/lib/commonjs/components/Insight/insetRadius.js.map +1 -0
  16. package/lib/commonjs/components/Insight/styles.js +186 -0
  17. package/lib/commonjs/components/Insight/styles.js.map +1 -0
  18. package/lib/commonjs/components/InsightStack/index.js +93 -0
  19. package/lib/commonjs/components/InsightStack/index.js.map +1 -0
  20. package/lib/commonjs/components/InsightStack/styles.js +61 -0
  21. package/lib/commonjs/components/InsightStack/styles.js.map +1 -0
  22. package/lib/commonjs/components/index.js +24 -0
  23. package/lib/commonjs/components/index.js.map +1 -1
  24. package/lib/commonjs/figma/Insight.figma.js +73 -0
  25. package/lib/commonjs/figma/Insight.figma.js.map +1 -0
  26. package/lib/commonjs/figma/InsightStack.figma.js +39 -0
  27. package/lib/commonjs/figma/InsightStack.figma.js.map +1 -0
  28. package/lib/module/components/Insight/index.js +433 -0
  29. package/lib/module/components/Insight/index.js.map +1 -0
  30. package/lib/module/components/Insight/insetRadius.js +30 -0
  31. package/lib/module/components/Insight/insetRadius.js.map +1 -0
  32. package/lib/module/components/Insight/styles.js +183 -0
  33. package/lib/module/components/Insight/styles.js.map +1 -0
  34. package/lib/module/components/InsightStack/index.js +89 -0
  35. package/lib/module/components/InsightStack/index.js.map +1 -0
  36. package/lib/module/components/InsightStack/styles.js +58 -0
  37. package/lib/module/components/InsightStack/styles.js.map +1 -0
  38. package/lib/module/components/index.js +2 -0
  39. package/lib/module/components/index.js.map +1 -1
  40. package/lib/module/figma/Insight.figma.js +67 -0
  41. package/lib/module/figma/Insight.figma.js.map +1 -0
  42. package/lib/module/figma/InsightStack.figma.js +33 -0
  43. package/lib/module/figma/InsightStack.figma.js.map +1 -0
  44. package/lib/typescript/components/Insight/index.d.ts +87 -0
  45. package/lib/typescript/components/Insight/index.d.ts.map +1 -0
  46. package/lib/typescript/components/Insight/insetRadius.d.ts +8 -0
  47. package/lib/typescript/components/Insight/insetRadius.d.ts.map +1 -0
  48. package/lib/typescript/components/Insight/styles.d.ts +33 -0
  49. package/lib/typescript/components/Insight/styles.d.ts.map +1 -0
  50. package/lib/typescript/components/InsightStack/index.d.ts +21 -0
  51. package/lib/typescript/components/InsightStack/index.d.ts.map +1 -0
  52. package/lib/typescript/components/InsightStack/styles.d.ts +11 -0
  53. package/lib/typescript/components/InsightStack/styles.d.ts.map +1 -0
  54. package/lib/typescript/components/index.d.ts +2 -0
  55. package/lib/typescript/components/index.d.ts.map +1 -1
  56. package/lib/typescript/figma/Insight.figma.d.ts +8 -0
  57. package/lib/typescript/figma/Insight.figma.d.ts.map +1 -0
  58. package/lib/typescript/figma/InsightStack.figma.d.ts +8 -0
  59. package/lib/typescript/figma/InsightStack.figma.d.ts.map +1 -0
  60. package/package.json +5 -5
  61. package/src/components/Insight/index.tsx +531 -0
  62. package/src/components/Insight/insetRadius.ts +30 -0
  63. package/src/components/Insight/styles.ts +193 -0
  64. package/src/components/InsightStack/index.tsx +86 -0
  65. package/src/components/InsightStack/styles.ts +61 -0
  66. package/src/components/index.ts +2 -0
@@ -0,0 +1,21 @@
1
+ import { type ReactNode } from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ export interface InsightStackProps extends ViewProps {
4
+ /**
5
+ * How many insights the stack stands for. `1` or less draws no layers, `2` draws one, and `3`
6
+ * or more draws two. The layers are decorative, so put the count in the front Insight's
7
+ * accessible label or hint.
8
+ */
9
+ count: number;
10
+ /** Merges the root props onto a single child element instead of rendering a `View`. */
11
+ asChild?: boolean;
12
+ className?: string;
13
+ /** The front card, usually a summary `Insight`. */
14
+ children?: ReactNode;
15
+ }
16
+ /**
17
+ * Draws up to two decorative cards behind a summary Insight to show that more insights exist.
18
+ * The root adds no role, so the front Insight stays the only interactive element.
19
+ */
20
+ export declare const InsightStack: import("react").ForwardRefExoticComponent<InsightStackProps & import("react").RefAttributes<View>>;
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/InsightStack/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA+D,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACpG,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAapD,MAAM,WAAW,iBAAkB,SAAQ,SAAS;IAClD;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,uFAAuF;IACvF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAwBD;;;GAGG;AACH,eAAO,MAAM,YAAY,oGA6BxB,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { type VariantProps } from '@cdx-ui/utils';
2
+ export declare const insightStackRootVariants: (props?: ({
3
+ layers?: 0 | 1 | 2 | null | undefined;
4
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
5
+ export declare const insightStackFrontVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
6
+ export declare const insightStackLayerVariants: (props?: ({
7
+ depth?: 1 | 2 | null | undefined;
8
+ layers?: 1 | 2 | null | undefined;
9
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
10
+ export type InsightStackLayerVariantProps = VariantProps<typeof insightStackLayerVariants>;
11
+ //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../../src/components/InsightStack/styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAKvD,eAAO,MAAM,wBAAwB;;8EAWnC,CAAC;AAKH,eAAO,MAAM,yBAAyB,oFAAyB,CAAC;AAShE,eAAO,MAAM,yBAAyB;;;8EA4BrC,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG,YAAY,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
@@ -34,6 +34,8 @@ export * from './Field';
34
34
  export * from './Form';
35
35
  export * from './FullScreenDialog';
36
36
  export * from './Image';
37
+ export * from './Insight';
38
+ export * from './InsightStack';
37
39
  export * from './InsuranceDisclosure';
38
40
  export * from './Input';
39
41
  export * from './OtpInput';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,OAAO,CAAC;AACtB,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACjG,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,oBAAoB,CAAC;AACnC,cAAc,SAAS,CAAC;AACxB,cAAc,uBAAuB,CAAC;AACtC,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,mBAAmB,CAAC;AAClC,YAAY,EACV,qBAAqB,EACrB,oBAAoB,EACpB,6BAA6B,GAC9B,MAAM,yBAAyB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACzC,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,OAAO,CAAC;AACtB,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACjG,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,oBAAoB,CAAC;AACnC,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,mBAAmB,CAAC;AAClC,YAAY,EACV,qBAAqB,EACrB,oBAAoB,EACpB,6BAA6B,GAC9B,MAAM,yBAAyB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACzC,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC"}
@@ -0,0 +1,8 @@
1
+ import figma from 'figma';
2
+ declare const _default: {
3
+ id: string;
4
+ imports: string[];
5
+ example: figma.TemplateStringResult;
6
+ };
7
+ export default _default;
8
+ //# sourceMappingURL=Insight.figma.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Insight.figma.d.ts","sourceRoot":"","sources":["../../../src/figma/Insight.figma.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,OAAO,CAAC;;;;;;AAsD1B,wBAwBE"}
@@ -0,0 +1,8 @@
1
+ import figma from 'figma';
2
+ declare const _default: {
3
+ id: string;
4
+ imports: string[];
5
+ example: figma.TemplateStringResult;
6
+ };
7
+ export default _default;
8
+ //# sourceMappingURL=InsightStack.figma.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InsightStack.figma.d.ts","sourceRoot":"","sources":["../../../src/figma/InsightStack.figma.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,OAAO,CAAC;;;;;;AAqB1B,wBAWE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cdx-ui/components",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -106,10 +106,10 @@
106
106
  "@marceloterreiro/flash-calendar": "2.0.0",
107
107
  "react-native-reanimated-carousel": "5.0.0-beta.5",
108
108
  "uniwind": "1.10.0",
109
- "@cdx-ui/icons": "0.3.2",
110
- "@cdx-ui/formatters": "0.3.2",
111
- "@cdx-ui/primitives": "0.3.2",
112
- "@cdx-ui/utils": "0.3.2"
109
+ "@cdx-ui/formatters": "0.3.3",
110
+ "@cdx-ui/icons": "0.3.3",
111
+ "@cdx-ui/primitives": "0.3.3",
112
+ "@cdx-ui/utils": "0.3.3"
113
113
  },
114
114
  "devDependencies": {
115
115
  "@dnd-kit/core": "6.3.1",
@@ -0,0 +1,531 @@
1
+ import {
2
+ cloneElement,
3
+ forwardRef,
4
+ isValidElement,
5
+ useCallback,
6
+ useEffect,
7
+ useLayoutEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ type ReactElement,
12
+ type ReactNode,
13
+ type Ref,
14
+ } from 'react';
15
+ import { Platform, Text, View, type TextProps, type ViewProps } from 'react-native';
16
+ import Animated, {
17
+ useAnimatedStyle,
18
+ useSharedValue,
19
+ withTiming,
20
+ type SharedValue,
21
+ } from 'react-native-reanimated';
22
+ import { useCSSVariable } from 'uniwind';
23
+ import {
24
+ InsightRoot,
25
+ Slot,
26
+ dataAttributes,
27
+ mergeDataAttributes,
28
+ useInsightContext,
29
+ type IInsightDetailProps,
30
+ type IInsightProps,
31
+ type IInsightSummaryProps,
32
+ } from '@cdx-ui/primitives';
33
+ import { Close, Sparkle } from '@cdx-ui/icons';
34
+ import { cn, createContext, ParentContext, useParentContext, useStyleContext } from '@cdx-ui/utils';
35
+ import { Avatar } from '../Avatar';
36
+ import { Chip } from '../Chip';
37
+ import { IconButton } from '../IconButton';
38
+ import { useCombinedAnimationDisabledState } from '../../animation';
39
+ import { insetRadius } from './insetRadius';
40
+ import {
41
+ FRAME_WIDTH,
42
+ insightCloseVariants,
43
+ insightContentVariants,
44
+ insightDescriptionVariants,
45
+ insightFooterVariants,
46
+ insightFrameVariants,
47
+ insightHeaderVariants,
48
+ insightIconVariants,
49
+ insightPressRegionVariants,
50
+ insightSummaryTextVariants,
51
+ insightSurfaceVariants,
52
+ insightTagIconVariants,
53
+ insightTagVariants,
54
+ insightTitleVariants,
55
+ } from './styles';
56
+
57
+ const SCOPE = 'INSIGHT';
58
+
59
+ type InsightColor = 'info' | 'warning' | 'danger';
60
+
61
+ interface InsightStyleContextValue {
62
+ layout: 'summary' | 'detail';
63
+ color: InsightColor;
64
+ hasClose: boolean;
65
+ }
66
+
67
+ const useInsightStyleContext = () => useStyleContext(SCOPE) as unknown as InsightStyleContextValue;
68
+
69
+ type RegisteredInsightTag = InsightTagProps & { ref?: Ref<View> };
70
+
71
+ interface InsightTagRegistry {
72
+ /** `owner` identifies the Tag instance, so one Tag unmounting never clears another's registration. */
73
+ registerTag: (owner: object, tag: RegisteredInsightTag | null) => void;
74
+ }
75
+
76
+ const [InsightTagRegistryProvider, useInsightTagRegistry] =
77
+ createContext<InsightTagRegistry>('InsightTagRegistry');
78
+
79
+ let hasWarnedMissingTag = false;
80
+
81
+ const PRESSED_SCALE = 0.98;
82
+ const PRESS_SCALE_DURATION_MS = 120;
83
+
84
+ // =============================================================================
85
+ // ROOT
86
+ // =============================================================================
87
+
88
+ interface InsightStyleProps {
89
+ /**
90
+ * Tints the border, the summary medallion, and the detail tag. Visual only: any meaning it
91
+ * carries must also be in the text.
92
+ * @default 'info'
93
+ */
94
+ color?: 'info' | 'warning' | 'danger';
95
+ /**
96
+ * Swaps the `color` border for the AI gradient border.
97
+ * @default false
98
+ */
99
+ aiGenerated?: boolean;
100
+ /** Renders a dismiss button at the top-end corner. In `summary`, it sits beside the press region, not inside it. */
101
+ onClose?: () => void;
102
+ /**
103
+ * Accessible name for the dismiss button.
104
+ * @default 'Dismiss insight'
105
+ */
106
+ closeAccessibilityLabel?: string;
107
+ }
108
+
109
+ /**
110
+ * `className` and `style` style the outer frame (the border). `asChild` replaces the inner
111
+ * surface element. `onPress` exists only on `layout="summary"`.
112
+ *
113
+ * `ref` targets the element that takes focus: the press region of a summary with `onPress`
114
+ * (the surface, or the region beside the dismiss button when `onClose` is also set), so
115
+ * `ref.current?.focus()` moves focus to the card. A static Insight puts `ref` on its surface.
116
+ */
117
+ export type InsightProps =
118
+ | (Omit<IInsightSummaryProps, 'accessory' | 'pressableClassName'> & InsightStyleProps)
119
+ | (Omit<IInsightDetailProps, 'accessory' | 'pressableClassName'> & InsightStyleProps);
120
+
121
+ /**
122
+ * Press state lives in InsightRoot's context, but the scaled element is the frame around it, so
123
+ * this child of the root writes the frame's shared value.
124
+ */
125
+ function InsightPressScale({ scale }: { scale: SharedValue<number> }) {
126
+ const { isPressed, isPressable } = useInsightContext();
127
+ const isAllAnimationsDisabled = useCombinedAnimationDisabledState(undefined);
128
+
129
+ useEffect(() => {
130
+ if (!isPressable || isAllAnimationsDisabled) {
131
+ scale.set(1);
132
+ return;
133
+ }
134
+ scale.set(withTiming(isPressed ? PRESSED_SCALE : 1, { duration: PRESS_SCALE_DURATION_MS }));
135
+ }, [scale, isPressed, isPressable, isAllAnimationsDisabled]);
136
+
137
+ return null;
138
+ }
139
+
140
+ function SummaryBody({
141
+ hasClose,
142
+ scale,
143
+ children,
144
+ }: {
145
+ hasClose: boolean;
146
+ scale: SharedValue<number>;
147
+ children?: ReactNode;
148
+ }) {
149
+ const { color } = useInsightStyleContext();
150
+
151
+ return (
152
+ <>
153
+ <InsightPressScale scale={scale} />
154
+ <View
155
+ aria-hidden
156
+ accessibilityElementsHidden
157
+ importantForAccessibility="no-hide-descendants"
158
+ {...dataAttributes({ slot: 'insight-icon' })}
159
+ >
160
+ <Avatar size="md" variant="subtle" color={color}>
161
+ <Avatar.Icon as={Sparkle} className={insightIconVariants({ color })} />
162
+ </Avatar>
163
+ </View>
164
+ <View className={insightSummaryTextVariants({ hasClose })}>{children}</View>
165
+ </>
166
+ );
167
+ }
168
+
169
+ const InsightRootStyled = forwardRef<View, InsightProps>((props, ref) => {
170
+ const {
171
+ color = 'info',
172
+ aiGenerated = false,
173
+ onClose,
174
+ closeAccessibilityLabel = 'Dismiss insight',
175
+ asChild = false,
176
+ className,
177
+ style,
178
+ children,
179
+ ...rootProps
180
+ } = props;
181
+ const layout = props.layout ?? 'summary';
182
+ const onPress = props.layout !== 'detail' ? props.onPress : undefined;
183
+ const isDisabled = props.layout !== 'detail' && !!props.isDisabled;
184
+ const hasClose = !!onClose;
185
+ const asChildElement = asChild && isValidElement(children);
186
+ const isPressable = layout === 'summary' && !!onPress;
187
+ // Mirrors InsightRoot: an accessory splits a pressable or `asChild` root into a container
188
+ // and a press region.
189
+ const isSplit = hasClose && (isPressable || asChildElement);
190
+
191
+ // The frame scales, not the press region, so the border and the close button move with it.
192
+ const scale = useSharedValue(1);
193
+ // Explicit deps for Vite/web without the Reanimated Babel plugin.
194
+ const pressScaleStyle = useAnimatedStyle(
195
+ () => ({ transform: [{ scale: scale.get() }] }),
196
+ [scale],
197
+ );
198
+
199
+ const [tag, setTag] = useState<RegisteredInsightTag | null>(null);
200
+ const tagOwnerRef = useRef<object | null>(null);
201
+ const registerTag = useCallback((owner: object, next: RegisteredInsightTag | null) => {
202
+ if (next) {
203
+ tagOwnerRef.current = owner;
204
+ setTag(next);
205
+ } else if (tagOwnerRef.current === owner) {
206
+ tagOwnerRef.current = null;
207
+ setTag(null);
208
+ }
209
+ }, []);
210
+ const registry = useMemo(() => ({ registerTag }), [registerTag]);
211
+
212
+ useEffect(() => {
213
+ if (__DEV__ && layout === 'detail' && tagOwnerRef.current === null && !hasWarnedMissingTag) {
214
+ hasWarnedMissingTag = true;
215
+ console.warn(
216
+ '[Insight] A `layout="detail"` Insight needs an `Insight.Tag` child. The design has no tagless detail layout.',
217
+ );
218
+ }
219
+ }, [layout]);
220
+
221
+ // A runtime override reaches native as a `px` string, which a `calc()` class turns into NaN.
222
+ // Web keeps the `calc()` class, which tracks the root font size and scoped overrides.
223
+ const radiusVariable = useCSSVariable('--border-radius-default');
224
+ const surfaceRadius =
225
+ Platform.OS === 'web' ? undefined : insetRadius(radiusVariable, FRAME_WIDTH);
226
+ const surfaceStyle = useMemo(
227
+ () => (surfaceRadius === undefined ? undefined : { borderRadius: surfaceRadius }),
228
+ [surfaceRadius],
229
+ );
230
+
231
+ const parent = useParentContext();
232
+ const ctx = useMemo(
233
+ () => ({ ...parent, [SCOPE]: { layout, color, hasClose } }),
234
+ [parent, layout, color, hasClose],
235
+ );
236
+
237
+ let content: ReactNode = children;
238
+ if (layout === 'summary') {
239
+ content = asChildElement ? (
240
+ cloneElement(
241
+ children as ReactElement<{ children?: ReactNode }>,
242
+ undefined,
243
+ <SummaryBody hasClose={hasClose} scale={scale}>
244
+ {(children as ReactElement<{ children?: ReactNode }>).props.children}
245
+ </SummaryBody>,
246
+ )
247
+ ) : (
248
+ <SummaryBody hasClose={hasClose} scale={scale}>
249
+ {children}
250
+ </SummaryBody>
251
+ );
252
+ }
253
+
254
+ const closeButton = onClose ? (
255
+ <IconButton
256
+ as={Close}
257
+ variant="ghost"
258
+ size="small"
259
+ color="neutral"
260
+ onPress={onClose}
261
+ accessibilityLabel={closeAccessibilityLabel}
262
+ className={insightCloseVariants()}
263
+ {...dataAttributes({ slot: 'insight-close' })}
264
+ />
265
+ ) : undefined;
266
+
267
+ return (
268
+ <ParentContext.Provider value={ctx}>
269
+ <InsightTagRegistryProvider value={registry}>
270
+ <Animated.View
271
+ className={cn(insightFrameVariants({ layout, color, aiGenerated }), className)}
272
+ style={isPressable ? [style, pressScaleStyle] : style}
273
+ {...dataAttributes({
274
+ slot: 'insight-frame',
275
+ ...(isPressable && { disabled: isDisabled }),
276
+ })}
277
+ >
278
+ {layout === 'detail' && tag ? <InsightTagChip {...tag} /> : null}
279
+ <InsightRoot
280
+ {...(rootProps as IInsightProps)}
281
+ ref={ref}
282
+ asChild={asChild}
283
+ style={surfaceStyle}
284
+ accessory={closeButton}
285
+ className={insightSurfaceVariants({ layout, split: isSplit })}
286
+ pressableClassName={
287
+ isSplit && layout === 'summary' ? insightPressRegionVariants() : undefined
288
+ }
289
+ {...mergeDataAttributes(rootProps, dataAttributes({ slot: 'insight' }))}
290
+ >
291
+ {content}
292
+ </InsightRoot>
293
+ </Animated.View>
294
+ </InsightTagRegistryProvider>
295
+ </ParentContext.Provider>
296
+ );
297
+ });
298
+
299
+ InsightRootStyled.displayName = 'Insight';
300
+
301
+ // =============================================================================
302
+ // TAG
303
+ // =============================================================================
304
+
305
+ export interface InsightTagProps extends ViewProps {
306
+ /** Merges the label props onto a single text child instead of rendering `Text`. */
307
+ asChild?: boolean;
308
+ /** Classes for the positioned tag wrapper. */
309
+ className?: string;
310
+ /** The insight's category, read before the title. */
311
+ children?: ReactNode;
312
+ }
313
+
314
+ function InsightTagChip({
315
+ asChild,
316
+ className,
317
+ style,
318
+ children,
319
+ ref,
320
+ ...props
321
+ }: RegisteredInsightTag) {
322
+ const { color } = useInsightStyleContext();
323
+
324
+ return (
325
+ <View
326
+ ref={ref}
327
+ className={cn(insightTagVariants(), className)}
328
+ style={style}
329
+ {...props}
330
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'insight-tag' }))}
331
+ >
332
+ <Chip variant="subtle" size="small" color={color}>
333
+ <Chip.Icon as={Sparkle} position="leading" className={insightTagIconVariants({ color })} />
334
+ <Chip.Label asChild={asChild}>{children}</Chip.Label>
335
+ </Chip>
336
+ </View>
337
+ );
338
+ }
339
+
340
+ /**
341
+ * The detail layout's category tag. Insight renders it as a Sparkle chip straddling the top
342
+ * border, ahead of the header in reading order, wherever it appears among the children.
343
+ */
344
+ const InsightTag = forwardRef<View, InsightTagProps>((props, ref) => {
345
+ const { registerTag } = useInsightTagRegistry();
346
+ const [owner] = useState(() => ({}));
347
+
348
+ useLayoutEffect(() => {
349
+ registerTag(owner, { ...props, ref });
350
+ }, [registerTag, owner, props, ref]);
351
+
352
+ useLayoutEffect(() => () => registerTag(owner, null), [registerTag, owner]);
353
+
354
+ return null;
355
+ });
356
+
357
+ InsightTag.displayName = 'Insight.Tag';
358
+
359
+ // =============================================================================
360
+ // HEADER
361
+ // =============================================================================
362
+
363
+ export interface InsightHeaderProps extends ViewProps {
364
+ asChild?: boolean;
365
+ className?: string;
366
+ children?: ReactNode;
367
+ }
368
+
369
+ const InsightHeader = forwardRef<View, InsightHeaderProps>(
370
+ ({ asChild, className, children, style, ...props }, ref) => {
371
+ const Comp = asChild ? Slot : View;
372
+
373
+ return (
374
+ <Comp
375
+ ref={ref}
376
+ className={cn(insightHeaderVariants(), className)}
377
+ style={style}
378
+ {...props}
379
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'insight-header' }))}
380
+ >
381
+ {children}
382
+ </Comp>
383
+ );
384
+ },
385
+ );
386
+
387
+ InsightHeader.displayName = 'Insight.Header';
388
+
389
+ // =============================================================================
390
+ // TITLE
391
+ // =============================================================================
392
+
393
+ export interface InsightTitleProps extends TextProps {
394
+ asChild?: boolean;
395
+ className?: string;
396
+ children?: ReactNode;
397
+ }
398
+
399
+ const InsightTitle = forwardRef<Text, InsightTitleProps>(
400
+ ({ asChild, className, children, style, ...props }, ref) => {
401
+ const { hasClose } = useInsightStyleContext();
402
+ const Comp = asChild ? Slot : Text;
403
+
404
+ return (
405
+ <Comp
406
+ ref={ref}
407
+ role="heading"
408
+ className={cn(insightTitleVariants({ hasClose }), className)}
409
+ style={style}
410
+ {...props}
411
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'insight-title' }))}
412
+ >
413
+ {children}
414
+ </Comp>
415
+ );
416
+ },
417
+ );
418
+
419
+ InsightTitle.displayName = 'Insight.Title';
420
+
421
+ // =============================================================================
422
+ // DESCRIPTION
423
+ // =============================================================================
424
+
425
+ export interface InsightDescriptionProps extends TextProps {
426
+ asChild?: boolean;
427
+ className?: string;
428
+ children?: ReactNode;
429
+ }
430
+
431
+ const InsightDescription = forwardRef<Text, InsightDescriptionProps>(
432
+ ({ asChild, className, children, style, ...props }, ref) => {
433
+ const Comp = asChild ? Slot : Text;
434
+
435
+ return (
436
+ <Comp
437
+ ref={ref}
438
+ className={cn(insightDescriptionVariants(), className)}
439
+ style={style}
440
+ {...props}
441
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'insight-description' }))}
442
+ >
443
+ {children}
444
+ </Comp>
445
+ );
446
+ },
447
+ );
448
+
449
+ InsightDescription.displayName = 'Insight.Description';
450
+
451
+ // =============================================================================
452
+ // CONTENT
453
+ // =============================================================================
454
+
455
+ export interface InsightContentProps extends ViewProps {
456
+ asChild?: boolean;
457
+ className?: string;
458
+ children?: ReactNode;
459
+ }
460
+
461
+ const InsightContent = forwardRef<View, InsightContentProps>(
462
+ ({ asChild, className, children, style, ...props }, ref) => {
463
+ const Comp = asChild ? Slot : View;
464
+
465
+ return (
466
+ <Comp
467
+ ref={ref}
468
+ className={cn(insightContentVariants(), className)}
469
+ style={style}
470
+ {...props}
471
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'insight-content' }))}
472
+ >
473
+ {children}
474
+ </Comp>
475
+ );
476
+ },
477
+ );
478
+
479
+ InsightContent.displayName = 'Insight.Content';
480
+
481
+ // =============================================================================
482
+ // FOOTER
483
+ // =============================================================================
484
+
485
+ export interface InsightFooterProps extends ViewProps {
486
+ asChild?: boolean;
487
+ className?: string;
488
+ children?: ReactNode;
489
+ }
490
+
491
+ const InsightFooter = forwardRef<View, InsightFooterProps>(
492
+ ({ asChild, className, children, style, ...props }, ref) => {
493
+ const Comp = asChild ? Slot : View;
494
+
495
+ return (
496
+ <Comp
497
+ ref={ref}
498
+ className={cn(insightFooterVariants(), className)}
499
+ style={style}
500
+ {...props}
501
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'insight-footer' }))}
502
+ >
503
+ {children}
504
+ </Comp>
505
+ );
506
+ },
507
+ );
508
+
509
+ InsightFooter.displayName = 'Insight.Footer';
510
+
511
+ // =============================================================================
512
+ // COMPOUND COMPONENT
513
+ // =============================================================================
514
+
515
+ type InsightCompoundComponent = typeof InsightRootStyled & {
516
+ Tag: typeof InsightTag;
517
+ Header: typeof InsightHeader;
518
+ Title: typeof InsightTitle;
519
+ Description: typeof InsightDescription;
520
+ Content: typeof InsightContent;
521
+ Footer: typeof InsightFooter;
522
+ };
523
+
524
+ export const Insight = Object.assign(InsightRootStyled, {
525
+ Tag: InsightTag,
526
+ Header: InsightHeader,
527
+ Title: InsightTitle,
528
+ Description: InsightDescription,
529
+ Content: InsightContent,
530
+ Footer: InsightFooter,
531
+ }) as InsightCompoundComponent;
@@ -0,0 +1,30 @@
1
+ const ROOT_FONT_SIZE_PX = 16;
2
+
3
+ const LENGTH_PATTERN = /^(-?\d*\.?\d+)(px|rem)?$/;
4
+
5
+ /**
6
+ * Converts a resolved CSS variable (a number, or a `px`/`rem` string) to pixels. Native resolves
7
+ * built-in tokens to numbers, while runtime theme overrides and the web resolve to strings.
8
+ */
9
+ export function cssLengthToPx(value: string | number | undefined): number | undefined {
10
+ if (typeof value === 'number') {
11
+ return Number.isFinite(value) ? value : undefined;
12
+ }
13
+ if (typeof value !== 'string') {
14
+ return undefined;
15
+ }
16
+
17
+ const match = LENGTH_PATTERN.exec(value.trim());
18
+ if (!match) {
19
+ return undefined;
20
+ }
21
+
22
+ const amount = Number(match[1]);
23
+ return match[2] === 'rem' ? amount * ROOT_FONT_SIZE_PX : amount;
24
+ }
25
+
26
+ /** The radius of a surface inset `inset` px inside a frame with radius `outer`, or `undefined` when `outer` is not a length. */
27
+ export function insetRadius(outer: string | number | undefined, inset: number): number | undefined {
28
+ const px = cssLengthToPx(outer);
29
+ return px === undefined ? undefined : Math.max(0, px - inset);
30
+ }