panelui-native 0.30.0 → 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.
Files changed (61) hide show
  1. package/README.md +4 -2
  2. package/lib/module/components/calendar/index.js +107 -24
  3. package/lib/module/components/calendar/index.js.map +1 -1
  4. package/lib/module/components/heatmap-chart/index.js +9 -8
  5. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  6. package/lib/module/components/map/index.js +50 -4
  7. package/lib/module/components/map/index.js.map +1 -1
  8. package/lib/module/components/plan/index.js +307 -21
  9. package/lib/module/components/plan/index.js.map +1 -1
  10. package/lib/module/components/post/index.js +992 -0
  11. package/lib/module/components/post/index.js.map +1 -0
  12. package/lib/module/components/response/index.js +296 -0
  13. package/lib/module/components/response/index.js.map +1 -0
  14. package/lib/module/components/response/markdown.js +463 -0
  15. package/lib/module/components/response/markdown.js.map +1 -0
  16. package/lib/module/components/select/index.js +97 -15
  17. package/lib/module/components/select/index.js.map +1 -1
  18. package/lib/module/components/table/index.js +632 -0
  19. package/lib/module/components/table/index.js.map +1 -0
  20. package/lib/module/icons/index.js +200 -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/utils/date.js +129 -22
  25. package/lib/module/utils/date.js.map +1 -1
  26. package/lib/typescript/src/components/calendar/index.d.ts +12 -3
  27. package/lib/typescript/src/components/calendar/index.d.ts.map +1 -1
  28. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  29. package/lib/typescript/src/components/map/index.d.ts +8 -1
  30. package/lib/typescript/src/components/map/index.d.ts.map +1 -1
  31. package/lib/typescript/src/components/plan/index.d.ts +121 -10
  32. package/lib/typescript/src/components/plan/index.d.ts.map +1 -1
  33. package/lib/typescript/src/components/post/index.d.ts +358 -0
  34. package/lib/typescript/src/components/post/index.d.ts.map +1 -0
  35. package/lib/typescript/src/components/response/index.d.ts +91 -0
  36. package/lib/typescript/src/components/response/index.d.ts.map +1 -0
  37. package/lib/typescript/src/components/response/markdown.d.ts +88 -0
  38. package/lib/typescript/src/components/response/markdown.d.ts.map +1 -0
  39. package/lib/typescript/src/components/select/index.d.ts +27 -2
  40. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  41. package/lib/typescript/src/components/table/index.d.ts +274 -0
  42. package/lib/typescript/src/components/table/index.d.ts.map +1 -0
  43. package/lib/typescript/src/icons/index.d.ts +40 -1
  44. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  45. package/lib/typescript/src/index.d.ts +5 -2
  46. package/lib/typescript/src/index.d.ts.map +1 -1
  47. package/lib/typescript/src/utils/date.d.ts +49 -5
  48. package/lib/typescript/src/utils/date.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/components/calendar/index.tsx +136 -23
  51. package/src/components/heatmap-chart/index.tsx +9 -9
  52. package/src/components/map/index.tsx +53 -4
  53. package/src/components/plan/index.tsx +344 -21
  54. package/src/components/post/index.tsx +1116 -0
  55. package/src/components/response/index.tsx +390 -0
  56. package/src/components/response/markdown.ts +489 -0
  57. package/src/components/select/index.tsx +132 -12
  58. package/src/components/table/index.tsx +747 -0
  59. package/src/icons/index.tsx +162 -1
  60. package/src/index.ts +60 -0
  61. package/src/utils/date.ts +146 -22
@@ -20,6 +20,12 @@
20
20
  * <Select.Item value="eu" label="Europe" />
21
21
  * </Select>
