@cdx-ui/components 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/coverage.svg +1 -1
- package/docs/components/color-field/content.md +2 -2
- package/docs/components/content.md +6 -4
- package/docs/components/data-grid/content.md +125 -1
- package/docs/components/date-field/content.md +2 -2
- package/docs/components/date-time-field/content.md +2 -2
- package/docs/components/input/content.md +20 -2
- package/docs/components/input/design.md +1 -1
- package/docs/components/insight/content.md +644 -0
- package/docs/components/insight-stack/content.md +231 -0
- package/docs/components/insurance-disclosure/content.md +1 -1
- package/docs/components/otp-input/content.md +2 -2
- package/docs/components/select/content.md +223 -24
- package/docs/components/text/content.md +97 -0
- package/docs/foundations/styles/tokens/all-tokens/content.md +23 -3
- package/docs/llms.txt +8 -4
- package/docs/patterns/forms/content.md +407 -0
- package/docs/resources/release-notes/0.3.2/content.md +17 -0
- package/docs/resources/release-notes/0.3.3/content.md +23 -0
- package/docs/resources/release-notes/content.md +2 -0
- package/lib/commonjs/components/Drawer/index.js +1 -1
- package/lib/commonjs/components/Drawer/index.js.map +1 -1
- package/lib/commonjs/components/Input/index.js.map +1 -1
- package/lib/commonjs/components/Input/styles.js +13 -6
- package/lib/commonjs/components/Input/styles.js.map +1 -1
- package/lib/commonjs/components/Insight/index.js +438 -0
- package/lib/commonjs/components/Insight/index.js.map +1 -0
- package/lib/commonjs/components/Insight/insetRadius.js +35 -0
- package/lib/commonjs/components/Insight/insetRadius.js.map +1 -0
- package/lib/commonjs/components/Insight/styles.js +186 -0
- package/lib/commonjs/components/Insight/styles.js.map +1 -0
- package/lib/commonjs/components/InsightStack/index.js +93 -0
- package/lib/commonjs/components/InsightStack/index.js.map +1 -0
- package/lib/commonjs/components/InsightStack/styles.js +61 -0
- package/lib/commonjs/components/InsightStack/styles.js.map +1 -0
- package/lib/commonjs/components/OtpInput/index.js.map +1 -1
- package/lib/commonjs/components/Select/index.js +326 -24
- package/lib/commonjs/components/Select/index.js.map +1 -1
- package/lib/commonjs/components/Select/styles.js +32 -1
- package/lib/commonjs/components/Select/styles.js.map +1 -1
- package/lib/commonjs/components/index.js +24 -0
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/commonjs/components/picker/surfaces.js +18 -3
- package/lib/commonjs/components/picker/surfaces.js.map +1 -1
- package/lib/commonjs/figma/Insight.figma.js +73 -0
- package/lib/commonjs/figma/Insight.figma.js.map +1 -0
- package/lib/commonjs/figma/InsightStack.figma.js +39 -0
- package/lib/commonjs/figma/InsightStack.figma.js.map +1 -0
- package/lib/module/components/Drawer/index.js +1 -1
- package/lib/module/components/Drawer/index.js.map +1 -1
- package/lib/module/components/Input/index.js.map +1 -1
- package/lib/module/components/Input/styles.js +14 -6
- package/lib/module/components/Input/styles.js.map +1 -1
- package/lib/module/components/Insight/index.js +433 -0
- package/lib/module/components/Insight/index.js.map +1 -0
- package/lib/module/components/Insight/insetRadius.js +30 -0
- package/lib/module/components/Insight/insetRadius.js.map +1 -0
- package/lib/module/components/Insight/styles.js +183 -0
- package/lib/module/components/Insight/styles.js.map +1 -0
- package/lib/module/components/InsightStack/index.js +89 -0
- package/lib/module/components/InsightStack/index.js.map +1 -0
- package/lib/module/components/InsightStack/styles.js +58 -0
- package/lib/module/components/InsightStack/styles.js.map +1 -0
- package/lib/module/components/OtpInput/index.js.map +1 -1
- package/lib/module/components/Select/index.js +328 -26
- package/lib/module/components/Select/index.js.map +1 -1
- package/lib/module/components/Select/styles.js +31 -0
- package/lib/module/components/Select/styles.js.map +1 -1
- package/lib/module/components/index.js +2 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/components/picker/surfaces.js +17 -3
- package/lib/module/components/picker/surfaces.js.map +1 -1
- package/lib/module/figma/Insight.figma.js +67 -0
- package/lib/module/figma/Insight.figma.js.map +1 -0
- package/lib/module/figma/InsightStack.figma.js +33 -0
- package/lib/module/figma/InsightStack.figma.js.map +1 -0
- package/lib/typescript/components/ColorField/styles.d.ts +4 -2
- package/lib/typescript/components/ColorField/styles.d.ts.map +1 -1
- package/lib/typescript/components/DateField/styles.d.ts +4 -2
- package/lib/typescript/components/DateField/styles.d.ts.map +1 -1
- package/lib/typescript/components/Input/index.d.ts +3 -2
- package/lib/typescript/components/Input/index.d.ts.map +1 -1
- package/lib/typescript/components/Input/styles.d.ts +1 -1
- package/lib/typescript/components/Input/styles.d.ts.map +1 -1
- package/lib/typescript/components/Insight/index.d.ts +87 -0
- package/lib/typescript/components/Insight/index.d.ts.map +1 -0
- package/lib/typescript/components/Insight/insetRadius.d.ts +8 -0
- package/lib/typescript/components/Insight/insetRadius.d.ts.map +1 -0
- package/lib/typescript/components/Insight/styles.d.ts +33 -0
- package/lib/typescript/components/Insight/styles.d.ts.map +1 -0
- package/lib/typescript/components/InsightStack/index.d.ts +21 -0
- package/lib/typescript/components/InsightStack/index.d.ts.map +1 -0
- package/lib/typescript/components/InsightStack/styles.d.ts +11 -0
- package/lib/typescript/components/InsightStack/styles.d.ts.map +1 -0
- package/lib/typescript/components/OtpInput/index.d.ts +3 -2
- package/lib/typescript/components/OtpInput/index.d.ts.map +1 -1
- package/lib/typescript/components/Select/index.d.ts +73 -6
- package/lib/typescript/components/Select/index.d.ts.map +1 -1
- package/lib/typescript/components/Select/styles.d.ts +9 -0
- package/lib/typescript/components/Select/styles.d.ts.map +1 -1
- package/lib/typescript/components/index.d.ts +2 -0
- package/lib/typescript/components/index.d.ts.map +1 -1
- package/lib/typescript/components/picker/surfaces.d.ts +14 -3
- package/lib/typescript/components/picker/surfaces.d.ts.map +1 -1
- package/lib/typescript/figma/Insight.figma.d.ts +8 -0
- package/lib/typescript/figma/Insight.figma.d.ts.map +1 -0
- package/lib/typescript/figma/InsightStack.figma.d.ts +8 -0
- package/lib/typescript/figma/InsightStack.figma.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/components/ColorField/styles.ts +3 -1
- package/src/components/DateField/styles.ts +6 -1
- package/src/components/Drawer/index.tsx +1 -1
- package/src/components/Input/index.tsx +3 -2
- package/src/components/Input/styles.ts +35 -15
- package/src/components/Insight/index.tsx +531 -0
- package/src/components/Insight/insetRadius.ts +30 -0
- package/src/components/Insight/styles.ts +193 -0
- package/src/components/InsightStack/index.tsx +86 -0
- package/src/components/InsightStack/styles.ts +61 -0
- package/src/components/OtpInput/index.tsx +3 -2
- package/src/components/Select/index.tsx +411 -29
- package/src/components/Select/styles.ts +36 -0
- package/src/components/index.ts +2 -0
- package/src/components/picker/surfaces.tsx +29 -4
|
@@ -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
|
+
}
|