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.
- package/README.md +4 -2
- package/lib/module/components/calendar/index.js +107 -24
- package/lib/module/components/calendar/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/plan/index.js +307 -21
- package/lib/module/components/plan/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/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/table/index.js +632 -0
- package/lib/module/components/table/index.js.map +1 -0
- package/lib/module/icons/index.js +200 -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/utils/date.js +129 -22
- package/lib/module/utils/date.js.map +1 -1
- package/lib/typescript/src/components/calendar/index.d.ts +12 -3
- package/lib/typescript/src/components/calendar/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/plan/index.d.ts +121 -10
- package/lib/typescript/src/components/plan/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/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/table/index.d.ts +274 -0
- package/lib/typescript/src/components/table/index.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +40 -1
- 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/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 +136 -23
- package/src/components/heatmap-chart/index.tsx +9 -9
- package/src/components/map/index.tsx +53 -4
- package/src/components/plan/index.tsx +344 -21
- package/src/components/post/index.tsx +1116 -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/table/index.tsx +747 -0
- package/src/icons/index.tsx +162 -1
- package/src/index.ts +60 -0
- 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({
|
|
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({
|
|
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 ??
|
|
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
|
-
<
|
|
342
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
419
|
-
|
|
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>
|