panelui-native 0.14.0 → 0.19.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 (86) hide show
  1. package/README.md +2 -0
  2. package/lib/module/components/bottom-sheet/index.js +38 -10
  3. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  4. package/lib/module/components/checkbox/index.js +24 -11
  5. package/lib/module/components/checkbox/index.js.map +1 -1
  6. package/lib/module/components/direction/index.js +86 -0
  7. package/lib/module/components/direction/index.js.map +1 -0
  8. package/lib/module/components/field/index.js +337 -0
  9. package/lib/module/components/field/index.js.map +1 -0
  10. package/lib/module/components/flow/flow-paths.js +378 -0
  11. package/lib/module/components/flow/flow-paths.js.map +1 -0
  12. package/lib/module/components/flow/index.js +1598 -0
  13. package/lib/module/components/flow/index.js.map +1 -0
  14. package/lib/module/components/form/index.js +77 -0
  15. package/lib/module/components/form/index.js.map +1 -0
  16. package/lib/module/components/form/use-field.js +50 -0
  17. package/lib/module/components/form/use-field.js.map +1 -0
  18. package/lib/module/components/form/use-form.js +217 -0
  19. package/lib/module/components/form/use-form.js.map +1 -0
  20. package/lib/module/components/frame/index.js +124 -22
  21. package/lib/module/components/frame/index.js.map +1 -1
  22. package/lib/module/components/message/index.js +16 -1
  23. package/lib/module/components/message/index.js.map +1 -1
  24. package/lib/module/components/otp-input/index.js +303 -0
  25. package/lib/module/components/otp-input/index.js.map +1 -0
  26. package/lib/module/components/signature/index.js +500 -0
  27. package/lib/module/components/signature/index.js.map +1 -0
  28. package/lib/module/components/soundwave/index.js +813 -0
  29. package/lib/module/components/soundwave/index.js.map +1 -0
  30. package/lib/module/components/typography/index.js +196 -5
  31. package/lib/module/components/typography/index.js.map +1 -1
  32. package/lib/module/icons/index.js +287 -0
  33. package/lib/module/icons/index.js.map +1 -1
  34. package/lib/module/index.js +8 -1
  35. package/lib/module/index.js.map +1 -1
  36. package/lib/typescript/src/components/bottom-sheet/index.d.ts +15 -1
  37. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  38. package/lib/typescript/src/components/checkbox/index.d.ts +8 -0
  39. package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
  40. package/lib/typescript/src/components/direction/index.d.ts +64 -0
  41. package/lib/typescript/src/components/direction/index.d.ts.map +1 -0
  42. package/lib/typescript/src/components/field/index.d.ts +94 -0
  43. package/lib/typescript/src/components/field/index.d.ts.map +1 -0
  44. package/lib/typescript/src/components/flow/flow-paths.d.ts +89 -0
  45. package/lib/typescript/src/components/flow/flow-paths.d.ts.map +1 -0
  46. package/lib/typescript/src/components/flow/index.d.ts +310 -0
  47. package/lib/typescript/src/components/flow/index.d.ts.map +1 -0
  48. package/lib/typescript/src/components/form/index.d.ts +64 -0
  49. package/lib/typescript/src/components/form/index.d.ts.map +1 -0
  50. package/lib/typescript/src/components/form/use-field.d.ts +16 -0
  51. package/lib/typescript/src/components/form/use-field.d.ts.map +1 -0
  52. package/lib/typescript/src/components/form/use-form.d.ts +37 -0
  53. package/lib/typescript/src/components/form/use-form.d.ts.map +1 -0
  54. package/lib/typescript/src/components/frame/index.d.ts +39 -2
  55. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  56. package/lib/typescript/src/components/message/index.d.ts.map +1 -1
  57. package/lib/typescript/src/components/otp-input/index.d.ts +141 -0
  58. package/lib/typescript/src/components/otp-input/index.d.ts.map +1 -0
  59. package/lib/typescript/src/components/signature/index.d.ts +254 -0
  60. package/lib/typescript/src/components/signature/index.d.ts.map +1 -0
  61. package/lib/typescript/src/components/soundwave/index.d.ts +91 -0
  62. package/lib/typescript/src/components/soundwave/index.d.ts.map +1 -0
  63. package/lib/typescript/src/components/typography/index.d.ts +154 -1
  64. package/lib/typescript/src/components/typography/index.d.ts.map +1 -1
  65. package/lib/typescript/src/icons/index.d.ts +23 -0
  66. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  67. package/lib/typescript/src/index.d.ts +9 -2
  68. package/lib/typescript/src/index.d.ts.map +1 -1
  69. package/package.json +9 -1
  70. package/src/components/bottom-sheet/index.tsx +50 -12
  71. package/src/components/checkbox/index.tsx +40 -12
  72. package/src/components/direction/index.tsx +90 -0
  73. package/src/components/field/index.tsx +329 -0
  74. package/src/components/flow/flow-paths.ts +348 -0
  75. package/src/components/flow/index.tsx +1924 -0
  76. package/src/components/form/index.tsx +85 -0
  77. package/src/components/form/use-field.ts +65 -0
  78. package/src/components/form/use-form.ts +252 -0
  79. package/src/components/frame/index.tsx +162 -14
  80. package/src/components/message/index.tsx +17 -1
  81. package/src/components/otp-input/index.tsx +396 -0
  82. package/src/components/signature/index.tsx +681 -0
  83. package/src/components/soundwave/index.tsx +1029 -0
  84. package/src/components/typography/index.tsx +231 -10
  85. package/src/icons/index.tsx +195 -0
  86. package/src/index.ts +87 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Direction — reading direction for everything below it.
