panelui-native 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -23
- package/lib/module/components/button/index.js +12 -9
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/chip/index.js +313 -0
- package/lib/module/components/chip/index.js.map +1 -0
- package/lib/module/components/frame/index.js +41 -39
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +772 -0
- package/lib/module/components/heatmap-chart/index.js.map +1 -0
- package/lib/module/components/input/index.js +4 -0
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/scroll-canvas/index.js +175 -0
- package/lib/module/components/scroll-canvas/index.js.map +1 -0
- package/lib/module/components/scroll-text/index.js +213 -0
- package/lib/module/components/scroll-text/index.js.map +1 -0
- package/lib/module/components/select/index.js +20 -24
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/slider/index.js +2 -5
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/switch/index.js +15 -17
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/thinking-orb/index.js +741 -0
- package/lib/module/components/thinking-orb/index.js.map +1 -0
- package/lib/module/components/toggle-button/index.js +12 -3
- package/lib/module/components/toggle-button/index.js.map +1 -1
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-keyboard-avoidance.js +99 -59
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
- package/lib/module/hooks/use-reveal-progress.js +75 -0
- package/lib/module/hooks/use-reveal-progress.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js.map +1 -1
- package/lib/module/primitives/keyboard-avoider.js +21 -2
- package/lib/module/primitives/keyboard-avoider.js.map +1 -1
- package/lib/module/primitives/scroll-progress.js +114 -0
- package/lib/module/primitives/scroll-progress.js.map +1 -0
- package/lib/typescript/src/components/button/index.d.ts +3 -4
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/chip/index.d.ts +304 -0
- package/lib/typescript/src/components/chip/index.d.ts.map +1 -0
- package/lib/typescript/src/components/frame/index.d.ts +15 -13
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts +260 -0
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/input/index.d.ts +19 -4
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/scroll-canvas/index.d.ts +66 -0
- package/lib/typescript/src/components/scroll-canvas/index.d.ts.map +1 -0
- package/lib/typescript/src/components/scroll-text/index.d.ts +46 -0
- package/lib/typescript/src/components/scroll-text/index.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/thinking-orb/index.d.ts +28 -0
- package/lib/typescript/src/components/thinking-orb/index.d.ts.map +1 -0
- package/lib/typescript/src/components/toggle-button/index.d.ts +12 -0
- package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/index.d.ts +2 -1
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +16 -3
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-reveal-progress.d.ts +56 -0
- package/lib/typescript/src/hooks/use-reveal-progress.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +15 -7
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts +30 -3
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
- package/lib/typescript/src/primitives/scroll-progress.d.ts +61 -0
- package/lib/typescript/src/primitives/scroll-progress.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/button/index.tsx +13 -11
- package/src/components/chip/index.tsx +333 -0
- package/src/components/frame/index.tsx +39 -50
- package/src/components/heatmap-chart/index.tsx +1024 -0
- package/src/components/input/index.tsx +23 -4
- package/src/components/scroll-canvas/index.tsx +220 -0
- package/src/components/scroll-text/index.tsx +277 -0
- package/src/components/select/index.tsx +4 -9
- package/src/components/slider/index.tsx +4 -7
- package/src/components/switch/index.tsx +5 -8
- package/src/components/thinking-orb/index.tsx +792 -0
- package/src/components/toggle-button/index.tsx +24 -2
- package/src/hooks/index.ts +6 -0
- package/src/hooks/use-keyboard-avoidance.ts +111 -65
- package/src/hooks/use-reveal-progress.ts +106 -0
- package/src/index.ts +46 -0
- package/src/native/index.ts +15 -7
- package/src/primitives/keyboard-avoider.tsx +41 -4
- package/src/primitives/scroll-progress.tsx +167 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chip — a compact, interactive token.
|
|
3
|
+
*
|
|
4
|
+
* Where Badge only shows and ToggleButton owns a stateful toolbar, Chip is the
|
|
5
|
+
* thing in between: a small pill you can *press*, mark as *selected*, or
|
|
6
|
+
* *remove*. It is the filter in a filter bar, the tag on a photo, the removable
|
|
7
|
+
* recipient in a "To:" field — small, dense, and touchable.
|
|
8
|
+
*
|
|
9
|
+
* The close button is its own hit target nested inside the pill, so tapping the
|
|
10
|
+
* ✕ removes the chip without also firing the chip's own `onPress`. That split
|
|
11
|
+
* is the whole reason a removable chip needs a component and not just a styled
|
|
12
|
+
* Badge.
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <Chip>Design</Chip>
|
|
16
|
+
*
|
|
17
|
+
* <Chip variant="info" onClose={() => remove('design')}>Design</Chip>
|
|
18
|
+
*
|
|
19
|
+
* <Chip selected={on} onPress={() => setOn(!on)}>
|
|
20
|
+
* <CheckIcon size={14} />
|
|
21
|
+
* <Chip.Label>Available</Chip.Label>
|
|
22
|
+
* </Chip>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import {
|
|
26
|
+
createContext,
|
|
27
|
+
forwardRef,
|
|
28
|
+
useCallback,
|
|
29
|
+
useContext,
|
|
30
|
+
useMemo,
|
|
31
|
+
type ReactNode,
|
|
32
|
+
} from 'react';
|
|
33
|
+
import { Pressable, View, type ViewProps } from 'react-native';
|
|
34
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
35
|
+
import { useCSSVariable } from 'uniwind';
|
|
36
|
+
import { IconColorProvider, XIcon } from '../../icons';
|
|
37
|
+
import {
|
|
38
|
+
AnimatedPressable,
|
|
39
|
+
type AnimatedPressableProps,
|
|
40
|
+
} from '../../primitives/animated-pressable';
|
|
41
|
+
import { Text } from '../../primitives/text';
|
|
42
|
+
import { cn } from '../../utils/cn';
|
|
43
|
+
import { selectionTick } from '../../utils/haptics';
|
|
44
|
+
|
|
45
|
+
const chipVariants = tv({
|
|
46
|
+
slots: {
|
|
47
|
+
root: 'flex-row items-center self-start rounded-full border border-transparent',
|
|
48
|
+
label: 'font-medium',
|
|
49
|
+
close:
|
|
50
|
+
'items-center justify-center rounded-full -mr-1 aspect-square opacity-70',
|
|
51
|
+
},
|
|
52
|
+
variants: {
|
|
53
|
+
variant: {
|
|
54
|
+
/** The resting tag — a quiet neutral fill. */
|
|
55
|
+
default: { root: 'bg-secondary', label: 'text-secondary-foreground' },
|
|
56
|
+
/** The accent fill, for the one chip that leads. */
|
|
57
|
+
primary: { root: 'bg-primary', label: 'text-primary-foreground' },
|
|
58
|
+
/** Just an outline — the lightest a chip gets while still being a shape. */
|
|
59
|
+
outline: { root: 'border-border bg-transparent', label: 'text-foreground' },
|
|
60
|
+
success: {
|
|
61
|
+
root: 'bg-success-subtle',
|
|
62
|
+
label: 'text-success-foreground',
|
|
63
|
+
},
|
|
64
|
+
warning: {
|
|
65
|
+
root: 'bg-warning-subtle',
|
|
66
|
+
label: 'text-warning-foreground',
|
|
67
|
+
},
|
|
68
|
+
info: { root: 'bg-info-subtle', label: 'text-info-foreground' },
|
|
69
|
+
destructive: {
|
|
70
|
+
root: 'bg-destructive-subtle',
|
|
71
|
+
label: 'text-destructive-foreground',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
size: {
|
|
75
|
+
sm: { root: 'h-6 gap-1 px-2', label: 'text-xs' },
|
|
76
|
+
md: { root: 'h-7 gap-1.5 px-2.5', label: 'text-sm' },
|
|
77
|
+
lg: { root: 'h-9 gap-2 px-3.5', label: 'text-base' },
|
|
78
|
+
},
|
|
79
|
+
/**
|
|
80
|
+
* The filter "on" state — a loud accent surface that wins over the resting
|
|
81
|
+
* variant, so any chip can be a filter without picking a second colour.
|
|
82
|
+
*/
|
|
83
|
+
selected: {
|
|
84
|
+
true: {
|
|
85
|
+
root: 'border-primary/24 bg-accent',
|
|
86
|
+
label: 'text-accent-foreground',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
/** Trims the trailing padding so the close target hugs the right edge. */
|
|
90
|
+
closable: {
|
|
91
|
+
true: { root: 'pr-1' },
|
|
92
|
+
},
|
|
93
|
+
disabled: {
|
|
94
|
+
true: { root: 'opacity-[0.56]' },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
compoundVariants: [
|
|
98
|
+
{ size: 'sm', closable: true, class: { close: 'h-4' } },
|
|
99
|
+
{ size: 'md', closable: true, class: { close: 'h-5' } },
|
|
100
|
+
{ size: 'lg', closable: true, class: { close: 'h-6' } },
|
|
101
|
+
],
|
|
102
|
+
defaultVariants: {
|
|
103
|
+
variant: 'default',
|
|
104
|
+
size: 'md',
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
type ChipVariantProps = VariantProps<typeof chipVariants>;
|
|
109
|
+
export type ChipVariant = NonNullable<ChipVariantProps['variant']>;
|
|
110
|
+
export type ChipSize = NonNullable<ChipVariantProps['size']>;
|
|
111
|
+
|
|
112
|
+
/** Which foreground token each variant paints its icons and ✕ with. */
|
|
113
|
+
const TINT_VAR: Record<ChipVariant, string> = {
|
|
114
|
+
default: '--color-secondary-foreground',
|
|
115
|
+
primary: '--color-primary-foreground',
|
|
116
|
+
outline: '--color-foreground',
|
|
117
|
+
success: '--color-success-foreground',
|
|
118
|
+
warning: '--color-warning-foreground',
|
|
119
|
+
info: '--color-info-foreground',
|
|
120
|
+
destructive: '--color-destructive-foreground',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** The ✕ scales with the pill so it never looks stranded in a large chip. */
|
|
124
|
+
const CLOSE_ICON: Record<ChipSize, number> = { sm: 11, md: 13, lg: 15 };
|
|
125
|
+
|
|
126
|
+
/* -------------------------------------------------------------------------- */
|
|
127
|
+
/* State context */
|
|
128
|
+
/* -------------------------------------------------------------------------- */
|
|
129
|
+
|
|
130
|
+
interface ChipStateValue {
|
|
131
|
+
selected: boolean;
|
|
132
|
+
disabled: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface ChipContextValue extends ChipStateValue {
|
|
136
|
+
/** The label classes for this chip's variant/size, so `Chip.Label` matches. */
|
|
137
|
+
labelClasses: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const ChipContext = createContext<ChipContextValue | null>(null);
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The chip's own state, for anything rendered inside it — an icon that swaps
|
|
144
|
+
* when the chip is selected, a count that only shows when it is on.
|
|
145
|
+
*/
|
|
146
|
+
export function useChip(): ChipStateValue {
|
|
147
|
+
const ctx = useContext(ChipContext);
|
|
148
|
+
if (!ctx) {
|
|
149
|
+
throw new Error('useChip must be used within a <Chip>');
|
|
150
|
+
}
|
|
151
|
+
return { selected: ctx.selected, disabled: ctx.disabled };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* -------------------------------------------------------------------------- */
|
|
155
|
+
/* Chip */
|
|
156
|
+
/* -------------------------------------------------------------------------- */
|
|
157
|
+
|
|
158
|
+
export interface ChipProps
|
|
159
|
+
extends Omit<AnimatedPressableProps, 'children' | 'disabled'>,
|
|
160
|
+
Omit<ChipVariantProps, 'selected' | 'disabled' | 'closable'> {
|
|
161
|
+
children?: ReactNode;
|
|
162
|
+
className?: string;
|
|
163
|
+
/** Extra classes for the label when `children` is a string. */
|
|
164
|
+
labelClassName?: string;
|
|
165
|
+
/**
|
|
166
|
+
* The filter "on" state. Setting it (even to `false`) makes the chip a
|
|
167
|
+
* toggle, announced as such — pair it with `onPress` to flip it.
|
|
168
|
+
*/
|
|
169
|
+
selected?: boolean;
|
|
170
|
+
disabled?: boolean;
|
|
171
|
+
/** A leading icon or avatar, before the label. */
|
|
172
|
+
start?: ReactNode;
|
|
173
|
+
/**
|
|
174
|
+
* Shows a trailing ✕ and calls this when it is pressed. The ✕ is its own hit
|
|
175
|
+
* target, so removing a chip never also fires its `onPress`.
|
|
176
|
+
*/
|
|
177
|
+
onClose?: () => void;
|
|
178
|
+
/** Accessibility label for the ✕. Defaults to "Remove". */
|
|
179
|
+
closeLabel?: string;
|
|
180
|
+
/**
|
|
181
|
+
* A tick under the finger when the chip is pressed or removed. Off by
|
|
182
|
+
* default — needs the optional `expo-haptics`, and is silent without it.
|
|
183
|
+
*/
|
|
184
|
+
haptics?: boolean;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const ChipRoot = forwardRef<View, ChipProps>(
|
|
188
|
+
(
|
|
189
|
+
{
|
|
190
|
+
children,
|
|
191
|
+
className,
|
|
192
|
+
labelClassName,
|
|
193
|
+
variant = 'default',
|
|
194
|
+
size = 'md',
|
|
195
|
+
selected,
|
|
196
|
+
disabled = false,
|
|
197
|
+
start,
|
|
198
|
+
onClose,
|
|
199
|
+
closeLabel = 'Remove',
|
|
200
|
+
haptics = false,
|
|
201
|
+
onPress,
|
|
202
|
+
...props
|
|
203
|
+
},
|
|
204
|
+
ref
|
|
205
|
+
) => {
|
|
206
|
+
const isToggle = selected !== undefined;
|
|
207
|
+
const isSelected = selected ?? false;
|
|
208
|
+
const closable = onClose !== undefined;
|
|
209
|
+
// Only the whole chip becoming pressable warrants the press animation; a
|
|
210
|
+
// static attribute tag stays a plain View so it does not invite a tap.
|
|
211
|
+
const pressable = onPress !== undefined || isToggle;
|
|
212
|
+
|
|
213
|
+
const slots = chipVariants({
|
|
214
|
+
variant,
|
|
215
|
+
size,
|
|
216
|
+
selected: isSelected,
|
|
217
|
+
closable,
|
|
218
|
+
disabled,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Icons and the ✕ inherit this, so they track the surface they sit on
|
|
222
|
+
// instead of every caller hardcoding a hex that is wrong in one theme.
|
|
223
|
+
const tint = useCSSVariable(
|
|
224
|
+
isSelected ? '--color-accent-foreground' : TINT_VAR[variant]
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const handlePress = useCallback<NonNullable<AnimatedPressableProps['onPress']>>(
|
|
228
|
+
(event) => {
|
|
229
|
+
if (haptics) selectionTick();
|
|
230
|
+
onPress?.(event);
|
|
231
|
+
},
|
|
232
|
+
[haptics, onPress]
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const handleClose = useCallback(() => {
|
|
236
|
+
if (haptics) selectionTick();
|
|
237
|
+
onClose?.();
|
|
238
|
+
}, [haptics, onClose]);
|
|
239
|
+
|
|
240
|
+
const labelClasses = slots.label();
|
|
241
|
+
const state = useMemo(
|
|
242
|
+
() => ({ selected: isSelected, disabled, labelClasses }),
|
|
243
|
+
[isSelected, disabled, labelClasses]
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const body = (
|
|
247
|
+
<>
|
|
248
|
+
{start}
|
|
249
|
+
{typeof children === 'string' ? (
|
|
250
|
+
<Text className={slots.label({ className: labelClassName })}>
|
|
251
|
+
{children}
|
|
252
|
+
</Text>
|
|
253
|
+
) : (
|
|
254
|
+
children
|
|
255
|
+
)}
|
|
256
|
+
{closable ? (
|
|
257
|
+
<Pressable
|
|
258
|
+
accessibilityRole="button"
|
|
259
|
+
accessibilityLabel={closeLabel}
|
|
260
|
+
// A small pill leaves little room for the ✕; the slop gives the
|
|
261
|
+
// finger the target the pixels do not.
|
|
262
|
+
hitSlop={8}
|
|
263
|
+
disabled={disabled}
|
|
264
|
+
onPress={handleClose}
|
|
265
|
+
className={slots.close()}
|
|
266
|
+
>
|
|
267
|
+
<XIcon size={CLOSE_ICON[size]} />
|
|
268
|
+
</Pressable>
|
|
269
|
+
) : null}
|
|
270
|
+
</>
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const sharedProps = {
|
|
274
|
+
accessibilityRole: 'button' as const,
|
|
275
|
+
// A toggle chip is read out as checked/unchecked; a plain pressable one
|
|
276
|
+
// as a button. A static tag gets neither, so it is not announced as
|
|
277
|
+
// something to press.
|
|
278
|
+
accessibilityState: { disabled, ...(isToggle ? { selected: isSelected } : {}) },
|
|
279
|
+
'aria-pressed': isToggle ? isSelected : undefined,
|
|
280
|
+
disabled,
|
|
281
|
+
className: slots.root({ className }),
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<ChipContext.Provider value={state}>
|
|
286
|
+
<IconColorProvider color={typeof tint === 'string' ? tint : undefined}>
|
|
287
|
+
{pressable ? (
|
|
288
|
+
<AnimatedPressable
|
|
289
|
+
ref={ref}
|
|
290
|
+
onPress={handlePress}
|
|
291
|
+
{...sharedProps}
|
|
292
|
+
{...props}
|
|
293
|
+
>
|
|
294
|
+
{body}
|
|
295
|
+
</AnimatedPressable>
|
|
296
|
+
) : (
|
|
297
|
+
<View ref={ref} className={slots.root({ className })} {...(props as ViewProps)}>
|
|
298
|
+
{body}
|
|
299
|
+
</View>
|
|
300
|
+
)}
|
|
301
|
+
</IconColorProvider>
|
|
302
|
+
</ChipContext.Provider>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
ChipRoot.displayName = 'Chip';
|
|
307
|
+
|
|
308
|
+
/* -------------------------------------------------------------------------- */
|
|
309
|
+
/* Chip.Label */
|
|
310
|
+
/* -------------------------------------------------------------------------- */
|
|
311
|
+
|
|
312
|
+
export interface ChipLabelProps {
|
|
313
|
+
className?: string;
|
|
314
|
+
children: ReactNode;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The label, when the chip holds more than a string — an icon beside text. It
|
|
319
|
+
* reads the selected state itself, so composing the two does not mean threading
|
|
320
|
+
* the colour through by hand.
|
|
321
|
+
*/
|
|
322
|
+
function ChipLabel({ className, children }: ChipLabelProps) {
|
|
323
|
+
const ctx = useContext(ChipContext);
|
|
324
|
+
if (!ctx) {
|
|
325
|
+
throw new Error('Chip.Label must be used within a <Chip>');
|
|
326
|
+
}
|
|
327
|
+
return <Text className={cn(ctx.labelClasses, className)}>{children}</Text>;
|
|
328
|
+
}
|
|
329
|
+
ChipLabel.displayName = 'Chip.Label';
|
|
330
|
+
|
|
331
|
+
export const Chip = Object.assign(ChipRoot, {
|
|
332
|
+
Label: ChipLabel,
|
|
333
|
+
});
|
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Frame — a widget shell: a
|
|
3
|
-
*
|
|
2
|
+
* Frame — a widget shell: a card of rows sitting in a tray, with the tray's
|
|
3
|
+
* one exposed strip along the top carrying the title.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* The two surfaces are nested rather than stacked, and only one edge of the
|
|
6
|
+
* outer one is ever visible. The panel is flush to the shell's left, right and
|
|
7
|
+
* bottom, so the shell reads as something the card is *sitting in* rather than
|
|
8
|
+
* as a border around it — and the strip left at the top is the header, which
|
|
9
|
+
* is why the header needs no rule under it and no background of its own.
|
|
10
|
+
*
|
|
11
|
+
* The shell's radius is the larger of the two, and the panel's top corners are
|
|
12
|
+
* tighter. That is the reverse of the usual nested-radius rule, and it is
|
|
13
|
+
* deliberate: with only the top corners free, matching them would make the two
|
|
14
|
+
* surfaces read as one misdrawn shape. The panel's bottom corners are not set
|
|
15
|
+
* at all — the shell clips them, so they take its radius exactly.
|
|
9
16
|
*
|
|
10
17
|
* ```tsx
|
|
11
18
|
* <Frame>
|
|
12
19
|
* <Frame.Header>
|
|
13
|
-
* <Frame.Title>
|
|
14
|
-
* <Frame.Action>
|
|
20
|
+
* <Frame.Title>Agent monitor</Frame.Title>
|
|
21
|
+
* <Frame.Action>All agents under 25% token limit</Frame.Action>
|
|
15
22
|
* </Frame.Header>
|
|
16
23
|
* <Frame.Panel>
|
|
17
24
|
* <Frame.Row>…</Frame.Row>
|
|
18
25
|
* <Frame.Row>…</Frame.Row>
|
|
19
26
|
* </Frame.Panel>
|
|
20
|
-
* <Frame.Footer>Updated 2 minutes ago</Frame.Footer>
|
|
21
27
|
* </Frame>
|
|
22
28
|
* ```
|
|
23
29
|
*
|
|
@@ -41,15 +47,24 @@ import { cn } from '../../utils/cn';
|
|
|
41
47
|
const frameVariants = tv({
|
|
42
48
|
slots: {
|
|
43
49
|
root: '',
|
|
44
|
-
|
|
50
|
+
// Flush left, right and bottom: no side or bottom border, because the
|
|
51
|
+
// shell's own edge is already there, and no bottom radius, because the
|
|
52
|
+
// shell clips it. Only the top corners and the rule under the header are
|
|
53
|
+
// the panel's to draw.
|
|
54
|
+
panel: 'overflow-hidden rounded-t-2xl border-t border-border bg-card',
|
|
45
55
|
},
|
|
46
56
|
variants: {
|
|
47
57
|
variant: {
|
|
48
|
-
|
|
58
|
+
// `overflow-hidden` is load-bearing — it is what makes the panel's
|
|
59
|
+
// bottom corners take the shell's radius instead of squaring off
|
|
60
|
+
// against it.
|
|
61
|
+
default: {
|
|
62
|
+
root: 'overflow-hidden rounded-3xl border border-border bg-surface',
|
|
63
|
+
},
|
|
49
64
|
// No shell: the panel is the whole widget. For a Frame nested inside a
|
|
50
65
|
// card that already draws a border, where the shell's own edge sitting
|
|
51
66
|
// just inside it reads as a double line.
|
|
52
|
-
plain: { root: '', panel: 'rounded-
|
|
67
|
+
plain: { root: '', panel: 'rounded-3xl border border-border' },
|
|
53
68
|
},
|
|
54
69
|
},
|
|
55
70
|
defaultVariants: {
|
|
@@ -83,15 +98,16 @@ export interface FrameHeaderProps extends FrameProps {
|
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
/**
|
|
86
|
-
* The header row
|
|
87
|
-
* `className="flex-col items-start"`
|
|
101
|
+
* The header row — the strip of shell left exposed above the panel. Title on
|
|
102
|
+
* the left, `Frame.Action` on the right. Add `className="flex-col items-start"`
|
|
103
|
+
* when you want a description underneath.
|
|
88
104
|
*/
|
|
89
105
|
const FrameHeader = forwardRef<View, FrameHeaderProps>(
|
|
90
106
|
({ className, ...props }, ref) => (
|
|
91
107
|
<View
|
|
92
108
|
ref={ref}
|
|
93
109
|
className={cn(
|
|
94
|
-
'flex-row items-center justify-between gap-3 px-
|
|
110
|
+
'flex-row items-center justify-between gap-3 px-4 pb-3 pt-2.5',
|
|
95
111
|
className
|
|
96
112
|
)}
|
|
97
113
|
{...props}
|
|
@@ -100,14 +116,12 @@ const FrameHeader = forwardRef<View, FrameHeaderProps>(
|
|
|
100
116
|
);
|
|
101
117
|
FrameHeader.displayName = 'Frame.Header';
|
|
102
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Muted by default. The header is a caption on the tray the card sits in, not
|
|
121
|
+
* a heading over a section — the card's own rows carry the weight.
|
|
122
|
+
*/
|
|
103
123
|
const FrameTitle = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => (
|
|
104
|
-
<Text
|
|
105
|
-
ref={ref}
|
|
106
|
-
size="base"
|
|
107
|
-
weight="medium"
|
|
108
|
-
className={cn('text-foreground', className)}
|
|
109
|
-
{...props}
|
|
110
|
-
/>
|
|
124
|
+
<Text ref={ref} size="sm" muted className={className} {...props} />
|
|
111
125
|
));
|
|
112
126
|
FrameTitle.displayName = 'Frame.Title';
|
|
113
127
|
|
|
@@ -123,7 +137,7 @@ const FrameAction = forwardRef<View, FrameActionProps>(
|
|
|
123
137
|
({ className, children, ...props }, ref) => (
|
|
124
138
|
<View ref={ref} className={cn('flex-row items-center gap-2', className)} {...props}>
|
|
125
139
|
{typeof children === 'string' ? (
|
|
126
|
-
<Text size="
|
|
140
|
+
<Text size="sm" muted>
|
|
127
141
|
{children}
|
|
128
142
|
</Text>
|
|
129
143
|
) : (
|
|
@@ -177,8 +191,8 @@ export interface FramePanelProps extends FrameProps {
|
|
|
177
191
|
}
|
|
178
192
|
|
|
179
193
|
/**
|
|
180
|
-
* The
|
|
181
|
-
*
|
|
194
|
+
* The card holding the frame's content — flush to the shell on three sides,
|
|
195
|
+
* with the header strip above it.
|
|
182
196
|
*/
|
|
183
197
|
const FramePanel = forwardRef<View, FramePanelProps>(
|
|
184
198
|
({ className, dividers = true, children, ...props }, ref) => (
|
|
@@ -284,30 +298,6 @@ FrameSection.displayName = 'Frame.Section';
|
|
|
284
298
|
/** Parts the panel divides. Declared after them, since it holds references. */
|
|
285
299
|
const DIVIDABLE = new Set<unknown>([FrameRow, FrameSection]);
|
|
286
300
|
|
|
287
|
-
export interface FrameFooterProps extends FrameProps {
|
|
288
|
-
children?: ReactNode;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/** Muted caption under the panel. Strings are wrapped for you. */
|
|
292
|
-
const FrameFooter = forwardRef<View, FrameFooterProps>(
|
|
293
|
-
({ className, children, ...props }, ref) => (
|
|
294
|
-
<View
|
|
295
|
-
ref={ref}
|
|
296
|
-
className={cn('flex-row items-center gap-2 px-3 pb-1.5 pt-2.5', className)}
|
|
297
|
-
{...props}
|
|
298
|
-
>
|
|
299
|
-
{typeof children === 'string' ? (
|
|
300
|
-
<Text size="sm" muted>
|
|
301
|
-
{children}
|
|
302
|
-
</Text>
|
|
303
|
-
) : (
|
|
304
|
-
children
|
|
305
|
-
)}
|
|
306
|
-
</View>
|
|
307
|
-
)
|
|
308
|
-
);
|
|
309
|
-
FrameFooter.displayName = 'Frame.Footer';
|
|
310
|
-
|
|
311
301
|
export const Frame = Object.assign(FrameRoot, {
|
|
312
302
|
Header: FrameHeader,
|
|
313
303
|
Title: FrameTitle,
|
|
@@ -316,5 +306,4 @@ export const Frame = Object.assign(FrameRoot, {
|
|
|
316
306
|
Panel: FramePanel,
|
|
317
307
|
Section: FrameSection,
|
|
318
308
|
Row: FrameRow,
|
|
319
|
-
Footer: FrameFooter,
|
|
320
309
|
});
|