panelui-native 0.35.2 → 0.38.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 +12 -1
- package/lib/module/components/combobox/index.js +718 -0
- package/lib/module/components/combobox/index.js.map +1 -0
- package/lib/module/components/form/use-field.js +18 -0
- package/lib/module/components/form/use-field.js.map +1 -1
- package/lib/module/components/form/use-form.js +91 -8
- package/lib/module/components/form/use-form.js.map +1 -1
- package/lib/module/components/input/index.js +142 -32
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/otp-input/index.js +4 -1
- package/lib/module/components/otp-input/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +1 -1
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/select/index.js +116 -15
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/textarea/index.js +3 -1
- package/lib/module/components/textarea/index.js.map +1 -1
- package/lib/module/components/time-picker/index.js +801 -0
- package/lib/module/components/time-picker/index.js.map +1 -0
- package/lib/module/icons/index.js +59 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +4 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/theme/use-theme.js +1 -1
- package/lib/module/utils/time.js +180 -0
- package/lib/module/utils/time.js.map +1 -0
- package/lib/typescript/src/components/combobox/index.d.ts +164 -0
- package/lib/typescript/src/components/combobox/index.d.ts.map +1 -0
- package/lib/typescript/src/components/form/use-field.d.ts.map +1 -1
- package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
- package/lib/typescript/src/components/input/index.d.ts +106 -0
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/otp-input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/select/index.d.ts +32 -0
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/textarea/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/index.d.ts +104 -0
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +4 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/theme/use-theme.d.ts +1 -1
- package/lib/typescript/src/utils/time.d.ts +88 -0
- package/lib/typescript/src/utils/time.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/combobox/index.tsx +927 -0
- package/src/components/form/use-field.ts +23 -0
- package/src/components/form/use-form.ts +106 -16
- package/src/components/input/index.tsx +185 -29
- package/src/components/otp-input/index.tsx +4 -1
- package/src/components/panelside/index.tsx +1 -1
- package/src/components/select/index.tsx +114 -15
- package/src/components/textarea/index.tsx +3 -1
- package/src/components/time-picker/index.tsx +971 -0
- package/src/icons/index.tsx +47 -0
- package/src/index.ts +35 -0
- package/src/theme/use-theme.ts +1 -1
- package/src/utils/time.ts +192 -0
- package/theme.css +32 -0
|
@@ -26,9 +26,16 @@
|
|
|
26
26
|
* the option labels. The filter narrows what is *shown* — the declared options
|
|
27
27
|
* are still the source of truth, so nothing has to be lifted into state to make
|
|
28
28
|
* it work.
|
|
29
|
+
*
|
|
30
|
+
* A list long enough to need a filter is usually long enough to want dividing,
|
|
31
|
+
* so options can be wrapped in `Select.Group` under a heading. Grouping is
|
|
32
|
+
* presentational — the value is still a flat string — and the filter reaches
|
|
33
|
+
* through it, dropping any group the query empties rather than leaving a
|
|
34
|
+
* heading standing over nothing.
|
|
29
35
|
*/
|
|
30
36
|
import {
|
|
31
37
|
Children,
|
|
38
|
+
cloneElement,
|
|
32
39
|
createContext,
|
|
33
40
|
isValidElement,
|
|
34
41
|
useCallback,
|
|
@@ -36,6 +43,7 @@ import {
|
|
|
36
43
|
useMemo,
|
|
37
44
|
useRef,
|
|
38
45
|
useState,
|
|
46
|
+
type ReactElement,
|
|
39
47
|
type ReactNode,
|
|
40
48
|
} from 'react';
|
|
41
49
|
import {
|
|
@@ -59,6 +67,7 @@ import { getNativeUI } from '../../native';
|
|
|
59
67
|
import { Portal } from '../../primitives/portal';
|
|
60
68
|
import { Text, textChildren } from '../../primitives/text';
|
|
61
69
|
import { useBackHandler } from '../../hooks/use-back-handler';
|
|
70
|
+
import { cn } from '../../utils/cn';
|
|
62
71
|
import { BottomSheet } from '../bottom-sheet';
|
|
63
72
|
import { InputGroup } from '../input-group';
|
|
64
73
|
|
|
@@ -74,6 +83,8 @@ const selectVariants = tv({
|
|
|
74
83
|
item: 'flex-row items-center gap-2 rounded-lg px-3 py-3',
|
|
75
84
|
itemLabel: 'flex-1 text-base font-medium text-foreground',
|
|
76
85
|
itemIndicator: 'h-5 w-5 items-center justify-center',
|
|
86
|
+
group: 'gap-1',
|
|
87
|
+
groupLabel: 'px-3 pb-1 pt-2',
|
|
77
88
|
},
|
|
78
89
|
variants: {
|
|
79
90
|
selected: {
|
|
@@ -151,6 +162,96 @@ function SelectItem({ value, label, disabled }: SelectItemProps) {
|
|
|
151
162
|
);
|
|
152
163
|
}
|
|
153
164
|
|
|
165
|
+
export interface SelectGroupProps {
|
|
166
|
+
/**
|
|
167
|
+
* Heading over the run of options. Announced as a header, so a screen reader
|
|
168
|
+
* reaching the group is told what it is before walking into it.
|
|
169
|
+
*/
|
|
170
|
+
label?: string;
|
|
171
|
+
/** Extra classes for the group wrapper. */
|
|
172
|
+
className?: string;
|
|
173
|
+
/** Extra classes for the heading. */
|
|
174
|
+
labelClassName?: string;
|
|
175
|
+
children: ReactNode;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A titled run of options.
|
|
180
|
+
*
|
|
181
|
+
* Purely a way of arranging the list: a grouped Select still reports one flat
|
|
182
|
+
* string, and `Select.Item` needs to know nothing about being inside one.
|
|
183
|
+
*/
|
|
184
|
+
function SelectGroup({ label, className, labelClassName, children }: SelectGroupProps) {
|
|
185
|
+
const { group, groupLabel } = selectVariants();
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<View className={cn(group(), className)}>
|
|
189
|
+
{label ? (
|
|
190
|
+
<View accessibilityRole="header" className={cn(groupLabel(), labelClassName)}>
|
|
191
|
+
<Text size="xs" weight="medium" muted className="uppercase tracking-wide">
|
|
192
|
+
{label}
|
|
193
|
+
</Text>
|
|
194
|
+
</View>
|
|
195
|
+
) : null}
|
|
196
|
+
{textChildren(children)}
|
|
197
|
+
</View>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Walk the declared children, visiting every `Select.Item` — including the ones
|
|
203
|
+
* nested inside a `Select.Group`.
|
|
204
|
+
*
|
|
205
|
+
* Grouping is a rendering concern, but the selected label and the native
|
|
206
|
+
* picker's option list both want the flat set, so the tree is flattened once
|
|
207
|
+
* here rather than in each of them.
|
|
208
|
+
*/
|
|
209
|
+
function eachOption(children: ReactNode, visit: (option: SelectItemProps) => void) {
|
|
210
|
+
Children.forEach(children, (child) => {
|
|
211
|
+
if (!isValidElement(child)) return;
|
|
212
|
+
if (child.type === SelectGroup) {
|
|
213
|
+
eachOption((child.props as SelectGroupProps).children, visit);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (child.type !== SelectItem) return;
|
|
217
|
+
const { value, label, disabled } = child.props as SelectItemProps;
|
|
218
|
+
visit({ value, label, disabled });
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The children a query leaves standing.
|
|
224
|
+
*
|
|
225
|
+
* A group is rebuilt around whatever survives inside it and dropped when that
|
|
226
|
+
* is nothing — a heading with no options under it reads as a section that
|
|
227
|
+
* failed to load rather than one the filter emptied. Anything that is neither
|
|
228
|
+
* an item nor a group is left alone: a caption or a divider the caller put in
|
|
229
|
+
* the list is not something a filter has an opinion about.
|
|
230
|
+
*/
|
|
231
|
+
function filterOptions(children: ReactNode, needle: string): ReactNode[] {
|
|
232
|
+
const kept: ReactNode[] = [];
|
|
233
|
+
|
|
234
|
+
Children.forEach(children, (child) => {
|
|
235
|
+
if (!isValidElement(child)) return;
|
|
236
|
+
|
|
237
|
+
if (child.type === SelectGroup) {
|
|
238
|
+
const props = child.props as SelectGroupProps;
|
|
239
|
+
const inner = filterOptions(props.children, needle);
|
|
240
|
+
if (inner.length) {
|
|
241
|
+
kept.push(cloneElement(child as ReactElement<SelectGroupProps>, {}, inner));
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (child.type === SelectItem) {
|
|
247
|
+
const { label } = child.props as SelectItemProps;
|
|
248
|
+
if (label.toLowerCase().includes(needle)) kept.push(child);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return kept;
|
|
253
|
+
}
|
|
254
|
+
|
|
154
255
|
/** Trigger frame in window coordinates, measured when the list opens. */
|
|
155
256
|
interface Anchor {
|
|
156
257
|
x: number;
|
|
@@ -242,15 +343,7 @@ function SelectRoot({
|
|
|
242
343
|
|
|
243
344
|
const options = useMemo(() => {
|
|
244
345
|
const collected: SelectItemProps[] = [];
|
|
245
|
-
|
|
246
|
-
if (isValidElement<SelectItemProps>(child)) {
|
|
247
|
-
collected.push({
|
|
248
|
-
value: child.props.value,
|
|
249
|
-
label: child.props.label,
|
|
250
|
-
disabled: child.props.disabled,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
});
|
|
346
|
+
eachOption(children, (option) => collected.push(option));
|
|
254
347
|
return collected;
|
|
255
348
|
}, [children]);
|
|
256
349
|
|
|
@@ -269,11 +362,7 @@ function SelectRoot({
|
|
|
269
362
|
const needle = searchable ? query.trim().toLowerCase() : '';
|
|
270
363
|
if (!needle) return null;
|
|
271
364
|
|
|
272
|
-
return
|
|
273
|
-
(child) =>
|
|
274
|
-
isValidElement<SelectItemProps>(child) &&
|
|
275
|
-
child.props.label.toLowerCase().includes(needle)
|
|
276
|
-
);
|
|
365
|
+
return filterOptions(children, needle);
|
|
277
366
|
}, [children, query, searchable]);
|
|
278
367
|
|
|
279
368
|
const close = useCallback(() => {
|
|
@@ -360,7 +449,13 @@ function SelectRoot({
|
|
|
360
449
|
const trigger = (
|
|
361
450
|
<Pressable
|
|
362
451
|
ref={triggerRef}
|
|
363
|
-
|
|
452
|
+
/*
|
|
453
|
+
* A trigger that owns a list of options and reports whether that list is
|
|
454
|
+
* open is a combobox, not a button — and `expanded` only means anything
|
|
455
|
+
* on a role that can be expanded. Announced as "collapsed"/"expanded"
|
|
456
|
+
* rather than as a button whose state has nowhere to be read.
|
|
457
|
+
*/
|
|
458
|
+
accessibilityRole="combobox"
|
|
364
459
|
accessibilityState={{ disabled: !!disabled, expanded: open }}
|
|
365
460
|
disabled={disabled}
|
|
366
461
|
onPress={toggle}
|
|
@@ -566,6 +661,10 @@ function SelectRoot({
|
|
|
566
661
|
);
|
|
567
662
|
}
|
|
568
663
|
|
|
664
|
+
SelectItem.displayName = 'Select.Item';
|
|
665
|
+
SelectGroup.displayName = 'Select.Group';
|
|
666
|
+
|
|
569
667
|
export const Select = Object.assign(SelectRoot, {
|
|
570
668
|
Item: SelectItem,
|
|
669
|
+
Group: SelectGroup,
|
|
571
670
|
});
|
|
@@ -66,7 +66,9 @@ const textareaVariants = tv({
|
|
|
66
66
|
* the leading is wanted — between several lines it separates them, where in
|
|
67
67
|
* a one-line field it would push the text off centre.
|
|
68
68
|
*/
|
|
69
|
-
|
|
69
|
+
/* `font-normal` so an app's --font-normal token reaches a TextInput,
|
|
70
|
+
which inherits nothing from the Text primitive. */
|
|
71
|
+
field: 'w-full rounded-lg border font-normal text-foreground',
|
|
70
72
|
footer: 'w-full flex-row items-start justify-between gap-3',
|
|
71
73
|
description: 'text-sm text-muted-foreground',
|
|
72
74
|
error: 'text-sm text-destructive',
|