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.
Files changed (60) hide show
  1. package/README.md +12 -1
  2. package/lib/module/components/combobox/index.js +718 -0
  3. package/lib/module/components/combobox/index.js.map +1 -0
  4. package/lib/module/components/form/use-field.js +18 -0
  5. package/lib/module/components/form/use-field.js.map +1 -1
  6. package/lib/module/components/form/use-form.js +91 -8
  7. package/lib/module/components/form/use-form.js.map +1 -1
  8. package/lib/module/components/input/index.js +142 -32
  9. package/lib/module/components/input/index.js.map +1 -1
  10. package/lib/module/components/otp-input/index.js +4 -1
  11. package/lib/module/components/otp-input/index.js.map +1 -1
  12. package/lib/module/components/panelside/index.js +1 -1
  13. package/lib/module/components/panelside/index.js.map +1 -1
  14. package/lib/module/components/select/index.js +116 -15
  15. package/lib/module/components/select/index.js.map +1 -1
  16. package/lib/module/components/textarea/index.js +3 -1
  17. package/lib/module/components/textarea/index.js.map +1 -1
  18. package/lib/module/components/time-picker/index.js +801 -0
  19. package/lib/module/components/time-picker/index.js.map +1 -0
  20. package/lib/module/icons/index.js +59 -0
  21. package/lib/module/icons/index.js.map +1 -1
  22. package/lib/module/index.js +4 -1
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/module/theme/use-theme.js +1 -1
  25. package/lib/module/utils/time.js +180 -0
  26. package/lib/module/utils/time.js.map +1 -0
  27. package/lib/typescript/src/components/combobox/index.d.ts +164 -0
  28. package/lib/typescript/src/components/combobox/index.d.ts.map +1 -0
  29. package/lib/typescript/src/components/form/use-field.d.ts.map +1 -1
  30. package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
  31. package/lib/typescript/src/components/input/index.d.ts +106 -0
  32. package/lib/typescript/src/components/input/index.d.ts.map +1 -1
  33. package/lib/typescript/src/components/otp-input/index.d.ts.map +1 -1
  34. package/lib/typescript/src/components/select/index.d.ts +32 -0
  35. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  36. package/lib/typescript/src/components/textarea/index.d.ts.map +1 -1
  37. package/lib/typescript/src/components/time-picker/index.d.ts +104 -0
  38. package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -0
  39. package/lib/typescript/src/icons/index.d.ts +4 -0
  40. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  41. package/lib/typescript/src/index.d.ts +5 -2
  42. package/lib/typescript/src/index.d.ts.map +1 -1
  43. package/lib/typescript/src/theme/use-theme.d.ts +1 -1
  44. package/lib/typescript/src/utils/time.d.ts +88 -0
  45. package/lib/typescript/src/utils/time.d.ts.map +1 -0
  46. package/package.json +1 -1
  47. package/src/components/combobox/index.tsx +927 -0
  48. package/src/components/form/use-field.ts +23 -0
  49. package/src/components/form/use-form.ts +106 -16
  50. package/src/components/input/index.tsx +185 -29
  51. package/src/components/otp-input/index.tsx +4 -1
  52. package/src/components/panelside/index.tsx +1 -1
  53. package/src/components/select/index.tsx +114 -15
  54. package/src/components/textarea/index.tsx +3 -1
  55. package/src/components/time-picker/index.tsx +971 -0
  56. package/src/icons/index.tsx +47 -0
  57. package/src/index.ts +35 -0
  58. package/src/theme/use-theme.ts +1 -1
  59. package/src/utils/time.ts +192 -0
  60. 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
- Children.forEach(children, (child) => {
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 Children.toArray(children).filter(
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
- accessibilityRole="button"
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
- field: 'w-full rounded-lg border text-foreground',
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',