panelui-native 0.28.1 → 0.32.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 +22 -3
- package/lib/module/components/calendar/index.js +450 -132
- package/lib/module/components/calendar/index.js.map +1 -1
- package/lib/module/components/code-block/highlight.js +363 -0
- package/lib/module/components/code-block/highlight.js.map +1 -0
- package/lib/module/components/code-block/index.js +313 -0
- package/lib/module/components/code-block/index.js.map +1 -0
- package/lib/module/components/date-picker/index.js +8 -1
- package/lib/module/components/date-picker/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +9 -8
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/map/index.js +50 -4
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/menu/index.js +714 -0
- package/lib/module/components/menu/index.js.map +1 -0
- package/lib/module/components/plan/index.js +582 -0
- package/lib/module/components/plan/index.js.map +1 -0
- package/lib/module/components/popover/index.js +35 -4
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/post/index.js +992 -0
- package/lib/module/components/post/index.js.map +1 -0
- package/lib/module/components/radio-group/index.js +13 -8
- package/lib/module/components/radio-group/index.js.map +1 -1
- package/lib/module/components/reasoning/index.js +244 -0
- package/lib/module/components/reasoning/index.js.map +1 -0
- package/lib/module/components/response/index.js +296 -0
- package/lib/module/components/response/index.js.map +1 -0
- package/lib/module/components/response/markdown.js +463 -0
- package/lib/module/components/response/markdown.js.map +1 -0
- package/lib/module/components/select/index.js +97 -15
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/sources/index.js +221 -0
- package/lib/module/components/sources/index.js.map +1 -0
- package/lib/module/components/table/index.js +632 -0
- package/lib/module/components/table/index.js.map +1 -0
- package/lib/module/components/task/index.js +300 -0
- package/lib/module/components/task/index.js.map +1 -0
- package/lib/module/components/textarea/index.js +295 -0
- package/lib/module/components/textarea/index.js.map +1 -0
- package/lib/module/components/tooltip/index.js +179 -23
- package/lib/module/components/tooltip/index.js.map +1 -1
- package/lib/module/icons/index.js +339 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +12 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/collapse.js +112 -0
- package/lib/module/primitives/collapse.js.map +1 -0
- package/lib/module/utils/date.js +129 -22
- package/lib/module/utils/date.js.map +1 -1
- package/lib/typescript/src/components/calendar/index.d.ts +79 -5
- package/lib/typescript/src/components/calendar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/code-block/highlight.d.ts +39 -0
- package/lib/typescript/src/components/code-block/highlight.d.ts.map +1 -0
- package/lib/typescript/src/components/code-block/index.d.ts +113 -0
- package/lib/typescript/src/components/code-block/index.d.ts.map +1 -0
- package/lib/typescript/src/components/date-picker/index.d.ts +7 -1
- package/lib/typescript/src/components/date-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/index.d.ts +8 -1
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/menu/index.d.ts +261 -0
- package/lib/typescript/src/components/menu/index.d.ts.map +1 -0
- package/lib/typescript/src/components/plan/index.d.ts +238 -0
- package/lib/typescript/src/components/plan/index.d.ts.map +1 -0
- package/lib/typescript/src/components/popover/index.d.ts +35 -1
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
- package/lib/typescript/src/components/post/index.d.ts +358 -0
- package/lib/typescript/src/components/post/index.d.ts.map +1 -0
- package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/reasoning/index.d.ts +97 -0
- package/lib/typescript/src/components/reasoning/index.d.ts.map +1 -0
- package/lib/typescript/src/components/response/index.d.ts +91 -0
- package/lib/typescript/src/components/response/index.d.ts.map +1 -0
- package/lib/typescript/src/components/response/markdown.d.ts +88 -0
- package/lib/typescript/src/components/response/markdown.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts +27 -2
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/sources/index.d.ts +85 -0
- package/lib/typescript/src/components/sources/index.d.ts.map +1 -0
- package/lib/typescript/src/components/table/index.d.ts +274 -0
- package/lib/typescript/src/components/table/index.d.ts.map +1 -0
- package/lib/typescript/src/components/task/index.d.ts +103 -0
- package/lib/typescript/src/components/task/index.d.ts.map +1 -0
- package/lib/typescript/src/components/textarea/index.d.ts +175 -0
- package/lib/typescript/src/components/textarea/index.d.ts.map +1 -0
- package/lib/typescript/src/components/tooltip/index.d.ts +63 -8
- package/lib/typescript/src/components/tooltip/index.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts +54 -1
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +14 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/collapse.d.ts +42 -0
- package/lib/typescript/src/primitives/collapse.d.ts.map +1 -0
- package/lib/typescript/src/utils/date.d.ts +49 -5
- package/lib/typescript/src/utils/date.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/calendar/index.tsx +519 -139
- package/src/components/code-block/highlight.ts +278 -0
- package/src/components/code-block/index.tsx +341 -0
- package/src/components/date-picker/index.tsx +19 -1
- package/src/components/heatmap-chart/index.tsx +9 -9
- package/src/components/map/index.tsx +53 -4
- package/src/components/menu/index.tsx +812 -0
- package/src/components/plan/index.tsx +648 -0
- package/src/components/popover/index.tsx +69 -2
- package/src/components/post/index.tsx +1116 -0
- package/src/components/radio-group/index.tsx +11 -2
- package/src/components/reasoning/index.tsx +297 -0
- package/src/components/response/index.tsx +390 -0
- package/src/components/response/markdown.ts +489 -0
- package/src/components/select/index.tsx +132 -12
- package/src/components/sources/index.tsx +278 -0
- package/src/components/table/index.tsx +747 -0
- package/src/components/task/index.tsx +329 -0
- package/src/components/textarea/index.tsx +381 -0
- package/src/components/tooltip/index.tsx +203 -20
- package/src/icons/index.tsx +264 -1
- package/src/index.ts +131 -0
- package/src/primitives/collapse.tsx +128 -0
- package/src/utils/date.ts +146 -22
- package/theme.css +112 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan — what an agent intends to do, before it does it.
|
|
3
|
+
*
|
|
4
|
+
* A card rather than a run of rows, because a plan is a thing the reader is
|
|
5
|
+
* being asked to approve: it needs an edge around it so that where it starts
|
|
6
|
+
* and stops is not a matter of interpretation, and a footer that can hold the
|
|
7
|
+
* button that approves it.
|
|
8
|
+
*
|
|
9
|
+
* The body is a rail of steps rather than prose, because a plan is an ordered
|
|
10
|
+
* list of things that will happen and the reader's question is always *which
|
|
11
|
+
* one is running*. A paragraph describing the same four steps cannot be glanced
|
|
12
|
+
* at to answer that; a column of markers can. The rail fills behind the steps
|
|
13
|
+
* that are done, so progress is legible from its left edge alone, and
|
|
14
|
+
* `Plan.Steps` counts itself so `Plan.Progress` can say `2 of 4` up in the
|
|
15
|
+
* header without the total being stated twice.
|
|
16
|
+
*
|
|
17
|
+
* It streams. `isStreaming` puts a shimmer on the title, the description and
|
|
18
|
+
* the one step that is running, which is the honest way to render text that is
|
|
19
|
+
* still arriving — the alternative is a title that grows a word at a time and
|
|
20
|
+
* reads as finished at every intermediate length.
|
|
21
|
+
*
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <Plan isStreaming={streaming}>
|
|
24
|
+
* <Plan.Header>
|
|
25
|
+
* <Plan.Icon><FileIcon size={16} /></Plan.Icon>
|
|
26
|
+
* <Plan.Title>Migrate the calendar</Plan.Title>
|
|
27
|
+
* <Plan.Description>Four files, no API change.</Plan.Description>
|
|
28
|
+
* <Plan.Action>
|
|
29
|
+
* <Plan.Progress />
|
|
30
|
+
* <Plan.Trigger />
|
|
31
|
+
* </Plan.Action>
|
|
32
|
+
* </Plan.Header>
|
|
33
|
+
* <Plan.Content>
|
|
34
|
+
* <Plan.Steps>
|
|
35
|
+
* <Plan.Step status="done" meta="utils/date.ts">Read the date utils</Plan.Step>
|
|
36
|
+
* <Plan.Step status="active">Replace the month grid</Plan.Step>
|
|
37
|
+
* <Plan.Step>Update the docs page</Plan.Step>
|
|
38
|
+
* </Plan.Steps>
|
|
39
|
+
* </Plan.Content>
|
|
40
|
+
* <Plan.Footer>
|
|
41
|
+
* <Button variant="outline">Revise</Button>
|
|
42
|
+
* <Button>Approve</Button>
|
|
43
|
+
* </Plan.Footer>
|
|
44
|
+
* </Plan>
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* ## Where the props come from
|
|
48
|
+
*
|
|
49
|
+
* With the AI SDK a plan is usually an `experimental_useObject` stream, where
|
|
50
|
+
* `isStreaming` is the hook's `isLoading` and the fields arrive one at a time —
|
|
51
|
+
* which is exactly the case the shimmer exists for, since a partial object has
|
|
52
|
+
* a title before it has anything else.
|
|
53
|
+
*/
|
|
54
|
+
import {
|
|
55
|
+
Children,
|
|
56
|
+
createContext,
|
|
57
|
+
isValidElement,
|
|
58
|
+
useCallback,
|
|
59
|
+
useContext,
|
|
60
|
+
useEffect,
|
|
61
|
+
useMemo,
|
|
62
|
+
useState,
|
|
63
|
+
type ReactElement,
|
|
64
|
+
type ReactNode,
|
|
65
|
+
} from 'react';
|
|
66
|
+
import { Pressable, View, type PressableProps, type ViewProps } from 'react-native';
|
|
67
|
+
import Animated, {
|
|
68
|
+
useAnimatedStyle,
|
|
69
|
+
useReducedMotion,
|
|
70
|
+
useSharedValue,
|
|
71
|
+
withTiming,
|
|
72
|
+
} from 'react-native-reanimated';
|
|
73
|
+
import { tv } from 'tailwind-variants';
|
|
74
|
+
import { useCSSVariable } from 'uniwind';
|
|
75
|
+
import { CheckIcon, ChevronDownIcon } from '../../icons';
|
|
76
|
+
import { Collapse } from '../../primitives/collapse';
|
|
77
|
+
import { Text, textChildren, type TextProps } from '../../primitives/text';
|
|
78
|
+
import { cn } from '../../utils/cn';
|
|
79
|
+
import { Shimmer } from '../shimmer';
|
|
80
|
+
|
|
81
|
+
const planVariants = tv({
|
|
82
|
+
slots: {
|
|
83
|
+
root: 'w-full overflow-hidden rounded-2xl border border-border bg-card shadow-sm',
|
|
84
|
+
header: 'flex-row items-start gap-3 p-4',
|
|
85
|
+
icon: 'h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted',
|
|
86
|
+
heading: 'min-w-0 flex-1 gap-1',
|
|
87
|
+
title: 'text-base font-semibold text-card-foreground',
|
|
88
|
+
description: 'text-sm text-muted-foreground',
|
|
89
|
+
action: 'shrink-0 flex-row items-center gap-1',
|
|
90
|
+
progress: 'text-xs font-medium text-muted-foreground',
|
|
91
|
+
trigger: 'h-8 w-8 items-center justify-center rounded-full active:bg-accent',
|
|
92
|
+
content: 'gap-2 px-4 pb-4',
|
|
93
|
+
steps: 'w-full',
|
|
94
|
+
step: 'w-full flex-row gap-3',
|
|
95
|
+
stepRail: 'w-4 items-center pt-px',
|
|
96
|
+
stepMarker: 'h-4 w-4 items-center justify-center rounded-full',
|
|
97
|
+
stepConnector: 'w-px flex-1 rounded-full',
|
|
98
|
+
stepBody: 'min-w-0 flex-1 gap-0.5 pb-3',
|
|
99
|
+
stepTitle: 'text-sm',
|
|
100
|
+
stepDescription: 'text-xs text-muted-foreground',
|
|
101
|
+
stepMeta:
|
|
102
|
+
'self-start rounded-md bg-muted px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground',
|
|
103
|
+
footer: 'flex-row items-center gap-2 border-t border-border p-4',
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
export type PlanStepStatus = 'pending' | 'active' | 'done' | 'skipped';
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* What each state does to a step's marker, its title and the rail below it.
|
|
111
|
+
*
|
|
112
|
+
* A lookup rather than a `tv()` variant because it belongs to `Plan.Step`, not
|
|
113
|
+
* to `Plan` — a variant here would be read off the root and documented as
|
|
114
|
+
* something you could pass to the card.
|
|
115
|
+
*
|
|
116
|
+
* The connector is filled behind a step that is finished, which is what turns
|
|
117
|
+
* the left-hand column into a progress bar stood on its end: how far the plan
|
|
118
|
+
* has got is readable from the rail alone, without counting markers.
|
|
119
|
+
*/
|
|
120
|
+
const stepStatus: Record<
|
|
121
|
+
PlanStepStatus,
|
|
122
|
+
{ marker: string; title: string; connector: string }
|
|
123
|
+
> = {
|
|
124
|
+
pending: {
|
|
125
|
+
marker: 'border border-border',
|
|
126
|
+
title: 'text-muted-foreground',
|
|
127
|
+
connector: 'bg-border',
|
|
128
|
+
},
|
|
129
|
+
active: {
|
|
130
|
+
marker: 'border-2 border-primary',
|
|
131
|
+
title: 'font-medium text-foreground',
|
|
132
|
+
connector: 'bg-border',
|
|
133
|
+
},
|
|
134
|
+
done: {
|
|
135
|
+
marker: 'bg-primary',
|
|
136
|
+
title: 'text-foreground',
|
|
137
|
+
connector: 'bg-primary',
|
|
138
|
+
},
|
|
139
|
+
skipped: {
|
|
140
|
+
marker: 'border border-dashed border-border',
|
|
141
|
+
title: 'text-muted-foreground line-through',
|
|
142
|
+
connector: 'bg-border',
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** How far down the rail the plan has got. Reported by `Plan.Steps`. */
|
|
147
|
+
export interface PlanStepCounts {
|
|
148
|
+
done: number;
|
|
149
|
+
total: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface PlanContextValue {
|
|
153
|
+
isStreaming: boolean;
|
|
154
|
+
open: boolean;
|
|
155
|
+
setOpen: (open: boolean) => void;
|
|
156
|
+
counts: PlanStepCounts | null;
|
|
157
|
+
reportCounts: (counts: PlanStepCounts) => void;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const PlanContext = createContext<PlanContextValue | null>(null);
|
|
161
|
+
|
|
162
|
+
function usePlan(component: string): PlanContextValue {
|
|
163
|
+
const context = useContext(PlanContext);
|
|
164
|
+
if (!context) {
|
|
165
|
+
throw new Error(`${component} must be used within a <Plan>`);
|
|
166
|
+
}
|
|
167
|
+
return context;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface PlanProps extends Omit<ViewProps, 'children'> {
|
|
171
|
+
className?: string;
|
|
172
|
+
/** Whether the plan is still being written. Shimmers the title and description. */
|
|
173
|
+
isStreaming?: boolean;
|
|
174
|
+
/** Controlled open state of the body. */
|
|
175
|
+
open?: boolean;
|
|
176
|
+
/** Initial state when uncontrolled. */
|
|
177
|
+
defaultOpen?: boolean;
|
|
178
|
+
onOpenChange?: (open: boolean) => void;
|
|
179
|
+
children?: ReactNode;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function PlanRoot({
|
|
183
|
+
className,
|
|
184
|
+
isStreaming = false,
|
|
185
|
+
open: openProp,
|
|
186
|
+
defaultOpen = true,
|
|
187
|
+
onOpenChange,
|
|
188
|
+
children,
|
|
189
|
+
...props
|
|
190
|
+
}: PlanProps) {
|
|
191
|
+
const { root } = planVariants();
|
|
192
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
193
|
+
const isControlled = openProp !== undefined;
|
|
194
|
+
const open = isControlled ? openProp : internalOpen;
|
|
195
|
+
|
|
196
|
+
const setOpen = useCallback(
|
|
197
|
+
(next: boolean) => {
|
|
198
|
+
if (!isControlled) setInternalOpen(next);
|
|
199
|
+
onOpenChange?.(next);
|
|
200
|
+
},
|
|
201
|
+
[isControlled, onOpenChange]
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// `Plan.Steps` counts its own children and reports up, so `Plan.Progress` can
|
|
205
|
+
// sit in the header — above the rail, and with no way to reach it — without
|
|
206
|
+
// the caller having to say how many steps there are twice.
|
|
207
|
+
const [counts, setCounts] = useState<PlanStepCounts | null>(null);
|
|
208
|
+
const reportCounts = useCallback((next: PlanStepCounts) => {
|
|
209
|
+
setCounts((current) =>
|
|
210
|
+
current && current.done === next.done && current.total === next.total ? current : next
|
|
211
|
+
);
|
|
212
|
+
}, []);
|
|
213
|
+
|
|
214
|
+
const context = useMemo(
|
|
215
|
+
() => ({ isStreaming, open, setOpen, counts, reportCounts }),
|
|
216
|
+
[isStreaming, open, setOpen, counts, reportCounts]
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<PlanContext.Provider value={context}>
|
|
221
|
+
<View {...props} className={cn(root(), className)}>
|
|
222
|
+
{children}
|
|
223
|
+
</View>
|
|
224
|
+
</PlanContext.Provider>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
PlanRoot.displayName = 'Plan';
|
|
228
|
+
|
|
229
|
+
export interface PlanHeaderProps extends ViewProps {
|
|
230
|
+
className?: string;
|
|
231
|
+
children?: ReactNode;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The title, the description and whatever acts on them.
|
|
236
|
+
*
|
|
237
|
+
* The heading is wrapped in a column of its own so the action stays pinned to
|
|
238
|
+
* the trailing edge as the title wraps, rather than riding down with it. An
|
|
239
|
+
* icon is pulled out to the leading edge for the same reason, in the other
|
|
240
|
+
* direction — a badge that rode down with a wrapping title would stop reading
|
|
241
|
+
* as a badge for the plan.
|
|
242
|
+
*/
|
|
243
|
+
function PlanHeader({ className, children, ...props }: PlanHeaderProps) {
|
|
244
|
+
const { header, heading } = planVariants();
|
|
245
|
+
|
|
246
|
+
const icons: ReactNode[] = [];
|
|
247
|
+
const headings: ReactNode[] = [];
|
|
248
|
+
const actions: ReactNode[] = [];
|
|
249
|
+
for (const child of Array.isArray(children) ? children : [children]) {
|
|
250
|
+
if (isType(child, PlanAction)) actions.push(child);
|
|
251
|
+
else if (isType(child, PlanIcon)) icons.push(child);
|
|
252
|
+
else headings.push(child);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<View {...props} className={cn(header(), className)}>
|
|
257
|
+
{icons}
|
|
258
|
+
<View className={heading()}>{headings}</View>
|
|
259
|
+
{actions}
|
|
260
|
+
</View>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isType(child: ReactNode, type: unknown): boolean {
|
|
265
|
+
return (
|
|
266
|
+
typeof child === 'object' &&
|
|
267
|
+
child !== null &&
|
|
268
|
+
'type' in child &&
|
|
269
|
+
(child as { type?: unknown }).type === type
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface PlanIconProps extends ViewProps {
|
|
274
|
+
className?: string;
|
|
275
|
+
children?: ReactNode;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A badge on the header's leading edge — what kind of plan this is. */
|
|
279
|
+
function PlanIcon({ className, children, ...props }: PlanIconProps) {
|
|
280
|
+
const { icon } = planVariants();
|
|
281
|
+
return (
|
|
282
|
+
<View {...props} className={cn(icon(), className)}>
|
|
283
|
+
{children}
|
|
284
|
+
</View>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export interface PlanTitleProps extends ViewProps {
|
|
289
|
+
className?: string;
|
|
290
|
+
children?: ReactNode;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** The plan's name. Shimmers while it is still being written. */
|
|
294
|
+
function PlanTitle({ className, children, ...props }: PlanTitleProps) {
|
|
295
|
+
const { isStreaming } = usePlan('Plan.Title');
|
|
296
|
+
const { title } = planVariants();
|
|
297
|
+
|
|
298
|
+
if (isStreaming && typeof children === 'string') {
|
|
299
|
+
return <Shimmer textClassName={cn(title(), className)}>{children}</Shimmer>;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<Text accessibilityRole="header" className={cn(title(), className)} {...props}>
|
|
304
|
+
{children}
|
|
305
|
+
</Text>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export interface PlanDescriptionProps extends ViewProps {
|
|
310
|
+
className?: string;
|
|
311
|
+
children?: ReactNode;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function PlanDescription({ className, children, ...props }: PlanDescriptionProps) {
|
|
315
|
+
const { isStreaming } = usePlan('Plan.Description');
|
|
316
|
+
const { description } = planVariants();
|
|
317
|
+
|
|
318
|
+
if (isStreaming && typeof children === 'string') {
|
|
319
|
+
return <Shimmer textClassName={cn(description(), className)}>{children}</Shimmer>;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
<Text className={cn(description(), className)} {...props}>
|
|
324
|
+
{children}
|
|
325
|
+
</Text>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export interface PlanActionProps extends ViewProps {
|
|
330
|
+
className?: string;
|
|
331
|
+
children?: ReactNode;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Pinned to the header's trailing edge — the toggle, a badge, a menu. */
|
|
335
|
+
function PlanAction({ className, children, ...props }: PlanActionProps) {
|
|
336
|
+
const { action } = planVariants();
|
|
337
|
+
return (
|
|
338
|
+
<View {...props} className={cn(action(), className)}>
|
|
339
|
+
{children}
|
|
340
|
+
</View>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export interface PlanTriggerProps extends Omit<PressableProps, 'children' | 'style'> {
|
|
345
|
+
className?: string;
|
|
346
|
+
children?: ReactNode;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Folds the body away. Its chevron turns to point at the state it will reach. */
|
|
350
|
+
function PlanTrigger({ className, children, onPress, ...props }: PlanTriggerProps) {
|
|
351
|
+
const { open, setOpen } = usePlan('Plan.Trigger');
|
|
352
|
+
const { trigger } = planVariants();
|
|
353
|
+
const reducedMotion = useReducedMotion();
|
|
354
|
+
const progress = useSharedValue(open ? 1 : 0);
|
|
355
|
+
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
progress.value = reducedMotion
|
|
358
|
+
? open
|
|
359
|
+
? 1
|
|
360
|
+
: 0
|
|
361
|
+
: withTiming(open ? 1 : 0, { duration: 180 });
|
|
362
|
+
}, [open, reducedMotion, progress]);
|
|
363
|
+
|
|
364
|
+
const chevronStyle = useAnimatedStyle(() => ({
|
|
365
|
+
transform: [{ rotate: `${progress.value * 180}deg` }],
|
|
366
|
+
}));
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<Pressable
|
|
370
|
+
accessibilityRole="button"
|
|
371
|
+
accessibilityLabel={open ? 'Collapse plan' : 'Expand plan'}
|
|
372
|
+
accessibilityState={{ expanded: open }}
|
|
373
|
+
hitSlop={6}
|
|
374
|
+
onPress={(event) => {
|
|
375
|
+
onPress?.(event);
|
|
376
|
+
setOpen(!open);
|
|
377
|
+
}}
|
|
378
|
+
className={cn(trigger(), className)}
|
|
379
|
+
{...props}
|
|
380
|
+
>
|
|
381
|
+
{children ?? (
|
|
382
|
+
<Animated.View style={chevronStyle}>
|
|
383
|
+
<ChevronDownIcon size={16} />
|
|
384
|
+
</Animated.View>
|
|
385
|
+
)}
|
|
386
|
+
</Pressable>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export interface PlanContentProps extends Omit<ViewProps, 'children'> {
|
|
391
|
+
className?: string;
|
|
392
|
+
children?: ReactNode;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** The steps. Collapses rather than unmounting, so it can still be growing. */
|
|
396
|
+
function PlanContent({ className, children, ...props }: PlanContentProps) {
|
|
397
|
+
const { open } = usePlan('Plan.Content');
|
|
398
|
+
const { content, description } = planVariants();
|
|
399
|
+
|
|
400
|
+
return (
|
|
401
|
+
<Collapse open={open} className={cn(content(), className)} {...props}>
|
|
402
|
+
{textChildren(children, (text) => (
|
|
403
|
+
<Text className={description()}>{text}</Text>
|
|
404
|
+
))}
|
|
405
|
+
</Collapse>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Where a step sits in the rail. Provided by `Plan.Steps` rather than passed
|
|
411
|
+
* down, so steps produced by a `.map()` through a component of your own still
|
|
412
|
+
* know they are last — a prop set on that wrapper would never reach the step.
|
|
413
|
+
*/
|
|
414
|
+
const PlanStepContext = createContext<{ last: boolean }>({ last: false });
|
|
415
|
+
|
|
416
|
+
export interface PlanStepsProps extends ViewProps {
|
|
417
|
+
className?: string;
|
|
418
|
+
children?: ReactNode;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* The rail. A plan is an ordered list of things that will happen, and this is
|
|
423
|
+
* the list — a paragraph describing the same four steps cannot be glanced at to
|
|
424
|
+
* see which one is running.
|
|
425
|
+
*
|
|
426
|
+
* It counts its own steps and reports the count up, which is what lets
|
|
427
|
+
* `Plan.Progress` sit in the header without the caller stating the total twice.
|
|
428
|
+
*/
|
|
429
|
+
function PlanSteps({ className, children, ...props }: PlanStepsProps) {
|
|
430
|
+
const { reportCounts } = usePlan('Plan.Steps');
|
|
431
|
+
const { steps } = planVariants();
|
|
432
|
+
|
|
433
|
+
const items = useMemo(
|
|
434
|
+
() => Children.toArray(children).filter((child) => isValidElement(child)),
|
|
435
|
+
[children]
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const counts = useMemo(() => {
|
|
439
|
+
let done = 0;
|
|
440
|
+
let total = 0;
|
|
441
|
+
for (const item of items) {
|
|
442
|
+
if (!isType(item, PlanStep)) continue;
|
|
443
|
+
total += 1;
|
|
444
|
+
// A skipped step is settled too — a plan that reports 2 of 4 while two
|
|
445
|
+
// more were deliberately passed over is reporting the wrong thing.
|
|
446
|
+
const status = (item as ReactElement<PlanStepProps>).props.status;
|
|
447
|
+
if (status === 'done' || status === 'skipped') done += 1;
|
|
448
|
+
}
|
|
449
|
+
return { done, total };
|
|
450
|
+
}, [items]);
|
|
451
|
+
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
reportCounts(counts);
|
|
454
|
+
}, [counts, reportCounts]);
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<View {...props} role="list" className={cn(steps(), className)}>
|
|
458
|
+
{items.map((item, index) => (
|
|
459
|
+
<PlanStepContext.Provider
|
|
460
|
+
// The step's own key stays on the element; this wrapper needs its own.
|
|
461
|
+
key={index}
|
|
462
|
+
value={{ last: index === items.length - 1 }}
|
|
463
|
+
>
|
|
464
|
+
{item}
|
|
465
|
+
</PlanStepContext.Provider>
|
|
466
|
+
))}
|
|
467
|
+
</View>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export interface PlanStepProps extends Omit<ViewProps, 'children'> {
|
|
472
|
+
className?: string;
|
|
473
|
+
/** How far this step has got. Decides the marker, the title and the rail below it. */
|
|
474
|
+
status?: PlanStepStatus;
|
|
475
|
+
/** A line under the title — what the step will touch, or what it found. */
|
|
476
|
+
description?: ReactNode;
|
|
477
|
+
/** A file path, a count, a duration. Rendered as a small mono chip. */
|
|
478
|
+
meta?: ReactNode;
|
|
479
|
+
/** Drop the connector below this step. `Plan.Steps` sets it for you. */
|
|
480
|
+
last?: boolean;
|
|
481
|
+
children?: ReactNode;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* One step. The marker says which of the four states it is in and the rail
|
|
486
|
+
* below it is filled once it is done, so the plan's progress is legible from
|
|
487
|
+
* the shape of the left-hand column alone.
|
|
488
|
+
*/
|
|
489
|
+
function PlanStep({
|
|
490
|
+
className,
|
|
491
|
+
status = 'pending',
|
|
492
|
+
description,
|
|
493
|
+
meta,
|
|
494
|
+
last,
|
|
495
|
+
children,
|
|
496
|
+
...props
|
|
497
|
+
}: PlanStepProps) {
|
|
498
|
+
const { isStreaming } = usePlan('Plan.Step');
|
|
499
|
+
const position = useContext(PlanStepContext);
|
|
500
|
+
const isLast = last ?? position.last;
|
|
501
|
+
const {
|
|
502
|
+
step,
|
|
503
|
+
stepRail,
|
|
504
|
+
stepMarker,
|
|
505
|
+
stepConnector,
|
|
506
|
+
stepBody,
|
|
507
|
+
stepTitle,
|
|
508
|
+
stepDescription,
|
|
509
|
+
stepMeta,
|
|
510
|
+
} = planVariants();
|
|
511
|
+
const tone = stepStatus[status];
|
|
512
|
+
const onPrimary = useCSSVariable('--color-primary-foreground');
|
|
513
|
+
|
|
514
|
+
const title = stepTitle({ className: tone.title });
|
|
515
|
+
// Only the step that is running is still arriving. Shimmering a finished one
|
|
516
|
+
// would say it is still being written, and shimmering the whole rail would
|
|
517
|
+
// say nothing at all.
|
|
518
|
+
const streamingTitle = isStreaming && status === 'active' && typeof children === 'string';
|
|
519
|
+
|
|
520
|
+
return (
|
|
521
|
+
<View {...props} role="listitem" className={cn(step(), className)}>
|
|
522
|
+
<View className={stepRail()}>
|
|
523
|
+
<View className={stepMarker({ className: tone.marker })}>
|
|
524
|
+
{status === 'done' ? (
|
|
525
|
+
<CheckIcon
|
|
526
|
+
size={10}
|
|
527
|
+
color={typeof onPrimary === 'string' ? onPrimary : '#ffffff'}
|
|
528
|
+
/>
|
|
529
|
+
) : status === 'active' ? (
|
|
530
|
+
<View className="h-1.5 w-1.5 rounded-full bg-primary" />
|
|
531
|
+
) : null}
|
|
532
|
+
</View>
|
|
533
|
+
{isLast ? null : <View className={stepConnector({ className: tone.connector })} />}
|
|
534
|
+
</View>
|
|
535
|
+
|
|
536
|
+
<View className={cn(stepBody(), isLast && 'pb-0')}>
|
|
537
|
+
{streamingTitle ? (
|
|
538
|
+
<Shimmer textClassName={title}>{children as string}</Shimmer>
|
|
539
|
+
) : (
|
|
540
|
+
textChildren(children, (text) => <Text className={title}>{text}</Text>)
|
|
541
|
+
)}
|
|
542
|
+
{textChildren(description, (text) => (
|
|
543
|
+
<Text className={stepDescription()}>{text}</Text>
|
|
544
|
+
))}
|
|
545
|
+
{meta === undefined ? null : (
|
|
546
|
+
<View className={stepMeta()}>
|
|
547
|
+
{textChildren(meta, (text) => (
|
|
548
|
+
<Text className="font-mono text-[11px] text-muted-foreground">{text}</Text>
|
|
549
|
+
))}
|
|
550
|
+
</View>
|
|
551
|
+
)}
|
|
552
|
+
</View>
|
|
553
|
+
</View>
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export interface PlanProgressProps extends TextProps {
|
|
558
|
+
className?: string;
|
|
559
|
+
/** Steps settled so far. Defaults to what `Plan.Steps` counted. */
|
|
560
|
+
value?: number;
|
|
561
|
+
/** Steps in total. Defaults to what `Plan.Steps` counted. */
|
|
562
|
+
total?: number;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* How far down the rail the plan has got, as `2 of 4`. Renders nothing until
|
|
567
|
+
* there is a rail to count — a plan with no steps has no progress to report,
|
|
568
|
+
* and `0 of 0` is worse than silence.
|
|
569
|
+
*/
|
|
570
|
+
function PlanProgress({ className, value, total, ...props }: PlanProgressProps) {
|
|
571
|
+
const { counts } = usePlan('Plan.Progress');
|
|
572
|
+
const { progress } = planVariants();
|
|
573
|
+
|
|
574
|
+
const done = value ?? counts?.done;
|
|
575
|
+
const all = total ?? counts?.total;
|
|
576
|
+
if (done === undefined || !all) return null;
|
|
577
|
+
|
|
578
|
+
return (
|
|
579
|
+
<Text className={cn(progress(), className)} {...props}>
|
|
580
|
+
{done} of {all}
|
|
581
|
+
</Text>
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export interface PlanFooterProps extends ViewProps {
|
|
586
|
+
className?: string;
|
|
587
|
+
/**
|
|
588
|
+
* How the actions divide the row. `stretch` splits it between them, which is
|
|
589
|
+
* what a phone wants: the decision is the point of the card, and the two
|
|
590
|
+
* buttons that make it should be the width of a thumb. `end` packs them
|
|
591
|
+
* against the trailing edge for a plan sitting inside something denser.
|
|
592
|
+
*/
|
|
593
|
+
layout?: 'stretch' | 'end';
|
|
594
|
+
children?: ReactNode;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Where the buttons that answer the plan go.
|
|
599
|
+
*
|
|
600
|
+
* Each action takes an equal share of the row by default. A pair of small
|
|
601
|
+
* buttons hugging the trailing corner is a pointer-and-cursor shape; on a phone
|
|
602
|
+
* the answer to "shall I do this" is the most important control on the screen
|
|
603
|
+
* and wants to be hit without aiming.
|
|
604
|
+
*/
|
|
605
|
+
function PlanFooter({ className, layout = 'stretch', children, ...props }: PlanFooterProps) {
|
|
606
|
+
const { footer } = planVariants();
|
|
607
|
+
|
|
608
|
+
return (
|
|
609
|
+
<View
|
|
610
|
+
{...props}
|
|
611
|
+
className={cn(footer(), layout === 'end' && 'justify-end', className)}
|
|
612
|
+
>
|
|
613
|
+
{layout === 'stretch'
|
|
614
|
+
? // A view stretches its children across the cross axis by default, so
|
|
615
|
+
// a button inside one of these fills it without being told to.
|
|
616
|
+
Children.map(children, (child) =>
|
|
617
|
+
isValidElement(child) ? <View className="flex-1">{child}</View> : child
|
|
618
|
+
)
|
|
619
|
+
: children}
|
|
620
|
+
</View>
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
PlanHeader.displayName = 'Plan.Header';
|
|
625
|
+
PlanIcon.displayName = 'Plan.Icon';
|
|
626
|
+
PlanTitle.displayName = 'Plan.Title';
|
|
627
|
+
PlanDescription.displayName = 'Plan.Description';
|
|
628
|
+
PlanAction.displayName = 'Plan.Action';
|
|
629
|
+
PlanProgress.displayName = 'Plan.Progress';
|
|
630
|
+
PlanTrigger.displayName = 'Plan.Trigger';
|
|
631
|
+
PlanContent.displayName = 'Plan.Content';
|
|
632
|
+
PlanSteps.displayName = 'Plan.Steps';
|
|
633
|
+
PlanStep.displayName = 'Plan.Step';
|
|
634
|
+
PlanFooter.displayName = 'Plan.Footer';
|
|
635
|
+
|
|
636
|
+
export const Plan = Object.assign(PlanRoot, {
|
|
637
|
+
Header: PlanHeader,
|
|
638
|
+
Icon: PlanIcon,
|
|
639
|
+
Title: PlanTitle,
|
|
640
|
+
Description: PlanDescription,
|
|
641
|
+
Action: PlanAction,
|
|
642
|
+
Progress: PlanProgress,
|
|
643
|
+
Trigger: PlanTrigger,
|
|
644
|
+
Content: PlanContent,
|
|
645
|
+
Steps: PlanSteps,
|
|
646
|
+
Step: PlanStep,
|
|
647
|
+
Footer: PlanFooter,
|
|
648
|
+
});
|