panelui-native 0.28.1 → 0.30.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 +18 -1
- package/lib/module/components/calendar/index.js +350 -115
- 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/menu/index.js +714 -0
- package/lib/module/components/menu/index.js.map +1 -0
- package/lib/module/components/plan/index.js +296 -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/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/sources/index.js +221 -0
- package/lib/module/components/sources/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 +139 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +8 -0
- 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/typescript/src/components/calendar/index.d.ts +68 -3
- 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/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 +127 -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/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/sources/index.d.ts +85 -0
- package/lib/typescript/src/components/sources/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 +14 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +10 -2
- 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/package.json +1 -1
- package/src/components/calendar/index.tsx +389 -122
- 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/menu/index.tsx +812 -0
- package/src/components/plan/index.tsx +325 -0
- package/src/components/popover/index.tsx +69 -2
- package/src/components/radio-group/index.tsx +11 -2
- package/src/components/reasoning/index.tsx +297 -0
- package/src/components/sources/index.tsx +278 -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 +102 -0
- package/src/index.ts +71 -0
- package/src/primitives/collapse.tsx +128 -0
- package/theme.css +112 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Menu — the list of things you can do to something.
|
|
5
|
+
*
|
|
6
|
+
* It is not a Select, and the difference is what the rows *are*. A select's
|
|
7
|
+
* rows are values: picking one answers a question the form asked, and the
|
|
8
|
+
* trigger then shows the answer. A menu's rows are verbs — rename, duplicate,
|
|
9
|
+
* delete — and the trigger goes on saying the same thing afterwards, because
|
|
10
|
+
* nothing about it was chosen. Anything built out of a popover and a column of
|
|
11
|
+
* pressables ends up re-deriving that distinction by hand, and re-deriving the
|
|
12
|
+
* roles, the dismiss-on-select rule and the destructive colour with it.
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <Menu>
|
|
16
|
+
* <Menu.Trigger>
|
|
17
|
+
* <Button variant="outline">Options</Button>
|
|
18
|
+
* </Menu.Trigger>
|
|
19
|
+
* <Menu.Content align="start">
|
|
20
|
+
* <Menu.Item icon={<PencilIcon size={16} />}>Rename</Menu.Item>
|
|
21
|
+
* <Menu.Separator />
|
|
22
|
+
* <Menu.Item variant="destructive" icon={<TrashIcon size={16} />}>
|
|
23
|
+
* Delete
|
|
24
|
+
* </Menu.Item>
|
|
25
|
+
* </Menu.Content>
|
|
26
|
+
* </Menu>
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* The panel is a `Popover` underneath, so the menu inherits its measuring,
|
|
30
|
+
* flipping and edge-clamping rather than owning a second copy of them — a menu
|
|
31
|
+
* near the bottom of the screen opens upwards for the same reason a popover
|
|
32
|
+
* does, and `presentation="bottom-sheet"` moves the same rows into a sheet.
|
|
33
|
+
*
|
|
34
|
+
* Submenus expand in place rather than flying out sideways. A flyout needs a
|
|
35
|
+
* pointer to travel from the parent row to the child panel without crossing
|
|
36
|
+
* anything that would close it; a finger has no such path, and a second panel
|
|
37
|
+
* hanging off the first is usually the thing that pushes a menu off the edge
|
|
38
|
+
* of a phone. Opening downwards into the panel keeps every row under the
|
|
39
|
+
* thumb that opened it.
|
|
40
|
+
*/
|
|
41
|
+
import { Children, createContext, isValidElement, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
42
|
+
import { Pressable, View } from 'react-native';
|
|
43
|
+
import Animated, { useAnimatedStyle, useReducedMotion, useSharedValue, withTiming } from 'react-native-reanimated';
|
|
44
|
+
import { tv } from 'tailwind-variants';
|
|
45
|
+
import { useCSSVariable } from 'uniwind';
|
|
46
|
+
import { CheckIcon, ChevronRightIcon } from "../../icons/index.js";
|
|
47
|
+
import { Text, textChildren } from "../../primitives/text.js";
|
|
48
|
+
import { useDirectionSign } from "../../hooks/use-direction.js";
|
|
49
|
+
import { selectionTick } from "../../utils/haptics.js";
|
|
50
|
+
import { cn } from "../../utils/cn.js";
|
|
51
|
+
import { Popover } from "../popover/index.js";
|
|
52
|
+
|
|
53
|
+
/** Side of the column every indicator is drawn in. */
|
|
54
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
55
|
+
const INDICATOR_SIZE = 18;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A theme token as a colour an icon will accept.
|
|
59
|
+
*
|
|
60
|
+
* `useCSSVariable` answers with whatever the token holds, which for a length
|
|
61
|
+
* or a number is not a colour at all — so anything that is not a string is
|
|
62
|
+
* dropped and the icon falls back to what it inherits.
|
|
63
|
+
*/
|
|
64
|
+
function useTint(variable) {
|
|
65
|
+
const raw = useCSSVariable(variable);
|
|
66
|
+
return typeof raw === 'string' ? raw : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** How long a row takes to light up under a finger, and to let go again. */
|
|
70
|
+
const PRESS_IN_DURATION = 90;
|
|
71
|
+
const PRESS_OUT_DURATION = 160;
|
|
72
|
+
|
|
73
|
+
/** How far a pressed row shrinks. Enough to feel, not enough to see move. */
|
|
74
|
+
const PRESS_SCALE = 0.98;
|
|
75
|
+
const menuVariants = tv({
|
|
76
|
+
slots: {
|
|
77
|
+
/*
|
|
78
|
+
* No background here: the panel's surface is a layer of its own, drawn by
|
|
79
|
+
* `Menu.Background` behind the rows, so that a caller can replace it with
|
|
80
|
+
* a gradient or a blur without also having to redraw the rows.
|
|
81
|
+
*/
|
|
82
|
+
content: 'gap-0.5 rounded-3xl p-1.5',
|
|
83
|
+
background: 'absolute inset-0 rounded-3xl bg-overlay',
|
|
84
|
+
label: 'px-3 pb-1 pt-2',
|
|
85
|
+
item: 'w-full flex-row items-center gap-2.5 rounded-2xl px-2.5 py-2.5',
|
|
86
|
+
itemLabel: 'text-base font-medium text-overlay-foreground',
|
|
87
|
+
itemDescription: 'text-sm',
|
|
88
|
+
shortcut: 'text-xs tracking-widest',
|
|
89
|
+
separator: 'my-1 h-px bg-border',
|
|
90
|
+
indicator: 'items-center justify-center'
|
|
91
|
+
},
|
|
92
|
+
variants: {
|
|
93
|
+
variant: {
|
|
94
|
+
// The pressed fill is animated rather than switched, so it is not a
|
|
95
|
+
// class here — only what the animation cannot carry.
|
|
96
|
+
default: {},
|
|
97
|
+
/*
|
|
98
|
+
* The row is tinted rather than only recoloured. A red word on an
|
|
99
|
+
* otherwise ordinary row is easy to read past at a glance, and this is
|
|
100
|
+
* the one row in the menu where reading past it is expensive.
|
|
101
|
+
*/
|
|
102
|
+
destructive: {
|
|
103
|
+
itemLabel: 'text-destructive'
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
disabled: {
|
|
107
|
+
true: {
|
|
108
|
+
item: 'opacity-[0.45]'
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
/** Reserves the indicator column on a row that has no indicator of its own. */
|
|
112
|
+
inset: {
|
|
113
|
+
true: {
|
|
114
|
+
item: 'ps-[38px]',
|
|
115
|
+
label: 'ps-[38px]'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
defaultVariants: {
|
|
120
|
+
variant: 'default',
|
|
121
|
+
disabled: false,
|
|
122
|
+
inset: false
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The pressed state of a row, animated rather than switched.
|
|
129
|
+
*
|
|
130
|
+
* `active:` swaps a class wholesale, which lands the fill in one frame and
|
|
131
|
+
* takes it away in one frame — on a row the size of a menu item that reads as
|
|
132
|
+
* a flash rather than as a press. Interpolating a shared value fades it in and
|
|
133
|
+
* back out on the UI thread, and carries a shrink along with it that a class
|
|
134
|
+
* cannot express at all.
|
|
135
|
+
*/
|
|
136
|
+
function useMenuPress(variant) {
|
|
137
|
+
const reduced = useReducedMotion();
|
|
138
|
+
const pressed = useSharedValue(0);
|
|
139
|
+
const accent = useTint('--color-accent');
|
|
140
|
+
const destructive = useTint('--color-destructive-subtle');
|
|
141
|
+
const fill = variant === 'destructive' ? destructive : accent;
|
|
142
|
+
|
|
143
|
+
/*
|
|
144
|
+
* The fill is a layer with an animated opacity rather than an animated
|
|
145
|
+
* `backgroundColor`, because crossing *from* transparent needs a colour to
|
|
146
|
+
* cross from and the row has none of its own — what is behind it is whatever
|
|
147
|
+
* the panel's background layer happens to be. Fading the row itself would
|
|
148
|
+
* take the label with it, so only the fill fades.
|
|
149
|
+
*/
|
|
150
|
+
const fillStyle = useAnimatedStyle(() => ({
|
|
151
|
+
opacity: pressed.value
|
|
152
|
+
}));
|
|
153
|
+
const rowStyle = useAnimatedStyle(() => ({
|
|
154
|
+
transform: [{
|
|
155
|
+
scale: 1 - (1 - PRESS_SCALE) * pressed.value
|
|
156
|
+
}]
|
|
157
|
+
}));
|
|
158
|
+
return {
|
|
159
|
+
rowStyle,
|
|
160
|
+
fillStyle,
|
|
161
|
+
fill,
|
|
162
|
+
onPressIn: () => {
|
|
163
|
+
pressed.value = reduced ? 1 : withTiming(1, {
|
|
164
|
+
duration: PRESS_IN_DURATION
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
onPressOut: () => {
|
|
168
|
+
pressed.value = reduced ? 0 : withTiming(0, {
|
|
169
|
+
duration: PRESS_OUT_DURATION
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const MenuContext = /*#__PURE__*/createContext(null);
|
|
175
|
+
function useMenu(component) {
|
|
176
|
+
const context = useContext(MenuContext);
|
|
177
|
+
if (!context) {
|
|
178
|
+
throw new Error(`${component} must be used within a <Menu>`);
|
|
179
|
+
}
|
|
180
|
+
return context;
|
|
181
|
+
}
|
|
182
|
+
function MenuRoot({
|
|
183
|
+
children,
|
|
184
|
+
open,
|
|
185
|
+
onOpenChange,
|
|
186
|
+
defaultOpen,
|
|
187
|
+
haptics = false,
|
|
188
|
+
...props
|
|
189
|
+
}) {
|
|
190
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);
|
|
191
|
+
const isControlled = open !== undefined;
|
|
192
|
+
|
|
193
|
+
/*
|
|
194
|
+
* The open state is mirrored here as well as inside the popover, because a
|
|
195
|
+
* row has to be able to dismiss the panel it is sitting in and the popover
|
|
196
|
+
* only publishes that through `Popover.Close` — which wraps a child and so
|
|
197
|
+
* cannot be handed to a row that already owns its own press handler.
|
|
198
|
+
*/
|
|
199
|
+
const setOpen = useCallback(next => {
|
|
200
|
+
if (!isControlled) setInternalOpen(next);
|
|
201
|
+
onOpenChange?.(next);
|
|
202
|
+
}, [isControlled, onOpenChange]);
|
|
203
|
+
const context = useMemo(() => ({
|
|
204
|
+
close: () => setOpen(false),
|
|
205
|
+
haptics
|
|
206
|
+
}), [setOpen, haptics]);
|
|
207
|
+
return /*#__PURE__*/_jsx(MenuContext.Provider, {
|
|
208
|
+
value: context,
|
|
209
|
+
children: /*#__PURE__*/_jsx(Popover, {
|
|
210
|
+
open: isControlled ? open : internalOpen,
|
|
211
|
+
onOpenChange: setOpen,
|
|
212
|
+
...props,
|
|
213
|
+
children: children
|
|
214
|
+
})
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/** Wraps a single child and opens the menu on press. Also what gets measured. */
|
|
218
|
+
function MenuTrigger({
|
|
219
|
+
children
|
|
220
|
+
}) {
|
|
221
|
+
return /*#__PURE__*/_jsx(Popover.Trigger, {
|
|
222
|
+
children: children
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The panel, and the thing screen readers announce as a menu.
|
|
227
|
+
*
|
|
228
|
+
* Its padding is the row gutter rather than the popover's content padding —
|
|
229
|
+
* rows run to the panel's inner edge so that a pressed row's highlight reads
|
|
230
|
+
* as part of the panel instead of a floating chip inside it.
|
|
231
|
+
*
|
|
232
|
+
* The surface is a layer rather than a background on the panel itself, so that
|
|
233
|
+
* a caller can put something *behind* the rows. Pass a `Menu.Background` of
|
|
234
|
+
* your own with a gradient, an image or a blur inside it and it replaces the
|
|
235
|
+
* default one; pass nothing and the default is drawn for you.
|
|
236
|
+
*/
|
|
237
|
+
function MenuContent({
|
|
238
|
+
className,
|
|
239
|
+
children,
|
|
240
|
+
scrollable = true,
|
|
241
|
+
...props
|
|
242
|
+
}) {
|
|
243
|
+
const {
|
|
244
|
+
content
|
|
245
|
+
} = menuVariants();
|
|
246
|
+
const context = useMenu('Menu.Content');
|
|
247
|
+
|
|
248
|
+
/*
|
|
249
|
+
* A caller's own background replaces the default rather than stacking on top
|
|
250
|
+
* of it, and it is lifted out of the children so that it lands outside the
|
|
251
|
+
* scroller — a surface inside one scrolls away with the rows sitting on it.
|
|
252
|
+
*/
|
|
253
|
+
const items = [];
|
|
254
|
+
let background = null;
|
|
255
|
+
for (const child of Children.toArray(children)) {
|
|
256
|
+
if (/*#__PURE__*/isValidElement(child) && child.type === MenuBackground) {
|
|
257
|
+
background = child;
|
|
258
|
+
} else {
|
|
259
|
+
items.push(child);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return /*#__PURE__*/_jsx(Popover.Content, {
|
|
263
|
+
accessibilityRole: "menu",
|
|
264
|
+
scrollable: scrollable,
|
|
265
|
+
unstyled: true,
|
|
266
|
+
background: background ?? /*#__PURE__*/_jsx(MenuBackground, {}),
|
|
267
|
+
className: cn(content(), 'shadow-lg', className),
|
|
268
|
+
...props,
|
|
269
|
+
children: /*#__PURE__*/_jsx(MenuContext.Provider, {
|
|
270
|
+
value: context,
|
|
271
|
+
children: textChildren(items)
|
|
272
|
+
})
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* The panel's surface, drawn behind every row.
|
|
277
|
+
*
|
|
278
|
+
* It exists as a part rather than as a background on the panel because a
|
|
279
|
+
* background cannot be got behind. A menu that wants to be frosted, tinted or
|
|
280
|
+
* gradient-filled needs something under the rows and over nothing, and
|
|
281
|
+
* `overflow-hidden` on the panel is what keeps whatever that is inside the
|
|
282
|
+
* corner radius.
|
|
283
|
+
*/
|
|
284
|
+
function MenuBackground({
|
|
285
|
+
className,
|
|
286
|
+
children,
|
|
287
|
+
...props
|
|
288
|
+
}) {
|
|
289
|
+
const {
|
|
290
|
+
background
|
|
291
|
+
} = menuVariants();
|
|
292
|
+
return /*#__PURE__*/_jsx(View, {
|
|
293
|
+
accessibilityElementsHidden: true,
|
|
294
|
+
importantForAccessibility: "no-hide-descendants",
|
|
295
|
+
pointerEvents: "none",
|
|
296
|
+
className: cn(background(), className),
|
|
297
|
+
...props,
|
|
298
|
+
children: children
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/** Non-interactive heading over a run of rows. */
|
|
302
|
+
function MenuLabel({
|
|
303
|
+
className,
|
|
304
|
+
inset,
|
|
305
|
+
children,
|
|
306
|
+
...props
|
|
307
|
+
}) {
|
|
308
|
+
const {
|
|
309
|
+
label
|
|
310
|
+
} = menuVariants({
|
|
311
|
+
inset
|
|
312
|
+
});
|
|
313
|
+
return /*#__PURE__*/_jsx(View, {
|
|
314
|
+
accessibilityRole: "header",
|
|
315
|
+
className: cn(label(), className),
|
|
316
|
+
...props,
|
|
317
|
+
children: textChildren(children, text => /*#__PURE__*/_jsx(Text, {
|
|
318
|
+
size: "xs",
|
|
319
|
+
weight: "medium",
|
|
320
|
+
muted: true,
|
|
321
|
+
className: "uppercase tracking-wide",
|
|
322
|
+
children: text
|
|
323
|
+
}))
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
/** Hairline between two runs of rows. */
|
|
327
|
+
function MenuSeparator({
|
|
328
|
+
className,
|
|
329
|
+
...props
|
|
330
|
+
}) {
|
|
331
|
+
const {
|
|
332
|
+
separator
|
|
333
|
+
} = menuVariants();
|
|
334
|
+
return /*#__PURE__*/_jsx(View, {
|
|
335
|
+
accessibilityElementsHidden: true,
|
|
336
|
+
importantForAccessibility: "no-hide-descendants",
|
|
337
|
+
className: cn(separator(), className),
|
|
338
|
+
...props
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* One row of the menu.
|
|
343
|
+
*
|
|
344
|
+
* `onSelect` rather than `onPress` is the handler that closes the panel:
|
|
345
|
+
* `onPress` is still forwarded and still fires, so a row can keep whatever
|
|
346
|
+
* press behaviour it had, but the dismissal is tied to the semantic event so
|
|
347
|
+
* that a row which is disabled or which opts out via `closeOnSelect` behaves
|
|
348
|
+
* the same either way.
|
|
349
|
+
*/
|
|
350
|
+
function MenuItem({
|
|
351
|
+
className,
|
|
352
|
+
children,
|
|
353
|
+
icon,
|
|
354
|
+
description,
|
|
355
|
+
shortcut,
|
|
356
|
+
trailing,
|
|
357
|
+
variant = 'default',
|
|
358
|
+
inset,
|
|
359
|
+
disabled = false,
|
|
360
|
+
closeOnSelect = true,
|
|
361
|
+
onSelect,
|
|
362
|
+
onPress,
|
|
363
|
+
...props
|
|
364
|
+
}) {
|
|
365
|
+
const {
|
|
366
|
+
close,
|
|
367
|
+
haptics
|
|
368
|
+
} = useMenu('Menu.Item');
|
|
369
|
+
const slots = menuVariants({
|
|
370
|
+
variant,
|
|
371
|
+
disabled,
|
|
372
|
+
inset: inset && !icon
|
|
373
|
+
});
|
|
374
|
+
const press = useMenuPress(variant);
|
|
375
|
+
const handlePress = (...args) => {
|
|
376
|
+
if (disabled) return;
|
|
377
|
+
if (haptics) selectionTick();
|
|
378
|
+
onPress?.(...args);
|
|
379
|
+
onSelect?.();
|
|
380
|
+
if (closeOnSelect) close();
|
|
381
|
+
};
|
|
382
|
+
return /*#__PURE__*/_jsxs(AnimatedPressable, {
|
|
383
|
+
accessibilityRole: "menuitem",
|
|
384
|
+
accessibilityState: {
|
|
385
|
+
disabled
|
|
386
|
+
},
|
|
387
|
+
disabled: disabled,
|
|
388
|
+
onPress: handlePress,
|
|
389
|
+
onPressIn: press.onPressIn,
|
|
390
|
+
onPressOut: press.onPressOut,
|
|
391
|
+
style: press.rowStyle,
|
|
392
|
+
className: cn(slots.item(), 'overflow-hidden', className),
|
|
393
|
+
...props,
|
|
394
|
+
children: [/*#__PURE__*/_jsx(Animated.View, {
|
|
395
|
+
pointerEvents: "none",
|
|
396
|
+
className: "absolute inset-0",
|
|
397
|
+
style: [press.fillStyle, {
|
|
398
|
+
backgroundColor: press.fill
|
|
399
|
+
}]
|
|
400
|
+
}), icon ? /*#__PURE__*/_jsx(MenuIndicatorSlot, {
|
|
401
|
+
children: icon
|
|
402
|
+
}) : null, /*#__PURE__*/_jsxs(View, {
|
|
403
|
+
className: "flex-1",
|
|
404
|
+
children: [textChildren(children, text => /*#__PURE__*/_jsx(Text, {
|
|
405
|
+
className: slots.itemLabel(),
|
|
406
|
+
children: text
|
|
407
|
+
})), description ? /*#__PURE__*/_jsx(Text, {
|
|
408
|
+
size: "xs",
|
|
409
|
+
muted: true,
|
|
410
|
+
className: slots.itemDescription(),
|
|
411
|
+
children: description
|
|
412
|
+
}) : null]
|
|
413
|
+
}), shortcut ? /*#__PURE__*/_jsx(Text, {
|
|
414
|
+
size: "xs",
|
|
415
|
+
muted: true,
|
|
416
|
+
className: slots.shortcut(),
|
|
417
|
+
children: shortcut
|
|
418
|
+
}) : null, trailing]
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Fixed-width column the leading glyph sits in.
|
|
424
|
+
*
|
|
425
|
+
* Fixed rather than sized to its contents so that a checkmark appearing and
|
|
426
|
+
* disappearing does not shift the label beside it, and so that rows with and
|
|
427
|
+
* without a glyph line their labels up in the same panel.
|
|
428
|
+
*/
|
|
429
|
+
function MenuIndicatorSlot({
|
|
430
|
+
children
|
|
431
|
+
}) {
|
|
432
|
+
const {
|
|
433
|
+
indicator
|
|
434
|
+
} = menuVariants();
|
|
435
|
+
return /*#__PURE__*/_jsx(View, {
|
|
436
|
+
className: indicator(),
|
|
437
|
+
style: {
|
|
438
|
+
width: INDICATOR_SIZE,
|
|
439
|
+
height: INDICATOR_SIZE
|
|
440
|
+
},
|
|
441
|
+
children: children
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Placeholder that keeps the indicator column open on an unmarked row.
|
|
447
|
+
*
|
|
448
|
+
* `Menu.Item` draws no column at all when it has no icon, which is right for a
|
|
449
|
+
* row that will never have one — but wrong for a row whose mark appears and
|
|
450
|
+
* disappears as it is toggled.
|
|
451
|
+
*/
|
|
452
|
+
function MenuIndicatorSpacer() {
|
|
453
|
+
return /*#__PURE__*/_jsx(View, {});
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* A row that carries a state instead of running an action.
|
|
457
|
+
*
|
|
458
|
+
* It keeps the menu open by default, which is the opposite of `Menu.Item` and
|
|
459
|
+
* deliberately so: a set of toggles is nearly always set more than one at a
|
|
460
|
+
* time, and closing after each one turns three taps into six.
|
|
461
|
+
*/
|
|
462
|
+
function MenuCheckboxItem({
|
|
463
|
+
checked = false,
|
|
464
|
+
onCheckedChange,
|
|
465
|
+
onSelect,
|
|
466
|
+
closeOnSelect = false,
|
|
467
|
+
...props
|
|
468
|
+
}) {
|
|
469
|
+
const tint = useTint('--color-popover-foreground');
|
|
470
|
+
return /*#__PURE__*/_jsx(MenuItem, {
|
|
471
|
+
accessibilityRole: "menuitem",
|
|
472
|
+
accessibilityState: {
|
|
473
|
+
checked,
|
|
474
|
+
disabled: props.disabled ?? false
|
|
475
|
+
},
|
|
476
|
+
closeOnSelect: closeOnSelect
|
|
477
|
+
// An empty column rather than no column: the mark comes and goes as the
|
|
478
|
+
// row is toggled, and a row whose label steps sideways each time reads
|
|
479
|
+
// as a different row rather than the same one in a new state.
|
|
480
|
+
,
|
|
481
|
+
icon: checked ? /*#__PURE__*/_jsx(CheckIcon, {
|
|
482
|
+
size: 16,
|
|
483
|
+
color: tint
|
|
484
|
+
}) : /*#__PURE__*/_jsx(MenuIndicatorSpacer, {}),
|
|
485
|
+
onSelect: () => {
|
|
486
|
+
onCheckedChange?.(!checked);
|
|
487
|
+
onSelect?.();
|
|
488
|
+
},
|
|
489
|
+
...props
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
const MenuRadioContext = /*#__PURE__*/createContext(null);
|
|
493
|
+
/** A run of rows of which exactly one is chosen. */
|
|
494
|
+
function MenuRadioGroup({
|
|
495
|
+
value,
|
|
496
|
+
onValueChange,
|
|
497
|
+
className,
|
|
498
|
+
children,
|
|
499
|
+
...props
|
|
500
|
+
}) {
|
|
501
|
+
const context = useMemo(() => ({
|
|
502
|
+
value,
|
|
503
|
+
select: next => onValueChange?.(next)
|
|
504
|
+
}), [value, onValueChange]);
|
|
505
|
+
return /*#__PURE__*/_jsx(MenuRadioContext.Provider, {
|
|
506
|
+
value: context,
|
|
507
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
508
|
+
accessibilityRole: "radiogroup",
|
|
509
|
+
className: className,
|
|
510
|
+
...props,
|
|
511
|
+
children: children
|
|
512
|
+
})
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
/** One option inside a `Menu.RadioGroup`. */
|
|
516
|
+
function MenuRadioItem({
|
|
517
|
+
value,
|
|
518
|
+
indicator = 'check',
|
|
519
|
+
onSelect,
|
|
520
|
+
closeOnSelect = true,
|
|
521
|
+
...props
|
|
522
|
+
}) {
|
|
523
|
+
const group = useContext(MenuRadioContext);
|
|
524
|
+
if (!group) {
|
|
525
|
+
throw new Error('Menu.RadioItem must be used within a <Menu.RadioGroup>');
|
|
526
|
+
}
|
|
527
|
+
const tint = useTint('--color-popover-foreground');
|
|
528
|
+
const selected = group.value === value;
|
|
529
|
+
return /*#__PURE__*/_jsx(MenuItem, {
|
|
530
|
+
accessibilityRole: "menuitem",
|
|
531
|
+
accessibilityState: {
|
|
532
|
+
selected,
|
|
533
|
+
disabled: props.disabled ?? false
|
|
534
|
+
},
|
|
535
|
+
closeOnSelect: closeOnSelect,
|
|
536
|
+
icon: selected ? indicator === 'dot' ? /*#__PURE__*/_jsx(View, {
|
|
537
|
+
className: "h-2 w-2 rounded-full bg-popover-foreground"
|
|
538
|
+
}) : /*#__PURE__*/_jsx(CheckIcon, {
|
|
539
|
+
size: 16,
|
|
540
|
+
color: tint
|
|
541
|
+
}) :
|
|
542
|
+
/*#__PURE__*/
|
|
543
|
+
// Held open, so the options stay in one column as the choice moves
|
|
544
|
+
// between them.
|
|
545
|
+
_jsx(MenuIndicatorSpacer, {}),
|
|
546
|
+
onSelect: () => {
|
|
547
|
+
group.select(value);
|
|
548
|
+
onSelect?.();
|
|
549
|
+
},
|
|
550
|
+
...props
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
const MenuSubContext = /*#__PURE__*/createContext(null);
|
|
554
|
+
function useMenuSub(component) {
|
|
555
|
+
const context = useContext(MenuSubContext);
|
|
556
|
+
if (!context) {
|
|
557
|
+
throw new Error(`${component} must be used within a <Menu.Sub>`);
|
|
558
|
+
}
|
|
559
|
+
return context;
|
|
560
|
+
}
|
|
561
|
+
/** Groups a `Menu.SubTrigger` with the rows it reveals. */
|
|
562
|
+
function MenuSub({
|
|
563
|
+
children,
|
|
564
|
+
defaultOpen = false,
|
|
565
|
+
open,
|
|
566
|
+
onOpenChange
|
|
567
|
+
}) {
|
|
568
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
569
|
+
const isControlled = open !== undefined;
|
|
570
|
+
const resolved = isControlled ? open : internalOpen;
|
|
571
|
+
const context = useMemo(() => ({
|
|
572
|
+
open: resolved,
|
|
573
|
+
toggle: () => {
|
|
574
|
+
if (!isControlled) setInternalOpen(!resolved);
|
|
575
|
+
onOpenChange?.(!resolved);
|
|
576
|
+
}
|
|
577
|
+
}), [resolved, isControlled, onOpenChange]);
|
|
578
|
+
return /*#__PURE__*/_jsx(MenuSubContext.Provider, {
|
|
579
|
+
value: context,
|
|
580
|
+
children: children
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* The row that opens a submenu.
|
|
585
|
+
*
|
|
586
|
+
* Its chevron points along the reading direction while closed and turns to
|
|
587
|
+
* point down once open, so the row states which way its rows will appear
|
|
588
|
+
* rather than only that it has some.
|
|
589
|
+
*/
|
|
590
|
+
function MenuSubTrigger({
|
|
591
|
+
className,
|
|
592
|
+
children,
|
|
593
|
+
icon,
|
|
594
|
+
onSelect,
|
|
595
|
+
...props
|
|
596
|
+
}) {
|
|
597
|
+
const {
|
|
598
|
+
open,
|
|
599
|
+
toggle
|
|
600
|
+
} = useMenuSub('Menu.SubTrigger');
|
|
601
|
+
const sign = useDirectionSign();
|
|
602
|
+
const reducedMotion = useReducedMotion();
|
|
603
|
+
const chevronTint = useTint('--color-muted-foreground');
|
|
604
|
+
const progress = useSharedValue(open ? 1 : 0);
|
|
605
|
+
useEffect(() => {
|
|
606
|
+
progress.value = reducedMotion ? open ? 1 : 0 : withTiming(open ? 1 : 0, {
|
|
607
|
+
duration: 180
|
|
608
|
+
});
|
|
609
|
+
}, [open, reducedMotion, progress]);
|
|
610
|
+
|
|
611
|
+
// 90° takes a chevron from pointing along the line of text to pointing down
|
|
612
|
+
// into the rows it is about to reveal. Mirrored under RTL, where the closed
|
|
613
|
+
// state points the other way to begin with.
|
|
614
|
+
const chevronStyle = useAnimatedStyle(() => ({
|
|
615
|
+
transform: [{
|
|
616
|
+
rotate: `${progress.value * 90 * sign}deg`
|
|
617
|
+
}]
|
|
618
|
+
}));
|
|
619
|
+
return /*#__PURE__*/_jsx(MenuItem, {
|
|
620
|
+
accessibilityState: {
|
|
621
|
+
expanded: open,
|
|
622
|
+
disabled: props.disabled ?? false
|
|
623
|
+
},
|
|
624
|
+
closeOnSelect: false,
|
|
625
|
+
icon: icon,
|
|
626
|
+
className: className,
|
|
627
|
+
trailing: /*#__PURE__*/_jsx(Animated.View, {
|
|
628
|
+
style: chevronStyle,
|
|
629
|
+
children: /*#__PURE__*/_jsx(ChevronRightIcon, {
|
|
630
|
+
size: 16,
|
|
631
|
+
color: chevronTint
|
|
632
|
+
})
|
|
633
|
+
}),
|
|
634
|
+
onSelect: () => {
|
|
635
|
+
toggle();
|
|
636
|
+
onSelect?.();
|
|
637
|
+
},
|
|
638
|
+
...props,
|
|
639
|
+
children: children
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* The rows a submenu reveals, opening in place.
|
|
644
|
+
*
|
|
645
|
+
* The height is animated from a measurement rather than left to a layout
|
|
646
|
+
* animation, and the measured copy is absolutely positioned so it always lays
|
|
647
|
+
* out at its natural size — a child of a view whose height is mid-animation
|
|
648
|
+
* would otherwise report the animated height back, and the panel would settle
|
|
649
|
+
* at whatever it happened to measure on the first frame.
|
|
650
|
+
*/
|
|
651
|
+
function MenuSubContent({
|
|
652
|
+
className,
|
|
653
|
+
children,
|
|
654
|
+
...props
|
|
655
|
+
}) {
|
|
656
|
+
const {
|
|
657
|
+
open
|
|
658
|
+
} = useMenuSub('Menu.SubContent');
|
|
659
|
+
const reducedMotion = useReducedMotion();
|
|
660
|
+
const [height, setHeight] = useState(0);
|
|
661
|
+
const progress = useSharedValue(open ? 1 : 0);
|
|
662
|
+
useEffect(() => {
|
|
663
|
+
progress.value = reducedMotion ? open ? 1 : 0 : withTiming(open ? 1 : 0, {
|
|
664
|
+
duration: 200
|
|
665
|
+
});
|
|
666
|
+
}, [open, reducedMotion, progress]);
|
|
667
|
+
const style = useAnimatedStyle(() => ({
|
|
668
|
+
height: progress.value * height,
|
|
669
|
+
opacity: progress.value
|
|
670
|
+
}));
|
|
671
|
+
return /*#__PURE__*/_jsx(Animated.View, {
|
|
672
|
+
style: style,
|
|
673
|
+
className: "overflow-hidden",
|
|
674
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
675
|
+
onLayout: event => setHeight(event.nativeEvent.layout.height),
|
|
676
|
+
style: {
|
|
677
|
+
position: 'absolute',
|
|
678
|
+
left: 0,
|
|
679
|
+
right: 0,
|
|
680
|
+
top: 0
|
|
681
|
+
},
|
|
682
|
+
className: cn('ms-4 gap-0.5 border-s border-border ps-1', className),
|
|
683
|
+
...props,
|
|
684
|
+
children: children
|
|
685
|
+
})
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
MenuTrigger.displayName = 'Menu.Trigger';
|
|
689
|
+
MenuContent.displayName = 'Menu.Content';
|
|
690
|
+
MenuBackground.displayName = 'Menu.Background';
|
|
691
|
+
MenuLabel.displayName = 'Menu.Label';
|
|
692
|
+
MenuItem.displayName = 'Menu.Item';
|
|
693
|
+
MenuCheckboxItem.displayName = 'Menu.CheckboxItem';
|
|
694
|
+
MenuRadioGroup.displayName = 'Menu.RadioGroup';
|
|
695
|
+
MenuRadioItem.displayName = 'Menu.RadioItem';
|
|
696
|
+
MenuSeparator.displayName = 'Menu.Separator';
|
|
697
|
+
MenuSub.displayName = 'Menu.Sub';
|
|
698
|
+
MenuSubTrigger.displayName = 'Menu.SubTrigger';
|
|
699
|
+
MenuSubContent.displayName = 'Menu.SubContent';
|
|
700
|
+
export const Menu = Object.assign(MenuRoot, {
|
|
701
|
+
Trigger: MenuTrigger,
|
|
702
|
+
Content: MenuContent,
|
|
703
|
+
Background: MenuBackground,
|
|
704
|
+
Label: MenuLabel,
|
|
705
|
+
Item: MenuItem,
|
|
706
|
+
CheckboxItem: MenuCheckboxItem,
|
|
707
|
+
RadioGroup: MenuRadioGroup,
|
|
708
|
+
RadioItem: MenuRadioItem,
|
|
709
|
+
Separator: MenuSeparator,
|
|
710
|
+
Sub: MenuSub,
|
|
711
|
+
SubTrigger: MenuSubTrigger,
|
|
712
|
+
SubContent: MenuSubContent
|
|
713
|
+
});
|
|
714
|
+
//# sourceMappingURL=index.js.map
|