3
+ *
4
+ * ```tsx
5
+ * <Direction dir="rtl">
6
+ * <App />
7
+ * </Direction>
8
+ * ```
9
+ *
10
+ * The layout flip is Yoga's, not ours: the wrapper carries the `direction`
11
+ * style, and every row, `start`/`end` inset and horizontal padding underneath
12
+ * it mirrors natively. That is the whole reason this is a component and not a
13
+ * bare context — `direction` is a *style*, so a subtree is the unit it applies
14
+ * to.
15
+ *
16
+ * It is also the reason to prefer it over flipping the process with
17
+ * `I18nManager.forceRTL`, which needs an app restart to take effect, cannot be
18
+ * scoped to part of a screen, and cannot be previewed side by side. Here the
19
+ * value is a prop: change it and the next frame is mirrored.
20
+ *
21
+ * ## What Yoga does not flip
22
+ *
23
+ * Anything measured in raw pixels rather than laid out — a drag translation, an
24
+ * indicator offset, the direction a sweep travels. Those have to consult the
25
+ * value and negate it themselves, which is what `useDirection()` is for:
26
+ *
27
+ * ```tsx
28
+ * const dir = useDirection();
29
+ * const delta = (dir === 'rtl' ? -event.translationX : event.translationX) / width;
30
+ * ```
31
+ *
32
+ * With no provider mounted, and for a `<Direction>` given no `dir`, the value
33
+ * comes from the device — so an app that did force RTL the old way still reads
34
+ * back the truth.
35
+ */
36
+ import { createContext, useContext, useMemo, type ReactNode } from 'react';
37
+ import { I18nManager, View, type ViewProps } from 'react-native';
38
+ import { cn } from '../../utils/cn';
39
+
40
+ export type DirectionValue = 'ltr' | 'rtl';
41
+
42
+ /** What the device is set to, for anything outside a provider. */
43
+ const deviceDirection = (): DirectionValue => (I18nManager.isRTL ? 'rtl' : 'ltr');
44
+
45
+ const DirectionContext = createContext<DirectionValue | null>(null);
46
+
47
+ export interface DirectionProps extends ViewProps {
48
+ className?: string;
49
+ children?: ReactNode;
50
+ /**
51
+ * Reading direction for this subtree. Defaults to the nearest enclosing
52
+ * `Direction`, or to the device when there is none — so a nested provider
53
+ * with no `dir` inherits rather than resetting to left-to-right.
54
+ */
55
+ dir?: DirectionValue;
56
+ }
57
+
58
+ /**
59
+ * Provider. Renders one `View` — it has to, since the flip is a style — which
60
+ * fills its parent by default so it can wrap an app. Pass `className` to change
61
+ * that: `flex-none` or a width class both win over the default.
62
+ */
63
+ export function Direction({ className, children, dir, style, ...props }: DirectionProps) {
64
+ const inherited = useContext(DirectionContext);
65
+ const value = dir ?? inherited ?? deviceDirection();
66
+
67
+ // The style object is memoised because it is the thing that invalidates
68
+ // layout for the entire subtree — a fresh one each render would re-run Yoga
69
+ // on every parent render, for a value that almost never changes.
70
+ const directionStyle = useMemo(() => ({ direction: value }) as const, [value]);
71
+
72
+ return (
73
+ <DirectionContext.Provider value={value}>
74
+ <View className={cn('flex-1', className)} style={[directionStyle, style]} {...props}>
75
+ {children}
76
+ </View>
77
+ </DirectionContext.Provider>
78
+ );
79
+ }
80
+
81
+ Direction.displayName = 'Direction';
82
+
83
+ /**
84
+ * The reading direction in force, for a component that has to flip maths Yoga
85
+ * cannot flip for it. Safe with no provider mounted: it falls back to the
86
+ * device's own setting.
87
+ */
88
+ export function useDirection(): DirectionValue {
89
+ return useContext(DirectionContext) ?? deviceDirection();
90
+ }
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Field — the layout and validation-state kit a form control composes into.
3
+ *
4
+ * PanelUI's own controls (`Input`, `Checkbox`, `Switch`, `RadioGroup`) already
5
+ * carry their own label, description and error slot, so the common case needs
6
+ * no wrapper at all — pass those props straight through. `Field` exists for
7
+ * what doesn't fit that shape: a horizontal row pairing a control with a
8
+ * label off to the side (a `Switch` in a settings list), several controls
9
+ * grouped under one legend, or a rule breaking a long form into sections.
10
+ *
11
+ * `invalid` / `disabled` / `required` set on the root flow to `Field.Label`
12
+ * and `Field.Description` through context, so a multi-field row states its
13
+ * validity once instead of repeating the prop at every leaf.
14
+ *
15
+ * ```tsx
16
+ * <Field orientation="horizontal">
17
+ * <Field.Content>
18
+ * <Field.Title>Marketing emails</Field.Title>
19
+ * <Field.Description>Product updates, at most weekly.</Field.Description>
20
+ * </Field.Content>
21
+ * <Switch value={subscribed} onValueChange={setSubscribed} />
22
+ * </Field>
23
+ * ```
24
+ */
25
+ import {
26
+ createContext,
27
+ forwardRef,
28
+ useContext,
29
+ useMemo,
30
+ type ReactNode,
31
+ } from 'react';
32
+ import { View, type Text as RNText, type ViewProps } from 'react-native';
33
+ import { tv } from 'tailwind-variants';
34
+ import { Text, type TextProps } from '../../primitives/text';
35
+ import { Label, type LabelProps } from '../label';
36
+ import { Separator } from '../separator';
37
+
38
+ const fieldVariants = tv({
39
+ slots: {
40
+ root: 'gap-2',
41
+ content: 'flex-1 flex-col gap-0.5',
42
+ description: 'text-sm text-muted-foreground',
43
+ error: 'gap-1',
44
+ errorText: 'text-sm text-destructive',
45
+ bullet: 'flex-row gap-1.5',
46
+ bulletDot: 'text-sm text-destructive',
47
+ set: 'gap-4',
48
+ legend: 'text-foreground',
49
+ group: 'gap-6',
50
+ separator: 'flex-row items-center gap-3',
51
+ separatorLabel: 'text-xs text-muted-foreground',
52
+ title: 'text-base font-medium text-foreground',
53
+ },
54
+ variants: {
55
+ orientation: {
56
+ vertical: { root: 'flex-col' },
57
+ horizontal: { root: 'flex-row items-center justify-between gap-4' },
58
+ },
59
+ disabled: {
60
+ true: { root: 'opacity-[0.64]' },
61
+ },
62
+ legendVariant: {
63
+ legend: { legend: 'text-base font-semibold' },
64
+ label: { legend: 'text-sm font-medium' },
65
+ },
66
+ },
67
+ defaultVariants: {
68
+ orientation: 'vertical',
69
+ },
70
+ });
71
+
72
+ interface FieldState {
73
+ invalid: boolean;
74
+ disabled: boolean;
75
+ required: boolean;
76
+ }
77
+
78
+ const FieldContext = createContext<FieldState>({
79
+ invalid: false,
80
+ disabled: false,
81
+ required: false,
82
+ });
83
+
84
+ export interface FieldProps extends ViewProps {
85
+ className?: string;
86
+ /** `horizontal` puts a label beside the control instead of above it. */
87
+ orientation?: 'vertical' | 'horizontal';
88
+ /** Marks every `Field.Label`/`Field.Description` below as invalid. */
89
+ invalid?: boolean;
90
+ disabled?: boolean;
91
+ /** Marks every `Field.Label` below as required, same as `Label`'s own prop. */
92
+ required?: boolean;
93
+ children?: ReactNode;
94
+ }
95
+
96
+ const FieldRoot = forwardRef<View, FieldProps>(
97
+ (
98
+ {
99
+ className,
100
+ orientation = 'vertical',
101
+ invalid = false,
102
+ disabled = false,
103
+ required = false,
104
+ children,
105
+ ...props
106
+ },
107
+ ref
108
+ ) => {
109
+ const { root } = fieldVariants({ orientation, disabled });
110
+ const state = useMemo(
111
+ () => ({ invalid, disabled, required }),
112
+ [invalid, disabled, required]
113
+ );
114
+
115
+ return (
116
+ <FieldContext.Provider value={state}>
117
+ <View
118
+ ref={ref}
119
+ role="group"
120
+ accessibilityState={{ disabled }}
121
+ className={root({ className })}
122
+ {...props}
123
+ >
124
+ {children}
125
+ </View>
126
+ </FieldContext.Provider>
127
+ );
128
+ }
129
+ );
130
+ FieldRoot.displayName = 'Field';
131
+
132
+ export interface FieldContentProps extends ViewProps {
133
+ className?: string;
134
+ children?: ReactNode;
135
+ }
136
+
137
+ const FieldContent = forwardRef<View, FieldContentProps>(
138
+ ({ className, ...props }, ref) => {
139
+ const { content } = fieldVariants();
140
+ return <View ref={ref} className={content({ className })} {...props} />;
141
+ }
142
+ );
143
+ FieldContent.displayName = 'Field.Content';
144
+
145
+ export interface FieldLabelProps extends LabelProps {}
146
+
147
+ const FieldLabel = forwardRef<View, FieldLabelProps>(
148
+ ({ isRequired, isInvalid, isDisabled, ...props }, ref) => {
149
+ const ctx = useContext(FieldContext);
150
+ return (
151
+ <Label
152
+ ref={ref}
153
+ isRequired={isRequired ?? ctx.required}
154
+ isInvalid={isInvalid ?? ctx.invalid}
155
+ isDisabled={isDisabled ?? ctx.disabled}
156
+ {...props}
157
+ />
158
+ );
159
+ }
160
+ );
161
+ FieldLabel.displayName = 'Field.Label';
162
+
163
+ export interface FieldDescriptionProps extends TextProps {
164
+ className?: string;
165
+ }
166
+
167
+ const FieldDescription = forwardRef<RNText, FieldDescriptionProps>(
168
+ ({ className, ...props }, ref) => {
169
+ const { disabled } = useContext(FieldContext);
170
+ const { description } = fieldVariants({ disabled });
171
+ return <Text ref={ref} className={description({ className })} {...props} />;
172
+ }
173
+ );
174
+ FieldDescription.displayName = 'Field.Description';
175
+
176
+ export interface FieldErrorProps extends Omit<ViewProps, 'children'> {
177
+ className?: string;
178
+ /**
179
+ * Error messages to render, deduplicated by message. A single entry renders
180
+ * as plain text; more than one renders as a bulleted list, since RN has no
181
+ * `<ul>`. Prefer this over `children` when the messages come from a form's
182
+ * validation state, which is naturally an array.
183
+ */
184
+ errors?: Array<string | { message?: string } | null | undefined>;
185
+ children?: ReactNode;
186
+ }
187
+
188
+ const FieldError = forwardRef<View, FieldErrorProps>(
189
+ ({ className, errors, children, ...props }, ref) => {
190
+ const messages = useMemo(() => {
191
+ if (!errors) return null;
192
+ const seen = new Set<string>();
193
+ const out: string[] = [];
194
+ for (const item of errors) {
195
+ const message = typeof item === 'string' ? item : item?.message;
196
+ if (message && !seen.has(message)) {
197
+ seen.add(message);
198
+ out.push(message);
199
+ }
200
+ }
201
+ return out;
202
+ }, [errors]);
203
+
204
+ const hasContent = messages ? messages.length > 0 : !!children;
205
+ if (!hasContent) return null;
206
+
207
+ const { error, errorText, bullet, bulletDot } = fieldVariants();
208
+
209
+ return (
210
+ <View
211
+ ref={ref}
212
+ // Web-aligned `role`, same as `Separator` already uses — RN's older
213
+ // accessibilityRole list has no alert, the ARIA-aligned one does.
214
+ role="alert"
215
+ accessibilityLiveRegion="polite"
216
+ className={error({ className })}
217
+ {...props}
218
+ >
219
+ {messages ? (
220
+ messages.length === 1 ? (
221
+ <Text className={errorText()}>{messages[0]}</Text>
222
+ ) : (
223
+ messages.map((message) => (
224
+ <View key={message} className={bullet()}>
225
+ <Text className={bulletDot()}>{'•'}</Text>
226
+ <Text className={errorText()}>{message}</Text>
227
+ </View>
228
+ ))
229
+ )
230
+ ) : typeof children === 'string' ? (
231
+ <Text className={errorText()}>{children}</Text>
232
+ ) : (
233
+ children
234
+ )}
235
+ </View>
236
+ );
237
+ }
238
+ );
239
+ FieldError.displayName = 'Field.Error';
240
+
241
+ export interface FieldSetProps extends ViewProps {
242
+ className?: string;
243
+ children?: ReactNode;
244
+ }
245
+
246
+ const FieldSet = forwardRef<View, FieldSetProps>(
247
+ ({ className, ...props }, ref) => {
248
+ const { set } = fieldVariants();
249
+ return <View ref={ref} role="group" className={set({ className })} {...props} />;
250
+ }
251
+ );
252
+ FieldSet.displayName = 'Field.Set';
253
+
254
+ export interface FieldLegendProps extends TextProps {
255
+ className?: string;
256
+ /** `legend` reads as a section heading; `label` sits closer to a field label. */
257
+ variant?: 'legend' | 'label';
258
+ }
259
+
260
+ const FieldLegend = forwardRef<RNText, FieldLegendProps>(
261
+ ({ className, variant = 'legend', ...props }, ref) => {
262
+ const { legend } = fieldVariants({ legendVariant: variant });
263
+ return (
264
+ <Text ref={ref} role="heading" className={legend({ className })} {...props} />
265
+ );
266
+ }
267
+ );
268
+ FieldLegend.displayName = 'Field.Legend';
269
+
270
+ export interface FieldGroupProps extends ViewProps {
271
+ className?: string;
272
+ children?: ReactNode;
273
+ }
274
+
275
+ const FieldGroup = forwardRef<View, FieldGroupProps>(
276
+ ({ className, ...props }, ref) => {
277
+ const { group } = fieldVariants();
278
+ return <View ref={ref} className={group({ className })} {...props} />;
279
+ }
280
+ );
281
+ FieldGroup.displayName = 'Field.Group';
282
+
283
+ export interface FieldSeparatorProps extends ViewProps {
284
+ className?: string;
285
+ /** Centered text, e.g. "Or" — omit for a plain rule. */
286
+ children?: ReactNode;
287
+ }
288
+
289
+ const FieldSeparator = forwardRef<View, FieldSeparatorProps>(
290
+ ({ className, children, ...props }, ref) => {
291
+ const { separator, separatorLabel } = fieldVariants();
292
+ return (
293
+ <View ref={ref} className={separator({ className })} {...props}>
294
+ <Separator className="flex-1" />
295
+ {children ? (
296
+ <>
297
+ <Text className={separatorLabel()}>{children}</Text>
298
+ <Separator className="flex-1" />
299
+ </>
300
+ ) : null}
301
+ </View>
302
+ );
303
+ }
304
+ );
305
+ FieldSeparator.displayName = 'Field.Separator';
306
+
307
+ export interface FieldTitleProps extends TextProps {
308
+ className?: string;
309
+ }
310
+
311
+ const FieldTitle = forwardRef<RNText, FieldTitleProps>(
312
+ ({ className, ...props }, ref) => {
313
+ const { title } = fieldVariants();
314
+ return <Text ref={ref} className={title({ className })} {...props} />;
315
+ }
316
+ );
317
+ FieldTitle.displayName = 'Field.Title';
318
+
319
+ export const Field = Object.assign(FieldRoot, {
320
+ Content: FieldContent,
321
+ Label: FieldLabel,
322
+ Description: FieldDescription,
323
+ Error: FieldError,
324
+ Set: FieldSet,
325
+ Legend: FieldLegend,
326
+ Group: FieldGroup,
327
+ Separator: FieldSeparator,
328
+ Title: FieldTitle,
329
+ });