@thewhileloop/whileui 2.0.0 → 4.0.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 +135 -70
- package/dist/blocks/commerce/metric-card.d.ts +9 -29
- package/dist/blocks/datepicker/date-range-picker-modal.d.ts +1 -1
- package/dist/blocks/datepicker/date-range-picker-modal.js +6 -14
- package/dist/blocks/datepicker/datepicker-modal.d.ts +1 -1
- package/dist/blocks/datepicker/datepicker-modal.js +6 -12
- package/dist/blocks/datepicker/index.d.ts +1 -0
- package/dist/blocks/datepicker/index.js +1 -0
- package/dist/blocks/datepicker/time-picker-modal.d.ts +43 -0
- package/dist/blocks/datepicker/time-picker-modal.js +187 -0
- package/dist/blocks/layout/smart-input.d.ts +3 -2
- package/dist/blocks/layout/smart-input.js +33 -18
- package/dist/blocks/media/smart-image.d.ts +39 -98
- package/dist/blocks/navigation/drawer-menu.js +88 -72
- package/dist/components/alert/alert.d.ts +23 -55
- package/dist/components/alert/alert.js +1 -1
- package/dist/components/alert-dialog/alert-dialog.js +1 -4
- package/dist/components/avatar/avatar.d.ts +18 -45
- package/dist/components/avatar/avatar.js +1 -1
- package/dist/components/badge/badge.d.ts +84 -212
- package/dist/components/button/button.d.ts +69 -188
- package/dist/components/card/card.d.ts +29 -73
- package/dist/components/card/card.js +1 -1
- package/dist/components/container/container.d.ts +24 -96
- package/dist/components/data-row/data-row.d.ts +18 -38
- package/dist/components/data-row/data-row.js +1 -1
- package/dist/components/fab/fab.d.ts +87 -0
- package/dist/components/fab/fab.js +59 -0
- package/dist/components/fab/index.d.ts +1 -0
- package/dist/components/fab/index.js +1 -0
- package/dist/components/form-field/form-field.d.ts +31 -87
- package/dist/components/form-field/form-field.js +1 -1
- package/dist/components/hover-card/hover-card.d.ts +3 -2
- package/dist/components/input/input.d.ts +18 -31
- package/dist/components/input/input.js +20 -8
- package/dist/components/label/label.d.ts +3 -3
- package/dist/components/labeled-field/labeled-field.d.ts +3 -2
- package/dist/components/numeric-input/numeric-input.d.ts +27 -82
- package/dist/components/numeric-input/numeric-input.js +79 -80
- package/dist/components/otp-input/otp-input.d.ts +21 -65
- package/dist/components/popover/popover.d.ts +4 -3
- package/dist/components/pressable/pressable.d.ts +3 -3
- package/dist/components/progress/progress.d.ts +15 -39
- package/dist/components/row/row.d.ts +63 -163
- package/dist/components/segmented-control/segmented-control.d.ts +69 -241
- package/dist/components/select/select.d.ts +10 -22
- package/dist/components/select/select.js +2 -7
- package/dist/components/separator/separator.d.ts +3 -3
- package/dist/components/skeleton/skeleton.d.ts +9 -29
- package/dist/components/spinner/spinner.d.ts +12 -36
- package/dist/components/stack/stack.d.ts +63 -163
- package/dist/components/tabs/tabs.d.ts +9 -29
- package/dist/components/text/text.d.ts +51 -131
- package/dist/components/textarea/textarea.d.ts +15 -39
- package/dist/components/textarea/textarea.js +3 -3
- package/dist/components/toast/toast.d.ts +12 -36
- package/dist/components/toggle/toggle.d.ts +42 -138
- package/dist/components/toggle-group/toggle-group.d.ts +42 -138
- package/dist/components/tooltip/tooltip.d.ts +3 -2
- package/dist/components/view/view.d.ts +3 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/lib/focus-ring.d.ts +10 -0
- package/dist/lib/focus-ring.js +10 -0
- package/dist/lib/interaction-tokens.d.ts +24 -1
- package/dist/lib/interaction-tokens.js +26 -44
- package/dist/lib/motion-policy.js +2 -2
- package/dist/lib/ref-types.d.ts +3 -0
- package/dist/lib/ref-types.js +1 -0
- package/dist/lib/theme-bridge.d.ts +4 -2
- package/dist/lib/theme-bridge.js +13 -9
- package/dist/lib/theme-presets.d.ts +26 -0
- package/dist/lib/theme-presets.js +28 -0
- package/package.json +29 -28
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Beautiful, accessible, themeable components built with [Uniwind](https://uniwind.dev) + Tailwind CSS v4.
|
|
6
6
|
|
|
7
|
-
**Requirements:** React 19+, React Native 0.
|
|
7
|
+
**Requirements:** React 19.3+, React Native 0.88+, Expo 58+ (if using Expo). Supports React Native Web.
|
|
8
8
|
|
|
9
9
|
WhileUI has one renderer: React Native. iOS, Android and React Native Web are all
|
|
10
10
|
production targets — iOS and Android set the interaction baseline. See
|
|
@@ -15,7 +15,7 @@ production targets — iOS and Android set the interaction baseline. See
|
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
bun add @thewhileloop/whileui
|
|
18
|
-
bun add react@^19.
|
|
18
|
+
bun add react@^19.3.0 react-native@^0.88.0 uniwind@^1.12.2 tailwindcss@^4.0.0 react-native-reanimated react-native-safe-area-context react-native-screens
|
|
19
19
|
# Only if you use Select, Popover, Tooltip, or HoverCard:
|
|
20
20
|
bun add @rn-primitives/portal @rn-primitives/hooks @rn-primitives/slot @rn-primitives/select @rn-primitives/popover @rn-primitives/tooltip @rn-primitives/hover-card
|
|
21
21
|
```
|
|
@@ -27,16 +27,20 @@ Use `npm install` instead of `bun add` if you prefer npm.
|
|
|
27
27
|
1. **metro.config.js** (wrap with withUniwindConfig):
|
|
28
28
|
|
|
29
29
|
```js
|
|
30
|
+
const { getDefaultConfig } = require('expo/metro-config');
|
|
30
31
|
const { withUniwindConfig } = require('uniwind/metro');
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
/** @type {import('expo/metro-config').MetroConfig} */
|
|
34
|
+
const config = getDefaultConfig(__dirname);
|
|
35
|
+
|
|
36
|
+
module.exports = withUniwindConfig(config, {
|
|
33
37
|
cssEntryFile: './global.css',
|
|
34
|
-
})({
|
|
35
|
-
// your metro config
|
|
36
38
|
});
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
> `withUniwindConfig`
|
|
41
|
+
> `withUniwindConfig(config, options)` takes the config first and returns the wrapped
|
|
42
|
+
> config — it is not curried. It must wrap the outermost config object, and
|
|
43
|
+
> `cssEntryFile` must be a relative path string.
|
|
40
44
|
|
|
41
45
|
2. **global.css** at app root:
|
|
42
46
|
|
|
@@ -44,6 +48,11 @@ module.exports = withUniwindConfig({
|
|
|
44
48
|
@import 'tailwindcss';
|
|
45
49
|
@import 'uniwind';
|
|
46
50
|
|
|
51
|
+
/* Required when WhileUI is installed from npm. Tailwind does not scan
|
|
52
|
+
node_modules, so without this its class names are never detected and every
|
|
53
|
+
component renders unstyled. */
|
|
54
|
+
@source './node_modules/@thewhileloop/whileui/dist';
|
|
55
|
+
|
|
47
56
|
/* WhileUI Noir theme - copy this or create your own */
|
|
48
57
|
@layer theme {
|
|
49
58
|
:root {
|
|
@@ -216,7 +225,9 @@ function MyScreen() {
|
|
|
216
225
|
- **PortalHost:** Add `<PortalHost />` at app root for Select, Popover, Tooltip, HoverCard.
|
|
217
226
|
- **Motion:** Wrap the root in `<MotionProvider>`; use `useMotionPolicy()` for semantic
|
|
218
227
|
durations, the responsive spring, and reduced-motion behavior.
|
|
219
|
-
- **Uniwind:** `withUniwindConfig`
|
|
228
|
+
- **Uniwind:** `withUniwindConfig(config, options)` wraps metro config — config first, not
|
|
229
|
+
curried. `global.css` at app root, imported in `App.tsx`. Installing from npm also needs
|
|
230
|
+
`@source './node_modules/@thewhileloop/whileui/dist';` or every component renders unstyled.
|
|
220
231
|
- **Reference:** Block props in `packages/ui/src/blocks` (core) and `apps/showcase/templates/` (auth, profile); flow patterns in README "Flow Patterns" section.
|
|
221
232
|
|
|
222
233
|
## Components
|
|
@@ -259,18 +270,19 @@ function MyScreen() {
|
|
|
259
270
|
|
|
260
271
|
### Display
|
|
261
272
|
|
|
262
|
-
| Component | Variants
|
|
263
|
-
| --------------- |
|
|
264
|
-
| **Card** | padding (none, sm, default, lg), unstyled
|
|
265
|
-
| **Badge** | default, secondary, destructive, outline, success
|
|
266
|
-
| **
|
|
267
|
-
| **
|
|
268
|
-
| **
|
|
269
|
-
| **
|
|
270
|
-
| **
|
|
271
|
-
| **
|
|
272
|
-
| **
|
|
273
|
-
| **
|
|
273
|
+
| Component | Variants | Notes |
|
|
274
|
+
| --------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
275
|
+
| **Card** | padding (none, sm, default, lg), unstyled | Compound: Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter |
|
|
276
|
+
| **Badge** | default, secondary, destructive, outline, success | BadgeText sub-component |
|
|
277
|
+
| **FAB** | default, secondary, destructive, surface; sm/default/lg | Floating action button. `label` required; pass your own icon as children |
|
|
278
|
+
| **Alert** | default, destructive, success, warning | AlertTitle & AlertDescription |
|
|
279
|
+
| **Avatar** | sm, default, lg | AvatarImage + AvatarFallback |
|
|
280
|
+
| **DataRow** | default, compact | DataRow, DataRowLeft/Center/Right, DataRowLabel/Description/Value |
|
|
281
|
+
| **Separator** | horizontal, vertical | Themed divider |
|
|
282
|
+
| **Progress** | sm, default, lg | Value-based progress bar with accessibility |
|
|
283
|
+
| **Spinner** | sm, default, lg | ActivityIndicator wrapper |
|
|
284
|
+
| **Skeleton** | pulse, shimmer | Loading placeholder (pulse = opacity fade, shimmer = sweep) |
|
|
285
|
+
| **AspectRatio** | — | Maintain aspect ratio container |
|
|
274
286
|
|
|
275
287
|
### Layout
|
|
276
288
|
|
|
@@ -404,6 +416,7 @@ Copy from `apps/showcase/templates/profile/`:
|
|
|
404
416
|
| **DatePickerModal** | Bottom sheet modal with calendar, compact trigger |
|
|
405
417
|
| **DatePickerInline** | Inline calendar for forms or dashboards |
|
|
406
418
|
| **DateRangePickerModal** | Range selection modal with period marking |
|
|
419
|
+
| **TimePickerModal** | Hour and minute wheels in the same bottom sheet |
|
|
407
420
|
|
|
408
421
|
**Tokens without a renderer.** `@thewhileloop/whileui/tokens` exports the theme
|
|
409
422
|
and visual contracts, the presets, and `deriveThemeColors(base, scheme)` — give it
|
|
@@ -412,6 +425,55 @@ as the default presets. Safe in a build script (no React, RN or Uniwind
|
|
|
412
425
|
imports); `useThemeColors()` uses it too, so a missing `--color-primary-soft`
|
|
413
426
|
derives from the app's own primary instead of falling back to the default palette.
|
|
414
427
|
|
|
428
|
+
**Building a product preset.** Use `createThemePreset` rather than a `ThemePreset`
|
|
429
|
+
literal — it takes the 25 base colours per scheme and derives the rest:
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
import { createThemePreset, themePresetToCss } from '@thewhileloop/whileui/tokens';
|
|
433
|
+
|
|
434
|
+
const preset = createThemePreset({
|
|
435
|
+
name: 'acme',
|
|
436
|
+
description: 'Acme brand palette.',
|
|
437
|
+
light: { background: '...', foreground: '...', primary: '...' /* 25 base keys */ },
|
|
438
|
+
dark: { background: '...', foreground: '...', primary: '...' },
|
|
439
|
+
visual: { radiusLg: 12 }, // optional, partial
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
writeFileSync('theme.generated.css', themePresetToCss(preset));
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
The literal form asks for all 52 colours per scheme, including every active variant,
|
|
446
|
+
soft variant, overlay, surface, interaction state and shadow. Those are exactly what
|
|
447
|
+
`deriveThemeColors` computes, so hand-writing them produces a second derivation that
|
|
448
|
+
drifts from the library's. Override a derived value by assigning to the result.
|
|
449
|
+
|
|
450
|
+
### FAB
|
|
451
|
+
|
|
452
|
+
One primary action, raised above the content. It owns no icon dependency — pass the
|
|
453
|
+
glyph as children and colour it from `useIconColors()`, because icon libraries need a
|
|
454
|
+
concrete colour rather than a class.
|
|
455
|
+
|
|
456
|
+
```tsx
|
|
457
|
+
import { FAB, useIconColors } from '@thewhileloop/whileui';
|
|
458
|
+
import { Feather } from '@expo/vector-icons';
|
|
459
|
+
|
|
460
|
+
const icons = useIconColors();
|
|
461
|
+
|
|
462
|
+
<FAB label="New group" onPress={create}>
|
|
463
|
+
<Feather name="plus" size={24} color={icons.primaryForeground} />
|
|
464
|
+
</FAB>;
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
| Prop | Type | Default | Notes |
|
|
468
|
+
| ----------- | ------------------------------------------------ | ---------- | --------------------------------------- |
|
|
469
|
+
| `label` | `string` | — | Required. The accessible name |
|
|
470
|
+
| `variant` | `default \| secondary \| destructive \| surface` | `default` | |
|
|
471
|
+
| `size` | `sm \| default \| lg` | `default` | 48 / 56 / 64 — all clear touch minimums |
|
|
472
|
+
| `placement` | `floating \| floating-left \| inline` | `floating` | `inline` lets the caller position it |
|
|
473
|
+
|
|
474
|
+
`FABGroup` stacks secondary actions above one. Sizes start at 48dp, so a FAB never
|
|
475
|
+
needs `hitSlop`.
|
|
476
|
+
|
|
415
477
|
## Layout Primitives (Stack, Row)
|
|
416
478
|
|
|
417
479
|
Use `Stack` for vertical layouts, `Row` for horizontal layouts. Both support `gap`, `align`, and `justify` variants.
|
|
@@ -592,7 +654,7 @@ The WhileUI token contract is strict for cross-app reuse. Define these in **ever
|
|
|
592
654
|
- Required core tokens: `background`, `foreground`, `card`, `card-foreground`, `popover`, `popover-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `muted`, `muted-foreground`, `accent`, `accent-foreground`, `destructive`, `destructive-foreground`, `border`, `input`, `ring`
|
|
593
655
|
- Optional status tokens: `success`, `success-foreground`, `warning`, `warning-foreground`, `info`, `info-foreground`
|
|
594
656
|
- Optional effect tokens: `overlay`, `overlay-strong`, `surface-elevated`, `surface-border`, `surface-highlight`, `surface-translucent`, `surface-translucent-border`, `state-hover`, `state-pressed`, `state-disabled`
|
|
595
|
-
- Optional
|
|
657
|
+
- Optional visual tokens, generated from a preset: `--ui-blur-intensity-subtle`, `--ui-blur-intensity-medium`, `--ui-blur-intensity-strong`, `--ui-blur-saturation-pct`, `--ui-frosted-highlight-height`, `--ui-frosted-backdrop-blur-intensity`, `--ui-frosted-backdrop-blur-scale`, `--ui-frosted-android-tint-alpha-scale`, `--ui-frosted-android-experimental-blur`, `--ui-drawer-frosted-inset`, `--ui-drawer-frosted-radius`, `--ui-drawer-content-top-padding`
|
|
596
658
|
- Optional scale tokens: spacing (`--spacing`, `--spacing-*`), typography (`--text-*`, `--leading-*`, `--tracking-*`), radius (`--radius-*`), elevation (`--shadow-*`)
|
|
597
659
|
|
|
598
660
|
Minimal contract example:
|
|
@@ -710,53 +772,25 @@ const iconColors = useIconColors();
|
|
|
710
772
|
|
|
711
773
|
Input, Textarea, NumericInput, SmartInput, Spinner, and LoadingScreen default to theme colors when you omit `placeholderTextColor` or `spinnerColor`.
|
|
712
774
|
|
|
713
|
-
### Interaction
|
|
714
|
-
|
|
715
|
-
WhileUI components also read optional `--ui-*` tokens for deeper control of press feedback, disabled states, and motion timing:
|
|
775
|
+
### Interaction and motion
|
|
716
776
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
--ui-disabled-opacity-soft: 0.6;
|
|
723
|
-
--ui-disabled-opacity-subtle: 0.4;
|
|
724
|
-
--ui-inactive-opacity: 0.5;
|
|
725
|
-
--ui-motion-fast: 160;
|
|
726
|
-
--ui-motion-normal: 220;
|
|
727
|
-
--ui-motion-slow: 300;
|
|
728
|
-
--ui-motion-spring-damping: 20;
|
|
729
|
-
--ui-motion-spring-stiffness: 260;
|
|
730
|
-
--ui-motion-spring-mass: 0.8;
|
|
731
|
-
--ui-drawer-open-duration: 300;
|
|
732
|
-
--ui-drawer-close-duration: 220;
|
|
733
|
-
--ui-blur-intensity-subtle: 18;
|
|
734
|
-
--ui-blur-intensity-medium: 22;
|
|
735
|
-
--ui-blur-intensity-strong: 28;
|
|
736
|
-
--ui-blur-saturation-pct: 185;
|
|
737
|
-
--ui-frosted-highlight-height: 0;
|
|
738
|
-
--ui-frosted-backdrop-blur-intensity: 14;
|
|
739
|
-
--ui-frosted-backdrop-blur-scale: 0.55;
|
|
740
|
-
--ui-frosted-android-tint-alpha-scale: 1.22;
|
|
741
|
-
--ui-frosted-android-experimental-blur: 1;
|
|
742
|
-
--ui-drawer-frosted-inset: 0;
|
|
743
|
-
--ui-drawer-frosted-radius: 28;
|
|
744
|
-
--ui-drawer-content-top-padding: 0;
|
|
745
|
-
}
|
|
746
|
-
```
|
|
777
|
+
Press opacity, disabled opacity, motion durations, the responsive spring, and the drawer
|
|
778
|
+
open and close durations are package constants on `INTERACTION_TOKENS`. They are not CSS
|
|
779
|
+
variables and they do not change with a preset. Delete any `--ui-press-*`,
|
|
780
|
+
`--ui-disabled-*`, `--ui-inactive-opacity`, `--ui-motion-*`, or `--ui-drawer-open-duration`
|
|
781
|
+
/ `--ui-drawer-close-duration` declarations from a theme: nothing reads them.
|
|
747
782
|
|
|
748
|
-
For custom
|
|
783
|
+
For a custom control, read that table:
|
|
749
784
|
|
|
750
785
|
```tsx
|
|
751
786
|
import { useInteractionTokens, withInteractivePressableStyle } from '@thewhileloop/whileui';
|
|
752
787
|
```
|
|
753
788
|
|
|
754
|
-
Wrap the application in `MotionProvider` to coordinate system reduced motion, explicit
|
|
755
|
-
preview
|
|
756
|
-
|
|
757
|
-
system setting
|
|
758
|
-
|
|
759
|
-
semantic durations to zero.
|
|
789
|
+
Wrap the application in `MotionProvider` to coordinate system reduced motion, an explicit
|
|
790
|
+
preview mode, and deterministic animation-free tests. The provider is optional. Without
|
|
791
|
+
it `useMotionPolicy` reports the durations and spring from `INTERACTION_TOKENS`, and
|
|
792
|
+
Reanimated follows the system setting. `reduced` preserves non-spatial feedback while asking Reanimated to skip motion;
|
|
793
|
+
`disabled` also resolves semantic durations to zero.
|
|
760
794
|
|
|
761
795
|
```tsx
|
|
762
796
|
import { MotionProvider, useMotionPolicy } from '@thewhileloop/whileui';
|
|
@@ -851,7 +885,8 @@ Optional RN fallback tokens (hex/rgb/hsl/named):
|
|
|
851
885
|
}
|
|
852
886
|
```
|
|
853
887
|
|
|
854
|
-
Or use the first-party
|
|
888
|
+
Or use the first-party theme bridge with optional persistence. The hook owns theme
|
|
889
|
+
state and exposes event-driven setters:
|
|
855
890
|
|
|
856
891
|
```tsx
|
|
857
892
|
import { useThemeBridge, type ThemeBridgeAdapter } from '@thewhileloop/whileui';
|
|
@@ -866,6 +901,16 @@ const adapter: ThemeBridgeAdapter = {
|
|
|
866
901
|
const { mode, resolvedTheme, setMode, cycleMode } = useThemeBridge({ adapter });
|
|
867
902
|
```
|
|
868
903
|
|
|
904
|
+
When another store already owns the mode, mount the controlled bridge once near the
|
|
905
|
+
application root. Change `mode` in the owning event handler; the bridge applies and
|
|
906
|
+
persists it without mirroring state back through a callback effect.
|
|
907
|
+
|
|
908
|
+
```tsx
|
|
909
|
+
import { ThemeBridge } from '@thewhileloop/whileui';
|
|
910
|
+
|
|
911
|
+
<ThemeBridge mode={mode} adapter={adapter} />;
|
|
912
|
+
```
|
|
913
|
+
|
|
869
914
|
## Focus rings
|
|
870
915
|
|
|
871
916
|
Every interactive component draws a themed focus ring on web. It uses `:focus-visible`,
|
|
@@ -963,12 +1008,12 @@ import { Input } from '@thewhileloop/whileui';
|
|
|
963
1008
|
<Input placeholder="Email" variant="default" value={value} onChangeText={setValue} />;
|
|
964
1009
|
```
|
|
965
1010
|
|
|
966
|
-
| Prop | Type
|
|
967
|
-
| -------------------- |
|
|
968
|
-
| variant | `'default' \| 'error'` | `'default'` | Input style
|
|
969
|
-
| placeholder | `string`
|
|
970
|
-
| placeholderTextColor | `string`
|
|
971
|
-
| editable | `boolean`
|
|
1011
|
+
| Prop | Type | Default | Description |
|
|
1012
|
+
| -------------------- | --------------------------------- | ----------- | --------------------------------------------------------------------- |
|
|
1013
|
+
| variant | `'default' \| 'error' \| 'plain'` | `'default'` | Input style; `plain` drops border and fill for a large display figure |
|
|
1014
|
+
| placeholder | `string` | — | Placeholder text |
|
|
1015
|
+
| placeholderTextColor | `string` | — | Hex for placeholder; defaults to theme `mutedForeground` |
|
|
1016
|
+
| editable | `boolean` | `true` | Whether input is editable |
|
|
972
1017
|
|
|
973
1018
|
## NumericInput
|
|
974
1019
|
|
|
@@ -1550,7 +1595,8 @@ announces as a heading to assistive technology — as do `DropdownMenuLabel` and
|
|
|
1550
1595
|
|
|
1551
1596
|
## SignInForm
|
|
1552
1597
|
|
|
1553
|
-
Copy
|
|
1598
|
+
Copy `sign-in-form.tsx` and its sibling `auth-form-footer.tsx` from
|
|
1599
|
+
`apps/showcase/templates/auth/`, then:
|
|
1554
1600
|
|
|
1555
1601
|
```tsx
|
|
1556
1602
|
import { SignInForm } from './templates/auth'; // or your path
|
|
@@ -1574,7 +1620,8 @@ import { SignInForm } from './templates/auth'; // or your path
|
|
|
1574
1620
|
|
|
1575
1621
|
## SignUpForm
|
|
1576
1622
|
|
|
1577
|
-
Copy
|
|
1623
|
+
Copy `sign-up-form.tsx` and its sibling `auth-form-footer.tsx` from
|
|
1624
|
+
`apps/showcase/templates/auth/`, then:
|
|
1578
1625
|
|
|
1579
1626
|
```tsx
|
|
1580
1627
|
import { SignUpForm } from './templates/auth'; // or your path
|
|
@@ -1730,7 +1777,7 @@ const [value, setValue] = useState('');
|
|
|
1730
1777
|
| `inputSafeArea` | `boolean` | SmartInput safe-area (default `true`) |
|
|
1731
1778
|
| `keyboardVerticalOffset` | `number` | For header offset when keyboard opens |
|
|
1732
1779
|
|
|
1733
|
-
## DatePickerModal / DatePickerInline / DateRangePickerModal
|
|
1780
|
+
## DatePickerModal / DatePickerInline / DateRangePickerModal / TimePickerModal
|
|
1734
1781
|
|
|
1735
1782
|
Date selection blocks using react-native-calendars. Theme-aware via `useCalendarTheme` (Uniwind light/dark). Optional `theme` prop for custom RN color strings (hex/rgb/hsl/named). `CalendarTheme` maps to react-native-calendars Theme and supports `arrowColor`, `disabledArrowColor`, and optional font keys.
|
|
1736
1783
|
|
|
@@ -1740,6 +1787,8 @@ Date selection blocks using react-native-calendars. Theme-aware via `useCalendar
|
|
|
1740
1787
|
|
|
1741
1788
|
**DateRangePickerModal** — Range selection with period marking. Use `DateRangePickerTrigger` as the trigger content.
|
|
1742
1789
|
|
|
1790
|
+
**TimePickerModal** — Hour and minute wheels (plus AM/PM under a 12-hour `hourCycle`) in the same bottom sheet, so a form's date and time fields open the same way. `value` is always `HH:MM` in 24-hour form. Each wheel is one `adjustable` element for assistive tech and the keyboard. Use `TimePickerTrigger` as the trigger content.
|
|
1791
|
+
|
|
1743
1792
|
```tsx
|
|
1744
1793
|
import {
|
|
1745
1794
|
DatePickerModal,
|
|
@@ -1747,6 +1796,8 @@ import {
|
|
|
1747
1796
|
DatePickerInline,
|
|
1748
1797
|
DateRangePickerModal,
|
|
1749
1798
|
DateRangePickerTrigger,
|
|
1799
|
+
TimePickerModal,
|
|
1800
|
+
TimePickerTrigger,
|
|
1750
1801
|
type DateRange,
|
|
1751
1802
|
} from '@thewhileloop/whileui';
|
|
1752
1803
|
|
|
@@ -1776,6 +1827,18 @@ const [range, setRange] = useState<DateRange | null>(null);
|
|
|
1776
1827
|
onOpenChange={setRangeOpen}
|
|
1777
1828
|
trigger={<DateRangePickerTrigger value={range} placeholder="Pick range" />}
|
|
1778
1829
|
/>;
|
|
1830
|
+
|
|
1831
|
+
// Time (modal) — "HH:MM", 24-hour, whatever the wheels show
|
|
1832
|
+
const [time, setTime] = useState<string | null>(null);
|
|
1833
|
+
|
|
1834
|
+
<TimePickerModal
|
|
1835
|
+
value={time}
|
|
1836
|
+
onValueChange={setTime}
|
|
1837
|
+
open={timeOpen}
|
|
1838
|
+
onOpenChange={setTimeOpen}
|
|
1839
|
+
minuteStep={5}
|
|
1840
|
+
trigger={<TimePickerTrigger value={time} placeholder="Pick a time" />}
|
|
1841
|
+
/>;
|
|
1779
1842
|
```
|
|
1780
1843
|
|
|
1781
1844
|
| Prop | Type | Description |
|
|
@@ -1786,6 +1849,8 @@ const [range, setRange] = useState<DateRange | null>(null);
|
|
|
1786
1849
|
| `trigger` | `ReactNode` | Custom trigger (modal variants) |
|
|
1787
1850
|
| `minDate` / `maxDate` | `string` | YYYY-MM-DD bounds |
|
|
1788
1851
|
| `theme` | `CalendarTheme` | Override calendar colors |
|
|
1852
|
+
| `hourCycle` | `'h12' \| 'h23'` | Time picker wheels; defaults to locale |
|
|
1853
|
+
| `minuteStep` | `number` | Minutes between options (default 1) |
|
|
1789
1854
|
| `frosted` | `boolean` | Enable frosted surface for modal panel |
|
|
1790
1855
|
| `blurIntensity` | `number` | Override blur amount |
|
|
1791
1856
|
| `blurTintToken` | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | Tint token for frosted panel |
|
|
@@ -3,40 +3,20 @@ import { type ViewProps } from 'react-native';
|
|
|
3
3
|
import { type VariantProps } from '../../lib/tv';
|
|
4
4
|
declare const metricCardVariants: import("tailwind-variants").TVReturnType<{
|
|
5
5
|
variant: {
|
|
6
|
-
default:
|
|
7
|
-
outlined:
|
|
6
|
+
default: "bg-card";
|
|
7
|
+
outlined: "bg-transparent border border-border";
|
|
8
8
|
};
|
|
9
|
-
}, undefined, "",
|
|
9
|
+
}, undefined, "", {
|
|
10
10
|
variant: {
|
|
11
|
-
default:
|
|
12
|
-
outlined:
|
|
11
|
+
default: "bg-card";
|
|
12
|
+
outlined: "bg-transparent border border-border";
|
|
13
13
|
};
|
|
14
|
-
}, {
|
|
14
|
+
}, undefined, import("tailwind-variants").TVReturnTypeLike<{
|
|
15
15
|
variant: {
|
|
16
|
-
default:
|
|
17
|
-
outlined:
|
|
16
|
+
default: "bg-card";
|
|
17
|
+
outlined: "bg-transparent border border-border";
|
|
18
18
|
};
|
|
19
|
-
}
|
|
20
|
-
variant: {
|
|
21
|
-
default: string;
|
|
22
|
-
outlined: string;
|
|
23
|
-
};
|
|
24
|
-
}, undefined, import("tailwind-variants").TVReturnType<{
|
|
25
|
-
variant: {
|
|
26
|
-
default: string;
|
|
27
|
-
outlined: string;
|
|
28
|
-
};
|
|
29
|
-
}, undefined, "", import("tailwind-variants/dist/config.js").TVConfig<{
|
|
30
|
-
variant: {
|
|
31
|
-
default: string;
|
|
32
|
-
outlined: string;
|
|
33
|
-
};
|
|
34
|
-
}, {
|
|
35
|
-
variant: {
|
|
36
|
-
default: string;
|
|
37
|
-
outlined: string;
|
|
38
|
-
};
|
|
39
|
-
}>, unknown, unknown, undefined>>;
|
|
19
|
+
}, undefined>>;
|
|
40
20
|
export interface MetricCardProps extends ViewProps, VariantProps<typeof metricCardVariants> {
|
|
41
21
|
className?: string;
|
|
42
22
|
label: string;
|
|
@@ -29,7 +29,7 @@ export interface DateRangePickerModalProps extends FrostedSurfaceProps {
|
|
|
29
29
|
className?: string;
|
|
30
30
|
}
|
|
31
31
|
declare function formatRangeDisplay(range: DateRange | null | undefined): string;
|
|
32
|
-
export declare function DateRangePickerModal(
|
|
32
|
+
export declare function DateRangePickerModal(props: DateRangePickerModalProps): React.JSX.Element;
|
|
33
33
|
/** Compact trigger showing formatted range. Use as DateRangePickerModal's trigger prop. */
|
|
34
34
|
export declare function DateRangePickerTrigger({ value, placeholder, className, }: {
|
|
35
35
|
value?: DateRange | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback,
|
|
2
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
3
3
|
import { Modal, Pressable, View } from 'react-native';
|
|
4
4
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
5
5
|
import { useUniwind } from 'uniwind';
|
|
@@ -47,7 +47,7 @@ function buildMarkedDates(start, end, theme) {
|
|
|
47
47
|
}
|
|
48
48
|
return marked;
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
function DateRangePickerModalSession({ value, onValueChange, open, onOpenChange, minDate, maxDate, theme: customTheme, trigger, title = 'Select date range', confirmLabel = 'Done', className, frosted = false, blurIntensity, blurTintToken, }) {
|
|
51
51
|
const insets = useSafeAreaInsets();
|
|
52
52
|
const { theme } = useUniwind();
|
|
53
53
|
const colors = useThemeColors();
|
|
@@ -68,18 +68,6 @@ export function DateRangePickerModal({ value, onValueChange, open, onOpenChange,
|
|
|
68
68
|
const renderArrow = useCallback((direction) => (_jsx(Text, { className: "font-medium", style: { color: arrowColor, fontSize: arrowFontSize }, children: direction === 'left' ? '<' : '>' })), [arrowColor, arrowFontSize]);
|
|
69
69
|
const [draftStart, setDraftStart] = useState(value?.start ?? null);
|
|
70
70
|
const [draftEnd, setDraftEnd] = useState(value?.end ?? null);
|
|
71
|
-
const prevOpenRef = useRef(open);
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
if (open && !prevOpenRef.current) {
|
|
74
|
-
setDraftStart(value?.start ?? null);
|
|
75
|
-
setDraftEnd(value?.end ?? null);
|
|
76
|
-
}
|
|
77
|
-
else if (!open && prevOpenRef.current) {
|
|
78
|
-
setDraftStart(null);
|
|
79
|
-
setDraftEnd(null);
|
|
80
|
-
}
|
|
81
|
-
prevOpenRef.current = open;
|
|
82
|
-
}, [open, value]);
|
|
83
71
|
const markedDates = useMemo(() => {
|
|
84
72
|
const color = calendarTheme.selectedDayBackgroundColor;
|
|
85
73
|
const textColor = calendarTheme.selectedDayTextColor;
|
|
@@ -151,6 +139,10 @@ export function DateRangePickerModal({ value, onValueChange, open, onOpenChange,
|
|
|
151
139
|
borderWidth: visual.borderWidthHairline,
|
|
152
140
|
}, children: _jsx(Calendar, { current: draftStart ?? value?.start ?? undefined, onDayPress: handleDayPress, markedDates: markedDates, markingType: "period", minDate: minDate, maxDate: maxDate, theme: calendarTheme, renderArrow: renderArrow, enableSwipeMonths: true }, theme) }) })] }) }) })] }));
|
|
153
141
|
}
|
|
142
|
+
export function DateRangePickerModal(props) {
|
|
143
|
+
const sessionKey = props.open ? 'open' : 'closed';
|
|
144
|
+
return _jsx(DateRangePickerModalSession, { ...props }, sessionKey);
|
|
145
|
+
}
|
|
154
146
|
/** Compact trigger showing formatted range. Use as DateRangePickerModal's trigger prop. */
|
|
155
147
|
export function DateRangePickerTrigger({ value, placeholder = 'Select date range', className, }) {
|
|
156
148
|
const visual = useVisualTokens();
|
|
@@ -25,7 +25,7 @@ export interface DatePickerModalProps extends FrostedSurfaceProps {
|
|
|
25
25
|
className?: string;
|
|
26
26
|
}
|
|
27
27
|
declare function formatDisplayDate(dateString: string | null | undefined): string;
|
|
28
|
-
export declare function DatePickerModal(
|
|
28
|
+
export declare function DatePickerModal(props: DatePickerModalProps): React.JSX.Element;
|
|
29
29
|
/** Compact trigger showing formatted date. Use as DatePickerModal's trigger prop. */
|
|
30
30
|
export declare function DatePickerTrigger({ value, placeholder, className, }: {
|
|
31
31
|
value?: string | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback,
|
|
2
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
3
3
|
import { Modal, Pressable, View } from 'react-native';
|
|
4
4
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
5
5
|
import { useUniwind } from 'uniwind';
|
|
@@ -23,7 +23,7 @@ function formatDisplayDate(dateString) {
|
|
|
23
23
|
year: 'numeric',
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
function DatePickerModalSession({ value, onValueChange, open, onOpenChange, minDate, maxDate, theme: customTheme, trigger, title = 'Select date', confirmLabel = 'Done', className, frosted = false, blurIntensity, blurTintToken, }) {
|
|
27
27
|
const insets = useSafeAreaInsets();
|
|
28
28
|
const { theme } = useUniwind();
|
|
29
29
|
const colors = useThemeColors();
|
|
@@ -43,16 +43,6 @@ export function DatePickerModal({ value, onValueChange, open, onOpenChange, minD
|
|
|
43
43
|
const arrowFontSize = typographyStyle(visual, 'body').fontSize;
|
|
44
44
|
const renderArrow = useCallback((direction) => (_jsx(Text, { className: "font-medium", style: { color: arrowColor, fontSize: arrowFontSize }, children: direction === 'left' ? '<' : '>' })), [arrowColor, arrowFontSize]);
|
|
45
45
|
const [draftDate, setDraftDate] = useState(value ?? null);
|
|
46
|
-
const prevOpenRef = useRef(open);
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
if (open && !prevOpenRef.current) {
|
|
49
|
-
setDraftDate(value ?? null);
|
|
50
|
-
}
|
|
51
|
-
else if (!open && prevOpenRef.current) {
|
|
52
|
-
setDraftDate(null);
|
|
53
|
-
}
|
|
54
|
-
prevOpenRef.current = open;
|
|
55
|
-
}, [open, value]);
|
|
56
46
|
const markedDates = useMemo(() => {
|
|
57
47
|
if (!draftDate)
|
|
58
48
|
return undefined;
|
|
@@ -95,6 +85,10 @@ export function DatePickerModal({ value, onValueChange, open, onOpenChange, minD
|
|
|
95
85
|
borderWidth: visual.borderWidthHairline,
|
|
96
86
|
}, children: _jsx(Calendar, { current: draftDate ?? value ?? undefined, onDayPress: handleDayPress, markedDates: markedDates, minDate: minDate, maxDate: maxDate, theme: calendarTheme, renderArrow: renderArrow, enableSwipeMonths: true }, theme) }) })] }) }) })] }));
|
|
97
87
|
}
|
|
88
|
+
export function DatePickerModal(props) {
|
|
89
|
+
const sessionKey = props.open ? 'open' : 'closed';
|
|
90
|
+
return _jsx(DatePickerModalSession, { ...props }, sessionKey);
|
|
91
|
+
}
|
|
98
92
|
/** Compact trigger showing formatted date. Use as DatePickerModal's trigger prop. */
|
|
99
93
|
export function DatePickerTrigger({ value, placeholder = 'Select date', className, }) {
|
|
100
94
|
const visual = useVisualTokens();
|
|
@@ -2,3 +2,4 @@ export { DatePickerModal, DatePickerTrigger, formatDisplayDate, type DatePickerM
|
|
|
2
2
|
export { DatePickerInline, type DatePickerInlineProps } from './datepicker-inline';
|
|
3
3
|
export { DateRangePickerModal, DateRangePickerTrigger, formatRangeDisplay, type DateRangePickerModalProps, type DateRange, } from './date-range-picker-modal';
|
|
4
4
|
export { useCalendarTheme, type CalendarTheme } from './use-calendar-theme';
|
|
5
|
+
export { TimePickerModal, TimePickerTrigger, formatDisplayTime, parseTime, type TimePickerModalProps, type HourCycle, } from './time-picker-modal';
|
|
@@ -2,3 +2,4 @@ export { DatePickerModal, DatePickerTrigger, formatDisplayDate, } from './datepi
|
|
|
2
2
|
export { DatePickerInline } from './datepicker-inline';
|
|
3
3
|
export { DateRangePickerModal, DateRangePickerTrigger, formatRangeDisplay, } from './date-range-picker-modal';
|
|
4
4
|
export { useCalendarTheme } from './use-calendar-theme';
|
|
5
|
+
export { TimePickerModal, TimePickerTrigger, formatDisplayTime, parseTime, } from './time-picker-modal';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type FrostedSurfaceProps } from '../../lib/frosted-surface';
|
|
3
|
+
export type HourCycle = 'h12' | 'h23';
|
|
4
|
+
export interface TimePickerModalProps extends FrostedSurfaceProps {
|
|
5
|
+
/** Controlled: selected time as `HH:MM`, 24-hour, or null for none. */
|
|
6
|
+
value?: string | null;
|
|
7
|
+
/** Controlled: change handler. Always receives `HH:MM` in 24-hour form. */
|
|
8
|
+
onValueChange?: (time: string | null) => void;
|
|
9
|
+
/** Modal open state */
|
|
10
|
+
open: boolean;
|
|
11
|
+
/** Modal open change handler */
|
|
12
|
+
onOpenChange: (open: boolean) => void;
|
|
13
|
+
/** Minutes between options. 1 by default; 5 or 15 for a coarser pick. */
|
|
14
|
+
minuteStep?: number;
|
|
15
|
+
/** 12-hour with AM/PM, or 24-hour. Defaults to the device locale's preference. */
|
|
16
|
+
hourCycle?: HourCycle;
|
|
17
|
+
/** Trigger element. If not provided, no trigger is rendered. */
|
|
18
|
+
trigger?: React.ReactNode;
|
|
19
|
+
/** Title shown in modal header */
|
|
20
|
+
title?: string;
|
|
21
|
+
/** Button label for confirm */
|
|
22
|
+
confirmLabel?: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
declare function parseTime(value: string | null | undefined): {
|
|
26
|
+
hour: number;
|
|
27
|
+
minute: number;
|
|
28
|
+
} | null;
|
|
29
|
+
/** `HH:MM` as the locale writes it — "10:30 PM" or "22:30". */
|
|
30
|
+
declare function formatDisplayTime(value: string | null | undefined, hourCycle?: HourCycle): string;
|
|
31
|
+
/**
|
|
32
|
+
* A time chosen on two wheels — three with AM/PM — in the same bottom sheet as
|
|
33
|
+
* `DatePickerModal`, so a form's date and time fields open the same way.
|
|
34
|
+
*/
|
|
35
|
+
export declare function TimePickerModal(props: TimePickerModalProps): React.JSX.Element;
|
|
36
|
+
/** Compact trigger showing the formatted time. Use as TimePickerModal's trigger prop. */
|
|
37
|
+
export declare function TimePickerTrigger({ value, placeholder, hourCycle, className, }: {
|
|
38
|
+
value?: string | null;
|
|
39
|
+
placeholder?: string;
|
|
40
|
+
hourCycle?: HourCycle;
|
|
41
|
+
className?: string;
|
|
42
|
+
}): React.JSX.Element;
|
|
43
|
+
export { formatDisplayTime, parseTime };
|