22
22
  * ```
23
+ *
24
+ * Past a couple of dozen options, scrolling stops being a way of finding
25
+ * anything: pass `searchable` and the list gets a filter above it, matching on
26
+ * the option labels. The filter narrows what is *shown* — the declared options
27
+ * are still the source of truth, so nothing has to be lifted into state to make
28
+ * it work.
23
29
  */
24
30
  import {
25
31
  Children,
@@ -48,12 +54,13 @@ import Animated, {
48
54
  } from 'react-native-reanimated';
49
55
  import { tv } from 'tailwind-variants';
50
56
  import { useCSSVariable } from 'uniwind';
51
- import { CheckIcon, ChevronDownIcon } from '../../icons';
57
+ import { CheckIcon, ChevronDownIcon, SearchIcon } from '../../icons';
52
58
  import { getNativeUI } from '../../native';
53
59
  import { Portal } from '../../primitives/portal';
54
60
  import { Text, textChildren } from '../../primitives/text';
55
61
  import { useBackHandler } from '../../hooks/use-back-handler';
56
62
  import { BottomSheet } from '../bottom-sheet';
63
+ import { InputGroup } from '../input-group';
57
64
 
58
65
  const selectVariants = tv({
59
66
  slots: {
@@ -75,6 +82,9 @@ const selectVariants = tv({
75
82
  disabled: {
76
83
  true: { trigger: 'opacity-[0.64]' },
77
84
  },
85
+ itemDisabled: {
86
+ true: { item: 'opacity-[0.64]' },
87
+ },
78
88
  presentation: {
79
89
  sheet: {},
80
90
  inline: { list: 'mt-2' },
@@ -98,23 +108,33 @@ const SelectContext = createContext<SelectContextValue | null>(null);
98
108
  export interface SelectItemProps {
99
109
  value: string;
100
110
  label: string;
111
+ /**
112
+ * Shows the option but refuses it — a plan above the current tier, a region
113
+ * with nothing in stock. Kept in the list rather than dropped from it, because
114
+ * an option that vanishes reads as one that never existed.
115
+ */
116
+ disabled?: boolean;
101
117
  }
102
118
 
103
119
  /** Declarative option. Rendered inside whichever surface is presenting. */
104
- function SelectItem({ value, label }: SelectItemProps) {
120
+ function SelectItem({ value, label, disabled }: SelectItemProps) {
105
121
  const context = useContext(SelectContext);
106
122
  if (!context) {
107
123
  throw new Error('Select.Item must be used within a <Select>');
108
124
  }
109
125
 
110
126
  const selected = context.value === value;
111
- const { item, itemLabel, itemIndicator } = selectVariants({ selected });
127
+ const { item, itemLabel, itemIndicator } = selectVariants({
128
+ selected,
129
+ itemDisabled: !!disabled,
130
+ });
112
131
  const checkColor = useCSSVariable('--color-muted-foreground');
113
132
 
114
133
  return (
115
134
  <Pressable
116
135
  accessibilityRole="menuitem"
117
- accessibilityState={{ selected }}
136
+ accessibilityState={{ selected, disabled: !!disabled }}
137
+ disabled={disabled}
118
138
  onPress={() => context.onSelect(value)}
119
139
  className={item()}
120
140
  >
@@ -162,6 +182,19 @@ export interface SelectProps {
162
182
  offset?: number;
163
183
  /** Called when the options open or close. */
164
184
  onOpenChange?: (open: boolean) => void;
185
+ /**
186
+ * Put a filter above the options, matching case-insensitively on any part of
187
+ * an option's label. For a list long enough that scrolling it is not finding
188
+ * anything — countries, currencies, a repository's branches.
189
+ *
190
+ * The field is not focused on open: on a phone that would throw the keyboard
191
+ * over the very list you are trying to look at.
192
+ */
193
+ searchable?: boolean;
194
+ /** Placeholder for the filter field. `searchable` only. */
195
+ searchPlaceholder?: string;
196
+ /** Shown in place of the list when the filter matches nothing. */
197
+ emptyMessage?: string;
165
198
  /**
166
199
  * Render the platform's own picker instead of the trigger-and-list pair.
167
200
  * Requires the optional `@expo/ui` package; without it this prop does
@@ -191,11 +224,15 @@ function SelectRoot({
191
224
  contentWidth = 'trigger',
192
225
  offset = 8,
193
226
  onOpenChange,
227
+ searchable = false,
228
+ searchPlaceholder = 'Search',
229
+ emptyMessage = 'No matches',
194
230
  native,
195
231
  nativeAppearance = 'menu',
196
232
  children,
197
233
  }: SelectProps) {
198
234
  const [open, setOpen] = useState(false);
235
+ const [query, setQuery] = useState('');
199
236
  const [anchor, setAnchor] = useState<Anchor | null>(null);
200
237
  const [listHeight, setListHeight] = useState(0);
201
238
  const triggerRef = useRef<View>(null);
@@ -207,7 +244,11 @@ function SelectRoot({
207
244
  const collected: SelectItemProps[] = [];
208
245
  Children.forEach(children, (child) => {
209
246
  if (isValidElement<SelectItemProps>(child)) {
210
- collected.push({ value: child.props.value, label: child.props.label });
247
+ collected.push({
248
+ value: child.props.value,
249
+ label: child.props.label,
250
+ disabled: child.props.disabled,
251
+ });
211
252
  }
212
253
  });
213
254
  return collected;
@@ -218,10 +259,30 @@ function SelectRoot({
218
259
  [options, value]
219
260
  );
220
261
 
262
+ /*
263
+ * The options the filter leaves standing, or null when nothing is being
264
+ * filtered — which is the common case, and the one that must not pay for the
265
+ * feature. `null` means "render the children as given", so an unsearched
266
+ * Select does no per-option work at all.
267
+ */
268
+ const filtered = useMemo(() => {
269
+ const needle = searchable ? query.trim().toLowerCase() : '';
270
+ if (!needle) return null;
271
+
272
+ return Children.toArray(children).filter(
273
+ (child) =>
274
+ isValidElement<SelectItemProps>(child) &&
275
+ child.props.label.toLowerCase().includes(needle)
276
+ );
277
+ }, [children, query, searchable]);
278
+
221
279
  const close = useCallback(() => {
222
280
  chevron.value = withTiming(0, { duration: 160 });
223
281
  setOpen(false);
224
282
  onOpenChange?.(false);
283
+ // A filter left behind would be waiting the next time the list opens, with
284
+ // most of the options missing and no obvious reason why.
285
+ setQuery('');
225
286
  }, [chevron, onOpenChange]);
226
287
 
227
288
  // An open overlay list catches the Android back button, closing itself
@@ -275,14 +336,15 @@ function SelectRoot({
275
336
  if (nativeUI) {
276
337
  const { Host, Picker } = nativeUI;
277
338
  // The native picker has no empty state, so an unset value shows the first
278
- // option rather than the placeholder.
339
+ // selectable option rather than the placeholder.
340
+ const firstEnabled = options.find((option) => !option.disabled);
279
341
  return (
280
342
  // A picker fills the width of the row it sits in and reports its own
281
343
  // height — a menu is a compact button, a wheel a full rotor, and the
282
344
  // platform is the only thing that knows which by how much.
283
345
  <Host matchContents={{ vertical: true }}>
284
346
  <Picker
285
- selectedValue={value ?? options[0]?.value ?? ''}
347
+ selectedValue={value ?? firstEnabled?.value ?? ''}
286
348
  onValueChange={(next: string) => onValueChange(next)}
287
349
  appearance={nativeAppearance}
288
350
  enabled={!disabled}
@@ -317,6 +379,48 @@ function SelectRoot({
317
379
  </Pressable>
318
380
  );
319
381
 
382
+ /*
383
+ * The filter, and the list it narrows. Both are built once here rather than
384
+ * per presentation: the three surfaces differ in where they put the field —
385
+ * above the scroller, so it does not scroll away with the options — not in
386
+ * what it is.
387
+ */
388
+ const search = searchable ? (
389
+ <View className="pb-2">
390
+ <InputGroup>
391
+ <InputGroup.Prefix isDecorative>
392
+ <SearchIcon
393
+ size={18}
394
+ color={typeof chevronColor === 'string' ? chevronColor : '#737373'}
395
+ />
396
+ </InputGroup.Prefix>
397
+ <InputGroup.Input
398
+ variant="filled"
399
+ size="sm"
400
+ value={query}
401
+ onChangeText={setQuery}
402
+ placeholder={searchPlaceholder}
403
+ accessibilityLabel={searchPlaceholder}
404
+ autoCapitalize="none"
405
+ autoCorrect={false}
406
+ returnKeyType="search"
407
+ clearButtonMode="while-editing"
408
+ />
409
+ </InputGroup>
410
+ </View>
411
+ ) : null;
412
+
413
+ const optionList =
414
+ filtered === null ? (
415
+ textChildren(children)
416
+ ) : filtered.length ? (
417
+ filtered
418
+ ) : (
419
+ <Text className="px-3 py-6 text-center text-sm text-muted-foreground">
420
+ {emptyMessage}
421
+ </Text>
422
+ );
423
+
320
424
  if (presentation === 'sheet') {
321
425
  return (
322
426
  <SelectContext.Provider value={context}>
@@ -338,8 +442,17 @@ function SelectRoot({
338
442
  {title}
339
443
  </Text>
340
444
  ) : null}
341
- <ScrollView bounces={false} className="max-h-96">
342
- <View className="gap-1 pb-2">{textChildren(children)}</View>
445
+ {search ? <View className="px-1">{search}</View> : null}
446
+ <ScrollView
447
+ bounces={false}
448
+ className="max-h-96"
449
+ // The filter is a text field above a scroller: dismissing the
450
+ // keyboard on a drag is what lets you look at what you filtered
451
+ // to without first tapping somewhere neutral.
452
+ keyboardDismissMode="on-drag"
453
+ keyboardShouldPersistTaps="handled"
454
+ >
455
+ <View className="gap-1 pb-2">{optionList}</View>
343
456
  </ScrollView>
344
457
  </SelectContext.Provider>
345
458
  </BottomSheet.Content>
@@ -359,7 +472,8 @@ function SelectRoot({
359
472
  exiting={FadeOut.duration(120)}
360
473
  className={slots.list()}
361
474
  >
362
- {textChildren(children)}
475
+ {search}
476
+ {optionList}
363
477
  </Animated.View>
364
478
  ) : null}
365
479
  </View>
@@ -415,8 +529,14 @@ function SelectRoot({
415
529
  style={overlayPosition}
416
530
  className={slots.list()}
417
531
  >
418
- <ScrollView bounces={false} showsVerticalScrollIndicator={false}>
419
- {textChildren(children)}
532
+ {search}
533
+ <ScrollView
534
+ bounces={false}
535
+ showsVerticalScrollIndicator={false}
536
+ keyboardDismissMode="on-drag"
537
+ keyboardShouldPersistTaps="handled"
538
+ >
539
+ {optionList}
420
540
  </ScrollView>
421
541
  </Animated.View>
422
542
  </SelectContext.Provider>