@thewhileloop/whileui 4.3.0 → 4.4.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 +60 -24
- package/dist/agent/catalog.json +6 -6
- package/dist/agent/llms-components.txt +43 -22
- package/dist/agent/llms-full.txt +60 -24
- package/dist/agent/llms.txt +1 -1
- package/dist/blocks/layout/smart-input.js +2 -2
- package/dist/components/fab/fab.d.ts +12 -0
- package/dist/components/fab/fab.js +14 -2
- package/dist/components/fab/index.d.ts +1 -1
- package/dist/components/fab/index.js +1 -1
- package/dist/components/input/input.d.ts +5 -0
- package/dist/components/input/input.js +2 -2
- package/dist/components/numeric-input/numeric-input.js +2 -2
- package/dist/components/popover/popover.js +9 -1
- package/dist/components/segmented-control/segmented-control.d.ts +38 -1
- package/dist/components/segmented-control/segmented-control.js +29 -5
- package/dist/components/select/select.js +7 -1
- package/dist/components/textarea/textarea.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/dismiss-keyboard-on-open.d.ts +16 -0
- package/dist/lib/dismiss-keyboard-on-open.js +22 -0
- package/dist/lib/recipes.d.ts +20 -0
- package/dist/lib/recipes.js +19 -2
- package/dist/lib/theme-bridge.d.ts +9 -0
- package/dist/lib/theme-bridge.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -477,6 +477,10 @@ const icons = useIconColors();
|
|
|
477
477
|
`FABGroup` stacks secondary actions above one. Sizes start at 48dp, so a FAB never
|
|
478
478
|
needs `hitSlop`.
|
|
479
479
|
|
|
480
|
+
A floating FAB covers whatever scrolls under it, so a list behind one ends with room for
|
|
481
|
+
it: `contentContainerStyle={{ paddingBottom: fabClearance() }}`, passing the FAB's `size`
|
|
482
|
+
when it is not the default.
|
|
483
|
+
|
|
480
484
|
## Layout Primitives (Stack, Row)
|
|
481
485
|
|
|
482
486
|
Use `Stack` for vertical layouts, `Row` for horizontal layouts. Both support `gap`, `align`, and `justify` variants.
|
|
@@ -628,9 +632,24 @@ reported, never changed, because a brand colour is a brand decision. The built-i
|
|
|
628
632
|
`WHILEUI_PRESET_SOFT` and `WHILEUI_PRESET_ATELIER`, go through `themePresetToCss` the same
|
|
629
633
|
way.
|
|
630
634
|
|
|
631
|
-
Switch at runtime with `Uniwind.setTheme('dark')` (or `'light'`, `'system'`)
|
|
632
|
-
|
|
633
|
-
|
|
635
|
+
Switch at runtime with `Uniwind.setTheme('dark')` (or `'light'`, `'system'`), or let
|
|
636
|
+
`useThemeBridge` hold the choice. It returns the chosen `mode`, the `resolvedTheme` on
|
|
637
|
+
screen, `setMode`, `toggleTheme` and `cycleMode`. A sun/moon button calls `toggleTheme`,
|
|
638
|
+
which always flips what is on screen; `cycleMode` steps light → dark → system and suits
|
|
639
|
+
a control that shows all three, because a step into `system` that resolves to the
|
|
640
|
+
current theme looks like a dead tap. Pass an `adapter` to keep the choice across launches:
|
|
641
|
+
|
|
642
|
+
```tsx
|
|
643
|
+
const { resolvedTheme, toggleTheme } = useThemeBridge({
|
|
644
|
+
adapter: {
|
|
645
|
+
loadThemeMode: () => storage.getItem('theme') as ThemeMode | null,
|
|
646
|
+
saveThemeMode: (mode) => storage.setItem('theme', mode),
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
A named extra theme is registered with `extraThemes: ['name']` in `metro.config.js` and
|
|
652
|
+
defined with `@variant name`. The full token list, with each default, is in `theme-contract.ts`,
|
|
634
653
|
and agents can read it from the MCP server's `get_theme_tokens`.
|
|
635
654
|
|
|
636
655
|
### Frosted surfaces
|
|
@@ -816,24 +835,34 @@ Also accepts the props of React Native's `TextProps`.
|
|
|
816
835
|
## Input
|
|
817
836
|
|
|
818
837
|
```tsx
|
|
819
|
-
import { Input } from '@thewhileloop/whileui';
|
|
838
|
+
import { Input, useIconColors } from '@thewhileloop/whileui';
|
|
839
|
+
import { Feather } from '@expo/vector-icons';
|
|
840
|
+
|
|
841
|
+
const icons = useIconColors();
|
|
820
842
|
|
|
821
843
|
<Input placeholder="Email" variant="default" value={value} onChangeText={setValue} />;
|
|
844
|
+
|
|
845
|
+
<Input
|
|
846
|
+
placeholder="Search"
|
|
847
|
+
prefix={<Feather name="search" size={16} color={icons.muted} aria-hidden />}
|
|
848
|
+
/>;
|
|
822
849
|
```
|
|
823
850
|
|
|
824
|
-
On
|
|
825
|
-
|
|
851
|
+
On iOS and Android, Input, Textarea, NumericInput and SmartInput draw the caret and
|
|
852
|
+
selection in the theme's primary colour; pass `selectionColor` (and `cursorColor` on
|
|
853
|
+
Android) to override it. On web, filled surfaces keep their themed background,
|
|
854
|
+
foreground and caret when the browser autofills them.
|
|
826
855
|
|
|
827
856
|
<!-- api:Input -->
|
|
828
857
|
|
|
829
|
-
| Prop | Type | Default | Description
|
|
830
|
-
| -------------- | --------------------------------- | ----------- |
|
|
831
|
-
| aria-invalid | `boolean` | — |
|
|
832
|
-
| className | `string` | — |
|
|
833
|
-
| prefix | `React.ReactNode` | — |
|
|
834
|
-
| suffix | `React.ReactNode` | — |
|
|
835
|
-
| inputClassName | `string` | — |
|
|
836
|
-
| variant | `'default' \| 'error' \| 'plain'` | `'default'` |
|
|
858
|
+
| Prop | Type | Default | Description |
|
|
859
|
+
| -------------- | --------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
860
|
+
| aria-invalid | `boolean` | — | |
|
|
861
|
+
| className | `string` | — | |
|
|
862
|
+
| prefix | `React.ReactNode` | — | Shown before the text. A decorative icon here needs `aria-hidden`; screen readers otherwise stop on it as a separate element and read out its glyph. |
|
|
863
|
+
| suffix | `React.ReactNode` | — | Shown after the text: a unit, or a control such as a clear button. |
|
|
864
|
+
| inputClassName | `string` | — | |
|
|
865
|
+
| variant | `'default' \| 'error' \| 'plain'` | `'default'` | |
|
|
837
866
|
|
|
838
867
|
Also accepts the props of `TextInputProps`.
|
|
839
868
|
<!-- /api:Input -->
|
|
@@ -1007,18 +1036,21 @@ import {
|
|
|
1007
1036
|
</SegmentedControl>;
|
|
1008
1037
|
```
|
|
1009
1038
|
|
|
1039
|
+
When the labels do not fit a phone, `wrap="scroll"` keeps them on one row at full width
|
|
1040
|
+
and scrolls sideways, instead of wrapping or truncating them.
|
|
1041
|
+
|
|
1010
1042
|
<!-- api:SegmentedControl -->
|
|
1011
1043
|
|
|
1012
|
-
| Prop | Type
|
|
1013
|
-
| ------------- |
|
|
1014
|
-
| value | `string`
|
|
1015
|
-
| defaultValue | `string`
|
|
1016
|
-
| onValueChange | `(value: string) => void`
|
|
1017
|
-
| variant | `'default' \| 'pill'`
|
|
1018
|
-
| wrap | `boolean \| 'auto'`
|
|
1019
|
-
| size | `'default' \| 'compact'`
|
|
1020
|
-
| disabled | `boolean`
|
|
1021
|
-
| className | `string`
|
|
1044
|
+
| Prop | Type | Default | Description |
|
|
1045
|
+
| ------------- | ------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1046
|
+
| value | `string` | — | |
|
|
1047
|
+
| defaultValue | `string` | `''` | |
|
|
1048
|
+
| onValueChange | `(value: string) => void` | — | |
|
|
1049
|
+
| variant | `'default' \| 'pill'` | `'default'` | Pill = rounded-full items. |
|
|
1050
|
+
| wrap | `boolean \| 'auto' \| 'scroll'` | `'auto'` | `true` wraps segments onto more rows; `false` keeps one row and shares its width. `'scroll'` keeps one row with each segment as wide as its label and scrolls sideways when they do not fit — for labels a phone cannot show in full. `'auto'` wraps above three segments. |
|
|
1051
|
+
| size | `'default' \| 'compact'` | `'default'` | |
|
|
1052
|
+
| disabled | `boolean` | `false` | |
|
|
1053
|
+
| className | `string` | — | |
|
|
1022
1054
|
|
|
1023
1055
|
Also accepts the props of `ViewProps`.
|
|
1024
1056
|
<!-- /api:SegmentedControl -->
|
|
@@ -1333,6 +1365,10 @@ selected values stay on one line and truncate without displacing the trigger che
|
|
|
1333
1365
|
For popover presentation on web, the options panel matches the trigger width, caps
|
|
1334
1366
|
itself to the collision-available viewport, and wraps long option labels inside it.
|
|
1335
1367
|
|
|
1368
|
+
Opening a Select, or a Popover, closes the on-screen keyboard first, so options opened
|
|
1369
|
+
from a form are never hidden behind it. Both draw in a portal, where the platform would
|
|
1370
|
+
otherwise leave the keyboard up; the Modal-hosted overlays close it on their own.
|
|
1371
|
+
|
|
1336
1372
|
`SelectContent` supports optional frosted props:
|
|
1337
1373
|
|
|
1338
1374
|
<!-- api:SelectContent -->
|
package/dist/agent/catalog.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "@thewhileloop/whileui",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"guides": {
|
|
5
5
|
"quickReference": "## Quick Reference (AI / Code Generation)\n\n- **Full-screen:** `AppShell` + `Header` in `header` + `BottomNav` in `bottomNav` + content in `children`\n- **No-jump loading:** keep `AppShell` mounted, set `loading` + `skeleton` (e.g., `PageSkeleton`/`ScreenSkeleton`)\n- **Layout:** `Stack` (vertical), `Row` (horizontal) — both support `gap`, `align`, `justify`\n- **Auth callbacks:** Auth templates use objects: `onSubmit({ email, password })`, `onSubmit({ firstName, lastName, email, password })`, etc. Copy templates from `apps/showcase/catalog/templates/auth/`.\n- **PortalHost:** Add `<PortalHost />` at app root for Select, Popover, Tooltip, HoverCard.\n Inside any WhileUI overlay they render into that overlay's layer without configuration.\n- **Motion:** Wrap the root in `<MotionProvider>`; use `useMotionPolicy()` for semantic\n durations, the responsive spring, and reduced-motion behavior.\n- **Metro:** `withWhileUIMetro(config, { cssEntryFile: './global.css' })` from\n `@thewhileloop/whileui/metro` — wraps Uniwind and keeps icon fonts loading on device.\n `global.css` at app root, imported in `App.tsx`. Installing from npm also needs\n `@source './node_modules/@thewhileloop/whileui/dist';` or every component renders unstyled.\n- **States:** `LoadingState`, `EmptyState` and `ErrorState` share one shape; `ErrorState`\n takes `onRetry`. `Text variant=\"eyebrow\"` is the muted section label above a title.\n- **Reference:** Block props in `packages/ui/src/blocks` (core) and `apps/showcase/catalog/templates/` (auth, profile); flow patterns in README \"Flow Patterns\" section.\n\n### Composition grammar\n\n- Start with one page canvas and flat sections. Add a `Surface` only when grouping or\n elevation communicates something; do not wrap every section in a card.\n- Use `level=\"base\"` for grouped content, `raised` for selectable or floating regions,\n and `overlay` for content above the page. Avoid nesting more than one surfaced level.\n- Express hierarchy through `Text` variants before custom font-size classes. Keep one\n primary action per region and constrain long-form content with `Container`.\n- Forms use `FormField`; its disabled and invalid state flows into `Input`, `Textarea`,\n `NumericInput`, and `Select`. Pass a control prop only to override that inherited state.\n- Chat mechanics belong to `ChatConversation` + `SmartInput`. Message payloads, tools,\n reasoning, approvals, sources, and action rows are application-owned compositions.\n- Import Markdown and code rendering from their optional subpaths. Do not make the root\n package pay for content rendering when the screen does not use it.",
|
|
6
6
|
"compositionGrammar": "### Composition grammar\n\n- Start with one page canvas and flat sections. Add a `Surface` only when grouping or\n elevation communicates something; do not wrap every section in a card.\n- Use `level=\"base\"` for grouped content, `raised` for selectable or floating regions,\n and `overlay` for content above the page. Avoid nesting more than one surfaced level.\n- Express hierarchy through `Text` variants before custom font-size classes. Keep one\n primary action per region and constrain long-form content with `Container`.\n- Forms use `FormField`; its disabled and invalid state flows into `Input`, `Textarea`,\n `NumericInput`, and `Select`. Pass a control prop only to override that inherited state.\n- Chat mechanics belong to `ChatConversation` + `SmartInput`. Message payloads, tools,\n reasoning, approvals, sources, and action rows are application-owned compositions.\n- Import Markdown and code rendering from their optional subpaths. Do not make the root\n package pay for content rendering when the screen does not use it.",
|
|
7
7
|
"installation": "## Installation\n\n```bash\nbun add @thewhileloop/whileui\nbun 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\n# Only if you use Select, Popover, Tooltip, or HoverCard:\nbun add @rn-primitives/portal @rn-primitives/hooks @rn-primitives/slot @rn-primitives/select @rn-primitives/popover @rn-primitives/tooltip @rn-primitives/hover-card\n# Only if you import @thewhileloop/whileui/markdown:\nbun add markdown-it\n```\n\nUse `npm install` instead of `bun add` if you prefer npm.\n\n### Setup Uniwind (required)\n\n1. **metro.config.js** (wrap with `withWhileUIMetro`):\n\n```js\nconst { getDefaultConfig } = require('expo/metro-config');\nconst { withWhileUIMetro } = require('@thewhileloop/whileui/metro');\n\n/** @type {import('expo/metro-config').MetroConfig} */\nconst config = getDefaultConfig(__dirname);\n\nmodule.exports = withWhileUIMetro(config, {\n cssEntryFile: './global.css',\n});\n```\n\n> `withWhileUIMetro` calls Uniwind's `withUniwindConfig(config, options)` for you — same\n> options, config first, `cssEntryFile` a relative path — and keeps `expo-asset`,\n> `expo-font` and `@expo/vector-icons` on the real `react-native`. Without that, Uniwind's\n> import rewrite leaves every icon glyph silently missing on iOS and Android while web\n> renders fine. Pass `exemptFromUniwindRewrite: ['pkg']` for any other package that\n> imports a `react-native` export Uniwind's wrapper lacks. It must be the outermost wrapper.\n\n2. **global.css** at app root, imported from `App.tsx`:\n\n```css\n@import 'tailwindcss';\n@import 'uniwind';\n\n/* Required when installed from npm: Tailwind does not scan node_modules, so without\n this line every component renders unstyled. */\n@source './node_modules/@thewhileloop/whileui/dist';\n\n/* Then your theme: generate it (Theming, below) rather than writing tokens by hand. */\n```\n\n3. **App.tsx**:\n\n```tsx\nimport './global.css';\nimport { MotionProvider, PortalHost } from '@thewhileloop/whileui';\nimport { Button, ButtonText } from '@thewhileloop/whileui';\n\nexport default function App() {\n return (\n <MotionProvider>\n {/* Your app content */}\n <PortalHost />\n </MotionProvider>\n );\n}\n```\n\n`PortalHost` (last child of the root) is where Select, Popover, Tooltip and HoverCard\nrender. Inside a Dialog, Sheet, menu or picker they use that overlay's own layer\nautomatically, and Android back or Escape closes the innermost layer first. Pass\n`portalHost` only to target a host you placed yourself, such as in a modal screen your\nnavigator presents.\n\n### Vite + React Native Web + WhileUI setup\n\nUse the WhileUI Vite compatibility helper when your app includes portal-based primitives.\n\n1. Install Vite + RN web basics:\n\n```bash\nbun add react-dom react-native-web\nbun add -d vite @vitejs/plugin-react\n```\n\n2. `vite.config.ts`:\n\n```ts\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport { withWhileUIViteCompat } from '@thewhileloop/whileui/vite';\n\nexport default defineConfig(withWhileUIViteCompat({ plugins: [react()] }));\n```\n\n3. Keep `PortalHost` at app root when using Select/Popover/Tooltip/HoverCard.\n\nTroubleshooting:\n\n| Symptom | Likely cause | Fix |\n| -------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |\n| `Unexpected token <` from `@rn-primitives/*/dist/*.mjs` | rn-primitives ships raw JSX in dist for some packages | Use `withWhileUIViteCompat(...)` in `vite.config.ts` |\n| `className` rejected on RN primitives in consumer TS app | RN `className` augmentation not loaded from package entry | Import components from `@thewhileloop/whileui` (or side-effect import the package entry before use) |\n| Theme token resolves differently on native/web | Missing `--app-color-*` fallback for non-RN-native values | Add `--app-color-*` fallbacks in `global.css` for any `oklch(...)` token used by native APIs |",
|
|
8
|
-
"theming": "## Theming\n\nA theme is a set of CSS variables in `global.css`, in OKLCH, with a `light` and a `dark`\nvariant. Every component reads the same contract, so a theme must define **every** token\nin both modes: a missing one silently falls back to an unstyled value at runtime.\n\n### Generate a theme\n\n`themeFromSeeds` builds a complete light and dark theme from three colours, and\n`themePresetToCss` writes every token, including the hex fallbacks React Native APIs need,\nready to paste into `global.css`:\n\n```ts\nimport { checkThemeContrast, themeFromSeeds, themePresetToCss } from '@thewhileloop/whileui/tokens';\n\nconst theme = themeFromSeeds({\n name: 'studio',\n description: 'Bone paper and burnt amber',\n light: { background: '#f0ede9', foreground: '#13161c', primary: '#b65400' },\n // optional: `dark` seeds (otherwise a dark page with the brand lifted to read on it),\n // `overrides` for any base colour, `visual` for radius and density tokens\n});\n\ncheckThemeContrast(theme.light).filter((c) => !c.pass);\n// [{ pair: 'primary on background', ratio: 4.23, min: 4.5, pass: false }, …]\nconst css = themePresetToCss(theme); // paste into global.css\n```\n\nSeeds are kept as given; the colours it derives (muted text, text on each fill, status\ncolours) are tuned to WCAG AA. `checkThemeContrast` checks any palette, hand-written ones\nincluded: every text pairing at 4.5:1 and the focus ring at 3:1. A failing seed is\nreported, never changed, because a brand colour is a brand decision. The built-in presets,\n`WHILEUI_PRESET_SOFT` and `WHILEUI_PRESET_ATELIER`, go through `themePresetToCss` the same\nway.\n\nSwitch at runtime with `Uniwind.setTheme('dark')` (or `'light'`, `'system'`). A
|
|
8
|
+
"theming": "## Theming\n\nA theme is a set of CSS variables in `global.css`, in OKLCH, with a `light` and a `dark`\nvariant. Every component reads the same contract, so a theme must define **every** token\nin both modes: a missing one silently falls back to an unstyled value at runtime.\n\n### Generate a theme\n\n`themeFromSeeds` builds a complete light and dark theme from three colours, and\n`themePresetToCss` writes every token, including the hex fallbacks React Native APIs need,\nready to paste into `global.css`:\n\n```ts\nimport { checkThemeContrast, themeFromSeeds, themePresetToCss } from '@thewhileloop/whileui/tokens';\n\nconst theme = themeFromSeeds({\n name: 'studio',\n description: 'Bone paper and burnt amber',\n light: { background: '#f0ede9', foreground: '#13161c', primary: '#b65400' },\n // optional: `dark` seeds (otherwise a dark page with the brand lifted to read on it),\n // `overrides` for any base colour, `visual` for radius and density tokens\n});\n\ncheckThemeContrast(theme.light).filter((c) => !c.pass);\n// [{ pair: 'primary on background', ratio: 4.23, min: 4.5, pass: false }, …]\nconst css = themePresetToCss(theme); // paste into global.css\n```\n\nSeeds are kept as given; the colours it derives (muted text, text on each fill, status\ncolours) are tuned to WCAG AA. `checkThemeContrast` checks any palette, hand-written ones\nincluded: every text pairing at 4.5:1 and the focus ring at 3:1. A failing seed is\nreported, never changed, because a brand colour is a brand decision. The built-in presets,\n`WHILEUI_PRESET_SOFT` and `WHILEUI_PRESET_ATELIER`, go through `themePresetToCss` the same\nway.\n\nSwitch at runtime with `Uniwind.setTheme('dark')` (or `'light'`, `'system'`), or let\n`useThemeBridge` hold the choice. It returns the chosen `mode`, the `resolvedTheme` on\nscreen, `setMode`, `toggleTheme` and `cycleMode`. A sun/moon button calls `toggleTheme`,\nwhich always flips what is on screen; `cycleMode` steps light → dark → system and suits\na control that shows all three, because a step into `system` that resolves to the\ncurrent theme looks like a dead tap. Pass an `adapter` to keep the choice across launches:\n\n```tsx\nconst { resolvedTheme, toggleTheme } = useThemeBridge({\n adapter: {\n loadThemeMode: () => storage.getItem('theme') as ThemeMode | null,\n saveThemeMode: (mode) => storage.setItem('theme', mode),\n },\n});\n```\n\nA named extra theme is registered with `extraThemes: ['name']` in `metro.config.js` and\ndefined with `@variant name`. The full token list, with each default, is in `theme-contract.ts`,\nand agents can read it from the MCP server's `get_theme_tokens`.\n\n### Frosted surfaces\n\nCards and overlays take `frosted`, `blurIntensity` and `blurTintToken`\n(`'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover'`). Without a blur view\nthey tint only; register `expo-blur` once for real blur:\n\n```tsx\nimport { BlurView } from 'expo-blur';\nimport { registerFrostedBlurView } from '@thewhileloop/whileui';\n\nregisterFrostedBlurView(BlurView);\n```\n\nFor a frosted look across a whole theme, make `surface-elevated`, `surface-border` and\n`surface-highlight` translucent, in your light and dark variants or in a separate theme.\nThe tuning tokens:\n\n| Token | Default | Effect |\n| ------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------- |\n| `--ui-blur-intensity-subtle` / `-medium` / `-strong` | 18 / 26 / 34 | The three blur presets |\n| `--ui-frosted-android-tint-alpha-scale` | 1.18 | Android needs denser tint; `1` matches iOS and web |\n| `--ui-frosted-android-experimental-blur` | 1 | Uses `expo-blur`'s experimental Android blur; `0` turns it off |\n| `--ui-frosted-highlight-height` | 2 | Top sheen in px; `0` removes it |\n| `--ui-blur-saturation-pct` | 145 | Web saturation multiplier |\n| `--ui-frosted-backdrop-blur-intensity`, `-scale` | 22, 0.72 | Backdrop blur, and its ratio when a component overrides blur |\n| `--ui-drawer-frosted-inset`, `-radius`, `--ui-drawer-content-top-padding` | 10, 34, 0 | Frosted drawer shell |\n\n### Colours for React Native APIs\n\nIcons, `ActivityIndicator` and other native APIs need hex or rgb, not OKLCH.\n`useThemeColors()` returns the whole palette as React Native colours, falling back to the\n`--app-color-*` tokens a generated theme includes; `useIconColors()` is the subset icons\nuse, with `muted` mapped to `mutedForeground`.\n\n```tsx\nconst icons = useIconColors();\n<Feather name=\"heart\" size={20} color={icons.muted} />;\n```\n\nInput, Textarea, NumericInput, SmartInput, Spinner and LoadingScreen pick theme colours\nwhen you omit `placeholderTextColor` or `spinnerColor`.\n\n### Interaction and motion\n\nPress opacity, disabled opacity, motion durations, the responsive spring, and the drawer\nopen and close durations are package constants on `INTERACTION_TOKENS`. They are not CSS\nvariables, so a theme does not change them.\n\nFor a custom control, read that table:\n\n```tsx\nimport { useInteractionTokens, withInteractivePressableStyle } from '@thewhileloop/whileui';\n```\n\nWrap the application in `MotionProvider` to coordinate system reduced motion, an explicit\npreview mode, and deterministic animation-free tests. The provider is optional. Without\nit `useMotionPolicy` reports the durations and spring from `INTERACTION_TOKENS`, and\nReanimated follows the system setting. `reduced` preserves non-spatial feedback while asking Reanimated to skip motion;\n`disabled` also resolves semantic durations to zero.\n\n```tsx\nimport { MotionProvider, useMotionPolicy } from '@thewhileloop/whileui';\n\n<MotionProvider mode=\"system\">{/* application */}</MotionProvider>;\n\nconst motion = useMotionPolicy();\nmotion.durations.normal;\nmotion.springs.responsive;\n```"
|
|
9
9
|
},
|
|
10
10
|
"components": [
|
|
11
11
|
{
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"category": "Form Controls",
|
|
70
70
|
"kind": "package",
|
|
71
71
|
"summary": "TextInput wrapper with themed styling",
|
|
72
|
-
"docs": "## Input\n\n```tsx\nimport { Input } from '@thewhileloop/whileui';\n\n<Input placeholder=\"Email\" variant=\"default\" value={value} onChangeText={setValue} />;\n```\n\nOn
|
|
72
|
+
"docs": "## Input\n\n```tsx\nimport { Input, useIconColors } from '@thewhileloop/whileui';\nimport { Feather } from '@expo/vector-icons';\n\nconst icons = useIconColors();\n\n<Input placeholder=\"Email\" variant=\"default\" value={value} onChangeText={setValue} />;\n\n<Input\n placeholder=\"Search\"\n prefix={<Feather name=\"search\" size={16} color={icons.muted} aria-hidden />}\n/>;\n```\n\nOn iOS and Android, Input, Textarea, NumericInput and SmartInput draw the caret and\nselection in the theme's primary colour; pass `selectionColor` (and `cursorColor` on\nAndroid) to override it. On web, filled surfaces keep their themed background,\nforeground and caret when the browser autofills them.\n\n\n| Prop | Type | Default | Description |\n| -------------- | --------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |\n| aria-invalid | `boolean` | — | |\n| className | `string` | — | |\n| prefix | `React.ReactNode` | — | Shown before the text. A decorative icon here needs `aria-hidden`; screen readers otherwise stop on it as a separate element and read out its glyph. |\n| suffix | `React.ReactNode` | — | Shown after the text: a unit, or a control such as a clear button. |\n| inputClassName | `string` | — | |\n| variant | `'default' \\| 'error' \\| 'plain'` | `'default'` | |\n\nAlso accepts the props of `TextInputProps`."
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"name": "NumericInput",
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"category": "Form Controls",
|
|
133
133
|
"kind": "package",
|
|
134
134
|
"summary": "Uses `SelectOption` type `{value, label}`. Includes SelectGroup, SelectLabel, SelectSeparator",
|
|
135
|
-
"docs": "## Select\n\n```tsx\nimport {\n Select,\n SelectTrigger,\n SelectValue,\n SelectContent,\n SelectItem,\n} from '@thewhileloop/whileui';\n\n<Select value={value} onValueChange={setValue}>\n <SelectTrigger>\n <SelectValue placeholder=\"Select...\" />\n </SelectTrigger>\n <SelectContent frosted blurIntensity={24} blurTintToken=\"surfaceTranslucent\">\n <SelectItem label=\"Option 1\" value=\"1\" />\n <SelectItem label=\"Option 2\" value=\"2\" />\n </SelectContent>\n</Select>;\n```\n\nFocused and pressed options use the subtle interaction-state tokens; the primary token\nmarks the selected indicator without replacing the option's foreground colour. Long\nselected values stay on one line and truncate without displacing the trigger chevron.\nFor popover presentation on web, the options panel matches the trigger width, caps\nitself to the collision-available viewport, and wraps long option labels inside it.\n\n`SelectContent` supports optional frosted props:\n\n\n| Prop | Type | Default | Description |\n| ------------- | ------------------------------------------------------------------ | ------------- | ------------------------------------ |\n| className | `string` | — | |\n| portalHost | `string` | — | |\n| insets | `{ top?: number; bottom?: number; left?: number; right?: number }` | — | |\n| frosted | `boolean` | `false` | Enable the optional frosted overlay. |\n| blurIntensity | `number` | subtle preset | Override blur amount directly. |\n| blurTintToken | `FrostedTintToken` | `'popover'` | Tint token used for frosted mode. |\n\nAlso accepts the props of the underlying primitive.\n\n`SelectContent` keeps its listbox inside the safe area by default; pass `insets` only to\nchange that margin. A list taller than the screen scrolls inside the listbox on every\nplatform. Inside a Dialog, Sheet or other WhileUI overlay it renders in that\noverlay's layer automatically. Option values must be non-empty strings: the web\nprimitive rejects `\"\"`, so model \"none\" as an explicit value such as `'none'`."
|
|
135
|
+
"docs": "## Select\n\n```tsx\nimport {\n Select,\n SelectTrigger,\n SelectValue,\n SelectContent,\n SelectItem,\n} from '@thewhileloop/whileui';\n\n<Select value={value} onValueChange={setValue}>\n <SelectTrigger>\n <SelectValue placeholder=\"Select...\" />\n </SelectTrigger>\n <SelectContent frosted blurIntensity={24} blurTintToken=\"surfaceTranslucent\">\n <SelectItem label=\"Option 1\" value=\"1\" />\n <SelectItem label=\"Option 2\" value=\"2\" />\n </SelectContent>\n</Select>;\n```\n\nFocused and pressed options use the subtle interaction-state tokens; the primary token\nmarks the selected indicator without replacing the option's foreground colour. Long\nselected values stay on one line and truncate without displacing the trigger chevron.\nFor popover presentation on web, the options panel matches the trigger width, caps\nitself to the collision-available viewport, and wraps long option labels inside it.\n\nOpening a Select, or a Popover, closes the on-screen keyboard first, so options opened\nfrom a form are never hidden behind it. Both draw in a portal, where the platform would\notherwise leave the keyboard up; the Modal-hosted overlays close it on their own.\n\n`SelectContent` supports optional frosted props:\n\n\n| Prop | Type | Default | Description |\n| ------------- | ------------------------------------------------------------------ | ------------- | ------------------------------------ |\n| className | `string` | — | |\n| portalHost | `string` | — | |\n| insets | `{ top?: number; bottom?: number; left?: number; right?: number }` | — | |\n| frosted | `boolean` | `false` | Enable the optional frosted overlay. |\n| blurIntensity | `number` | subtle preset | Override blur amount directly. |\n| blurTintToken | `FrostedTintToken` | `'popover'` | Tint token used for frosted mode. |\n\nAlso accepts the props of the underlying primitive.\n\n`SelectContent` keeps its listbox inside the safe area by default; pass `insets` only to\nchange that margin. A list taller than the screen scrolls inside the listbox on every\nplatform. Inside a Dialog, Sheet or other WhileUI overlay it renders in that\noverlay's layer automatically. Option values must be non-empty strings: the web\nprimitive rejects `\"\"`, so model \"none\" as an explicit value such as `'none'`."
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
138
|
"name": "Label",
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
"category": "Form Controls",
|
|
147
147
|
"kind": "package",
|
|
148
148
|
"summary": "SegmentedControl, SegmentedControlItem, SegmentedControlItemText with wrapping layout support",
|
|
149
|
-
"docs": "## SegmentedControl\n\n```tsx\nimport {\n SegmentedControl,\n SegmentedControlItem,\n SegmentedControlItemText,\n} from '@thewhileloop/whileui';\n\n<SegmentedControl value={unit} onValueChange={setUnit} variant=\"pill\" wrap>\n <SegmentedControlItem value=\"metric\">\n <SegmentedControlItemText>Metric</SegmentedControlItemText>\n </SegmentedControlItem>\n <SegmentedControlItem value=\"imperial\">\n <SegmentedControlItemText>Imperial</SegmentedControlItemText>\n </SegmentedControlItem>\n</SegmentedControl>;\n```\n\n\n| Prop | Type
|
|
149
|
+
"docs": "## SegmentedControl\n\n```tsx\nimport {\n SegmentedControl,\n SegmentedControlItem,\n SegmentedControlItemText,\n} from '@thewhileloop/whileui';\n\n<SegmentedControl value={unit} onValueChange={setUnit} variant=\"pill\" wrap>\n <SegmentedControlItem value=\"metric\">\n <SegmentedControlItemText>Metric</SegmentedControlItemText>\n </SegmentedControlItem>\n <SegmentedControlItem value=\"imperial\">\n <SegmentedControlItemText>Imperial</SegmentedControlItemText>\n </SegmentedControlItem>\n</SegmentedControl>;\n```\n\nWhen the labels do not fit a phone, `wrap=\"scroll\"` keeps them on one row at full width\nand scrolls sideways, instead of wrapping or truncating them.\n\n\n| Prop | Type | Default | Description |\n| ------------- | ------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| value | `string` | — | |\n| defaultValue | `string` | `''` | |\n| onValueChange | `(value: string) => void` | — | |\n| variant | `'default' \\| 'pill'` | `'default'` | Pill = rounded-full items. |\n| wrap | `boolean \\| 'auto' \\| 'scroll'` | `'auto'` | `true` wraps segments onto more rows; `false` keeps one row and shares its width. `'scroll'` keeps one row with each segment as wide as its label and scrolls sideways when they do not fit — for labels a phone cannot show in full. `'auto'` wraps above three segments. |\n| size | `'default' \\| 'compact'` | `'default'` | |\n| disabled | `boolean` | `false` | |\n| className | `string` | — | |\n\nAlso accepts the props of `ViewProps`."
|
|
150
150
|
},
|
|
151
151
|
{
|
|
152
152
|
"name": "Toggle",
|
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
"category": "Display",
|
|
189
189
|
"kind": "package",
|
|
190
190
|
"summary": "Floating action button. `label` required; pass your own icon as children",
|
|
191
|
-
"docs": "### FAB\n\nOne primary action, raised above the content. It owns no icon dependency — pass the\nglyph as children and colour it from `useIconColors()`, because icon libraries need a\nconcrete colour rather than a class.\n\n```tsx\nimport { FAB, useIconColors } from '@thewhileloop/whileui';\nimport { Feather } from '@expo/vector-icons';\n\nconst icons = useIconColors();\n\n<FAB label=\"New group\" onPress={create}>\n <Feather name=\"plus\" size={24} color={icons.primaryForeground} />\n</FAB>;\n```\n\n| Prop | Type | Default | Notes |\n| ----------- | ------------------------------------------------ | ---------- | --------------------------------------- |\n| `label` | `string` | — | Required. The accessible name |\n| `variant` | `default \\| secondary \\| destructive \\| surface` | `default` | |\n| `size` | `sm \\| default \\| lg` | `default` | 48 / 56 / 64 — all clear touch minimums |\n| `placement` | `floating \\| floating-left \\| inline` | `floating` | `inline` lets the caller position it |\n\n`FABGroup` stacks secondary actions above one. Sizes start at 48dp, so a FAB never\nneeds `hitSlop
|
|
191
|
+
"docs": "### FAB\n\nOne primary action, raised above the content. It owns no icon dependency — pass the\nglyph as children and colour it from `useIconColors()`, because icon libraries need a\nconcrete colour rather than a class.\n\n```tsx\nimport { FAB, useIconColors } from '@thewhileloop/whileui';\nimport { Feather } from '@expo/vector-icons';\n\nconst icons = useIconColors();\n\n<FAB label=\"New group\" onPress={create}>\n <Feather name=\"plus\" size={24} color={icons.primaryForeground} />\n</FAB>;\n```\n\n| Prop | Type | Default | Notes |\n| ----------- | ------------------------------------------------ | ---------- | --------------------------------------- |\n| `label` | `string` | — | Required. The accessible name |\n| `variant` | `default \\| secondary \\| destructive \\| surface` | `default` | |\n| `size` | `sm \\| default \\| lg` | `default` | 48 / 56 / 64 — all clear touch minimums |\n| `placement` | `floating \\| floating-left \\| inline` | `floating` | `inline` lets the caller position it |\n\n`FABGroup` stacks secondary actions above one. Sizes start at 48dp, so a FAB never\nneeds `hitSlop`.\n\nA floating FAB covers whatever scrolls under it, so a list behind one ends with room for\nit: `contentContainerStyle={{ paddingBottom: fabClearance() }}`, passing the FAB's `size`\nwhen it is not the default."
|
|
192
192
|
},
|
|
193
193
|
{
|
|
194
194
|
"name": "Alert",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# WhileUI 4.
|
|
1
|
+
# WhileUI 4.4.0
|
|
2
2
|
|
|
3
3
|
> React Native components and blocks for iOS, Android and web, styled with Uniwind and
|
|
4
4
|
> themed through a typed token contract. Package: `@thewhileloop/whileui`.
|
|
@@ -247,23 +247,33 @@ Also accepts the props of `PressableProps`.
|
|
|
247
247
|
## Input
|
|
248
248
|
|
|
249
249
|
```tsx
|
|
250
|
-
import { Input } from '@thewhileloop/whileui';
|
|
250
|
+
import { Input, useIconColors } from '@thewhileloop/whileui';
|
|
251
|
+
import { Feather } from '@expo/vector-icons';
|
|
252
|
+
|
|
253
|
+
const icons = useIconColors();
|
|
251
254
|
|
|
252
255
|
<Input placeholder="Email" variant="default" value={value} onChangeText={setValue} />;
|
|
256
|
+
|
|
257
|
+
<Input
|
|
258
|
+
placeholder="Search"
|
|
259
|
+
prefix={<Feather name="search" size={16} color={icons.muted} aria-hidden />}
|
|
260
|
+
/>;
|
|
253
261
|
```
|
|
254
262
|
|
|
255
|
-
On
|
|
256
|
-
|
|
263
|
+
On iOS and Android, Input, Textarea, NumericInput and SmartInput draw the caret and
|
|
264
|
+
selection in the theme's primary colour; pass `selectionColor` (and `cursorColor` on
|
|
265
|
+
Android) to override it. On web, filled surfaces keep their themed background,
|
|
266
|
+
foreground and caret when the browser autofills them.
|
|
257
267
|
|
|
258
268
|
|
|
259
|
-
| Prop | Type | Default | Description
|
|
260
|
-
| -------------- | --------------------------------- | ----------- |
|
|
261
|
-
| aria-invalid | `boolean` | — |
|
|
262
|
-
| className | `string` | — |
|
|
263
|
-
| prefix | `React.ReactNode` | — |
|
|
264
|
-
| suffix | `React.ReactNode` | — |
|
|
265
|
-
| inputClassName | `string` | — |
|
|
266
|
-
| variant | `'default' \| 'error' \| 'plain'` | `'default'` |
|
|
269
|
+
| Prop | Type | Default | Description |
|
|
270
|
+
| -------------- | --------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
271
|
+
| aria-invalid | `boolean` | — | |
|
|
272
|
+
| className | `string` | — | |
|
|
273
|
+
| prefix | `React.ReactNode` | — | Shown before the text. A decorative icon here needs `aria-hidden`; screen readers otherwise stop on it as a separate element and read out its glyph. |
|
|
274
|
+
| suffix | `React.ReactNode` | — | Shown after the text: a unit, or a control such as a clear button. |
|
|
275
|
+
| inputClassName | `string` | — | |
|
|
276
|
+
| variant | `'default' \| 'error' \| 'plain'` | `'default'` | |
|
|
267
277
|
|
|
268
278
|
Also accepts the props of `TextInputProps`.
|
|
269
279
|
|
|
@@ -516,6 +526,10 @@ selected values stay on one line and truncate without displacing the trigger che
|
|
|
516
526
|
For popover presentation on web, the options panel matches the trigger width, caps
|
|
517
527
|
itself to the collision-available viewport, and wraps long option labels inside it.
|
|
518
528
|
|
|
529
|
+
Opening a Select, or a Popover, closes the on-screen keyboard first, so options opened
|
|
530
|
+
from a form are never hidden behind it. Both draw in a portal, where the platform would
|
|
531
|
+
otherwise leave the keyboard up; the Modal-hosted overlays close it on their own.
|
|
532
|
+
|
|
519
533
|
`SelectContent` supports optional frosted props:
|
|
520
534
|
|
|
521
535
|
|
|
@@ -578,17 +592,20 @@ import {
|
|
|
578
592
|
</SegmentedControl>;
|
|
579
593
|
```
|
|
580
594
|
|
|
595
|
+
When the labels do not fit a phone, `wrap="scroll"` keeps them on one row at full width
|
|
596
|
+
and scrolls sideways, instead of wrapping or truncating them.
|
|
581
597
|
|
|
582
|
-
|
|
583
|
-
|
|
|
584
|
-
|
|
|
585
|
-
|
|
|
586
|
-
|
|
|
587
|
-
|
|
|
588
|
-
|
|
|
589
|
-
|
|
|
590
|
-
|
|
|
591
|
-
|
|
|
598
|
+
|
|
599
|
+
| Prop | Type | Default | Description |
|
|
600
|
+
| ------------- | ------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
601
|
+
| value | `string` | — | |
|
|
602
|
+
| defaultValue | `string` | `''` | |
|
|
603
|
+
| onValueChange | `(value: string) => void` | — | |
|
|
604
|
+
| variant | `'default' \| 'pill'` | `'default'` | Pill = rounded-full items. |
|
|
605
|
+
| wrap | `boolean \| 'auto' \| 'scroll'` | `'auto'` | `true` wraps segments onto more rows; `false` keeps one row and shares its width. `'scroll'` keeps one row with each segment as wide as its label and scrolls sideways when they do not fit — for labels a phone cannot show in full. `'auto'` wraps above three segments. |
|
|
606
|
+
| size | `'default' \| 'compact'` | `'default'` | |
|
|
607
|
+
| disabled | `boolean` | `false` | |
|
|
608
|
+
| className | `string` | — | |
|
|
592
609
|
|
|
593
610
|
Also accepts the props of `ViewProps`.
|
|
594
611
|
|
|
@@ -744,6 +761,10 @@ const icons = useIconColors();
|
|
|
744
761
|
`FABGroup` stacks secondary actions above one. Sizes start at 48dp, so a FAB never
|
|
745
762
|
needs `hitSlop`.
|
|
746
763
|
|
|
764
|
+
A floating FAB covers whatever scrolls under it, so a list behind one ends with room for
|
|
765
|
+
it: `contentContainerStyle={{ paddingBottom: fabClearance() }}`, passing the FAB's `size`
|
|
766
|
+
when it is not the default.
|
|
767
|
+
|
|
747
768
|
---
|
|
748
769
|
|
|
749
770
|
- **Alert** (Display) — AlertTitle & AlertDescription
|
package/dist/agent/llms-full.txt
CHANGED
|
@@ -477,6 +477,10 @@ const icons = useIconColors();
|
|
|
477
477
|
`FABGroup` stacks secondary actions above one. Sizes start at 48dp, so a FAB never
|
|
478
478
|
needs `hitSlop`.
|
|
479
479
|
|
|
480
|
+
A floating FAB covers whatever scrolls under it, so a list behind one ends with room for
|
|
481
|
+
it: `contentContainerStyle={{ paddingBottom: fabClearance() }}`, passing the FAB's `size`
|
|
482
|
+
when it is not the default.
|
|
483
|
+
|
|
480
484
|
## Layout Primitives (Stack, Row)
|
|
481
485
|
|
|
482
486
|
Use `Stack` for vertical layouts, `Row` for horizontal layouts. Both support `gap`, `align`, and `justify` variants.
|
|
@@ -624,9 +628,24 @@ reported, never changed, because a brand colour is a brand decision. The built-i
|
|
|
624
628
|
`WHILEUI_PRESET_SOFT` and `WHILEUI_PRESET_ATELIER`, go through `themePresetToCss` the same
|
|
625
629
|
way.
|
|
626
630
|
|
|
627
|
-
Switch at runtime with `Uniwind.setTheme('dark')` (or `'light'`, `'system'`)
|
|
628
|
-
|
|
629
|
-
|
|
631
|
+
Switch at runtime with `Uniwind.setTheme('dark')` (or `'light'`, `'system'`), or let
|
|
632
|
+
`useThemeBridge` hold the choice. It returns the chosen `mode`, the `resolvedTheme` on
|
|
633
|
+
screen, `setMode`, `toggleTheme` and `cycleMode`. A sun/moon button calls `toggleTheme`,
|
|
634
|
+
which always flips what is on screen; `cycleMode` steps light → dark → system and suits
|
|
635
|
+
a control that shows all three, because a step into `system` that resolves to the
|
|
636
|
+
current theme looks like a dead tap. Pass an `adapter` to keep the choice across launches:
|
|
637
|
+
|
|
638
|
+
```tsx
|
|
639
|
+
const { resolvedTheme, toggleTheme } = useThemeBridge({
|
|
640
|
+
adapter: {
|
|
641
|
+
loadThemeMode: () => storage.getItem('theme') as ThemeMode | null,
|
|
642
|
+
saveThemeMode: (mode) => storage.setItem('theme', mode),
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
A named extra theme is registered with `extraThemes: ['name']` in `metro.config.js` and
|
|
648
|
+
defined with `@variant name`. The full token list, with each default, is in `theme-contract.ts`,
|
|
630
649
|
and agents can read it from the MCP server's `get_theme_tokens`.
|
|
631
650
|
|
|
632
651
|
### Frosted surfaces
|
|
@@ -806,23 +825,33 @@ Also accepts the props of React Native's `TextProps`.
|
|
|
806
825
|
## Input
|
|
807
826
|
|
|
808
827
|
```tsx
|
|
809
|
-
import { Input } from '@thewhileloop/whileui';
|
|
828
|
+
import { Input, useIconColors } from '@thewhileloop/whileui';
|
|
829
|
+
import { Feather } from '@expo/vector-icons';
|
|
830
|
+
|
|
831
|
+
const icons = useIconColors();
|
|
810
832
|
|
|
811
833
|
<Input placeholder="Email" variant="default" value={value} onChangeText={setValue} />;
|
|
834
|
+
|
|
835
|
+
<Input
|
|
836
|
+
placeholder="Search"
|
|
837
|
+
prefix={<Feather name="search" size={16} color={icons.muted} aria-hidden />}
|
|
838
|
+
/>;
|
|
812
839
|
```
|
|
813
840
|
|
|
814
|
-
On
|
|
815
|
-
|
|
841
|
+
On iOS and Android, Input, Textarea, NumericInput and SmartInput draw the caret and
|
|
842
|
+
selection in the theme's primary colour; pass `selectionColor` (and `cursorColor` on
|
|
843
|
+
Android) to override it. On web, filled surfaces keep their themed background,
|
|
844
|
+
foreground and caret when the browser autofills them.
|
|
816
845
|
|
|
817
846
|
|
|
818
|
-
| Prop | Type | Default | Description
|
|
819
|
-
| -------------- | --------------------------------- | ----------- |
|
|
820
|
-
| aria-invalid | `boolean` | — |
|
|
821
|
-
| className | `string` | — |
|
|
822
|
-
| prefix | `React.ReactNode` | — |
|
|
823
|
-
| suffix | `React.ReactNode` | — |
|
|
824
|
-
| inputClassName | `string` | — |
|
|
825
|
-
| variant | `'default' \| 'error' \| 'plain'` | `'default'` |
|
|
847
|
+
| Prop | Type | Default | Description |
|
|
848
|
+
| -------------- | --------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
849
|
+
| aria-invalid | `boolean` | — | |
|
|
850
|
+
| className | `string` | — | |
|
|
851
|
+
| prefix | `React.ReactNode` | — | Shown before the text. A decorative icon here needs `aria-hidden`; screen readers otherwise stop on it as a separate element and read out its glyph. |
|
|
852
|
+
| suffix | `React.ReactNode` | — | Shown after the text: a unit, or a control such as a clear button. |
|
|
853
|
+
| inputClassName | `string` | — | |
|
|
854
|
+
| variant | `'default' \| 'error' \| 'plain'` | `'default'` | |
|
|
826
855
|
|
|
827
856
|
Also accepts the props of `TextInputProps`.
|
|
828
857
|
|
|
@@ -989,17 +1018,20 @@ import {
|
|
|
989
1018
|
</SegmentedControl>;
|
|
990
1019
|
```
|
|
991
1020
|
|
|
1021
|
+
When the labels do not fit a phone, `wrap="scroll"` keeps them on one row at full width
|
|
1022
|
+
and scrolls sideways, instead of wrapping or truncating them.
|
|
1023
|
+
|
|
992
1024
|
|
|
993
|
-
| Prop | Type
|
|
994
|
-
| ------------- |
|
|
995
|
-
| value | `string`
|
|
996
|
-
| defaultValue | `string`
|
|
997
|
-
| onValueChange | `(value: string) => void`
|
|
998
|
-
| variant | `'default' \| 'pill'`
|
|
999
|
-
| wrap | `boolean \| 'auto'`
|
|
1000
|
-
| size | `'default' \| 'compact'`
|
|
1001
|
-
| disabled | `boolean`
|
|
1002
|
-
| className | `string`
|
|
1025
|
+
| Prop | Type | Default | Description |
|
|
1026
|
+
| ------------- | ------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1027
|
+
| value | `string` | — | |
|
|
1028
|
+
| defaultValue | `string` | `''` | |
|
|
1029
|
+
| onValueChange | `(value: string) => void` | — | |
|
|
1030
|
+
| variant | `'default' \| 'pill'` | `'default'` | Pill = rounded-full items. |
|
|
1031
|
+
| wrap | `boolean \| 'auto' \| 'scroll'` | `'auto'` | `true` wraps segments onto more rows; `false` keeps one row and shares its width. `'scroll'` keeps one row with each segment as wide as its label and scrolls sideways when they do not fit — for labels a phone cannot show in full. `'auto'` wraps above three segments. |
|
|
1032
|
+
| size | `'default' \| 'compact'` | `'default'` | |
|
|
1033
|
+
| disabled | `boolean` | `false` | |
|
|
1034
|
+
| className | `string` | — | |
|
|
1003
1035
|
|
|
1004
1036
|
Also accepts the props of `ViewProps`.
|
|
1005
1037
|
|
|
@@ -1299,6 +1331,10 @@ selected values stay on one line and truncate without displacing the trigger che
|
|
|
1299
1331
|
For popover presentation on web, the options panel matches the trigger width, caps
|
|
1300
1332
|
itself to the collision-available viewport, and wraps long option labels inside it.
|
|
1301
1333
|
|
|
1334
|
+
Opening a Select, or a Popover, closes the on-screen keyboard first, so options opened
|
|
1335
|
+
from a form are never hidden behind it. Both draw in a portal, where the platform would
|
|
1336
|
+
otherwise leave the keyboard up; the Modal-hosted overlays close it on their own.
|
|
1337
|
+
|
|
1302
1338
|
`SelectContent` supports optional frosted props:
|
|
1303
1339
|
|
|
1304
1340
|
|
package/dist/agent/llms.txt
CHANGED
|
@@ -8,7 +8,7 @@ import { useThemeColors } from '../../lib/theme-colors';
|
|
|
8
8
|
import { useInteractionTokens } from '../../lib/interaction-tokens';
|
|
9
9
|
import { useVisualTokens } from '../../lib/visual-tokens';
|
|
10
10
|
import { webAutofillField } from '../../lib/web-autofill';
|
|
11
|
-
import { controlRecipe, shadowStyle, surfaceRadius, typographyStyle } from '../../lib/recipes';
|
|
11
|
+
import { controlRecipe, shadowStyle, surfaceRadius, textCursorColors, typographyStyle, } from '../../lib/recipes';
|
|
12
12
|
// The examples list is joined into one string so it can be an effect dependency without
|
|
13
13
|
// re-running on every render. A NUL byte cannot appear in a placeholder.
|
|
14
14
|
const SEPARATOR = '\0';
|
|
@@ -144,5 +144,5 @@ export const SmartInput = forwardRef(function SmartInput({ leftSlot, centerSlot,
|
|
|
144
144
|
{ maxHeight: visual.smartInputMaxHeight },
|
|
145
145
|
!editable ? { opacity: interaction.disabledOpacity } : null,
|
|
146
146
|
style,
|
|
147
|
-
], className: cn('flex-1 text-foreground placeholder:text-muted-foreground', focusRing, variant === 'bar' && 'border-border bg-muted', variant === 'bar' && webAutofillField, variant === 'card' && 'bg-transparent', inputClassName), multiline: true, submitBehavior: submitBehavior, textAlignVertical: "top", placeholderTextColor: placeholderTextColor ?? colors.placeholder, placeholder: placeholderState.placeholder, value: value, defaultValue: defaultValue, onChangeText: placeholderState.onChangeText, editable: editable, ...props }), _jsx(SmartInputSlot, { children: rightSlot })] }));
|
|
147
|
+
], className: cn('flex-1 text-foreground placeholder:text-muted-foreground', focusRing, variant === 'bar' && 'border-border bg-muted', variant === 'bar' && webAutofillField, variant === 'card' && 'bg-transparent', inputClassName), multiline: true, submitBehavior: submitBehavior, textAlignVertical: "top", placeholderTextColor: placeholderTextColor ?? colors.placeholder, ...textCursorColors(colors), placeholder: placeholderState.placeholder, value: value, defaultValue: defaultValue, onChangeText: placeholderState.onChangeText, editable: editable, ...props }), _jsx(SmartInputSlot, { children: rightSlot })] }));
|
|
148
148
|
});
|
|
@@ -61,6 +61,18 @@ declare const fabVariants: import("tailwind-variants").TVReturnType<{
|
|
|
61
61
|
'floating-left': "absolute bottom-6 left-6";
|
|
62
62
|
};
|
|
63
63
|
}, undefined>>;
|
|
64
|
+
/** The `size` classes in points, and the `floating` placement's `bottom-6` inset. */
|
|
65
|
+
declare const FAB_SIZE: {
|
|
66
|
+
readonly default: 56;
|
|
67
|
+
readonly sm: 48;
|
|
68
|
+
readonly lg: 64;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Bottom padding that keeps a floating FAB off the last row of a scrolling screen: its
|
|
72
|
+
* inset from the edge, its height and a gap. Nothing stops the FAB covering content, so a
|
|
73
|
+
* list under one ends with `contentContainerStyle={{ paddingBottom: fabClearance() }}`.
|
|
74
|
+
*/
|
|
75
|
+
export declare function fabClearance(size?: keyof typeof FAB_SIZE): number;
|
|
64
76
|
export interface FabProps extends Omit<PressableProps, 'children'>, VariantProps<typeof fabVariants> {
|
|
65
77
|
/**
|
|
66
78
|
* What the action does, for screen readers. Required: a FAB is an icon with no
|
|
@@ -17,8 +17,8 @@ const fabVariants = tv({
|
|
|
17
17
|
surface: 'border border-border bg-card',
|
|
18
18
|
},
|
|
19
19
|
size: {
|
|
20
|
-
// 56 and
|
|
21
|
-
// and Android 48dp minimums on their own, so a FAB never needs hitSlop.
|
|
20
|
+
// 56, 48 and 64pt, as FAB_SIZE below. 56 is Material's FAB size; all three clear
|
|
21
|
+
// the iOS 44pt and Android 48dp minimums on their own, so a FAB never needs hitSlop.
|
|
22
22
|
default: 'h-14 w-14',
|
|
23
23
|
sm: 'h-12 w-12',
|
|
24
24
|
lg: 'h-16 w-16',
|
|
@@ -37,6 +37,18 @@ const fabVariants = tv({
|
|
|
37
37
|
placement: 'floating',
|
|
38
38
|
},
|
|
39
39
|
});
|
|
40
|
+
/** The `size` classes in points, and the `floating` placement's `bottom-6` inset. */
|
|
41
|
+
const FAB_SIZE = { default: 56, sm: 48, lg: 64 };
|
|
42
|
+
const FAB_EDGE_INSET = 24;
|
|
43
|
+
const FAB_CONTENT_GAP = 16;
|
|
44
|
+
/**
|
|
45
|
+
* Bottom padding that keeps a floating FAB off the last row of a scrolling screen: its
|
|
46
|
+
* inset from the edge, its height and a gap. Nothing stops the FAB covering content, so a
|
|
47
|
+
* list under one ends with `contentContainerStyle={{ paddingBottom: fabClearance() }}`.
|
|
48
|
+
*/
|
|
49
|
+
export function fabClearance(size = 'default') {
|
|
50
|
+
return FAB_EDGE_INSET + FAB_SIZE[size] + FAB_CONTENT_GAP;
|
|
51
|
+
}
|
|
40
52
|
/**
|
|
41
53
|
* A floating action button: one primary action, raised above the content.
|
|
42
54
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { FAB, FABGroup, fabVariants, type FabProps, type FabGroupProps } from './fab';
|
|
1
|
+
export { FAB, FABGroup, fabClearance, fabVariants, type FabProps, type FabGroupProps } from './fab';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { FAB, FABGroup, fabVariants } from './fab';
|
|
1
|
+
export { FAB, FABGroup, fabClearance, fabVariants } from './fab';
|
|
@@ -27,7 +27,12 @@ declare const inputVariants: import("tailwind-variants").TVReturnType<{
|
|
|
27
27
|
export interface InputProps extends TextInputProps, VariantProps<typeof inputVariants> {
|
|
28
28
|
'aria-invalid'?: boolean;
|
|
29
29
|
className?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Shown before the text. A decorative icon here needs `aria-hidden`; screen readers
|
|
32
|
+
* otherwise stop on it as a separate element and read out its glyph.
|
|
33
|
+
*/
|
|
30
34
|
prefix?: React.ReactNode;
|
|
35
|
+
/** Shown after the text: a unit, or a control such as a clear button. */
|
|
31
36
|
suffix?: React.ReactNode;
|
|
32
37
|
inputClassName?: string;
|
|
33
38
|
}
|
|
@@ -7,7 +7,7 @@ import { tv } from '../../lib/tv';
|
|
|
7
7
|
import { useThemeColors } from '../../lib/theme-colors';
|
|
8
8
|
import { useInteractionTokens } from '../../lib/interaction-tokens';
|
|
9
9
|
import { useVisualTokens } from '../../lib/visual-tokens';
|
|
10
|
-
import { typographyStyle } from '../../lib/recipes';
|
|
10
|
+
import { textCursorColors, typographyStyle } from '../../lib/recipes';
|
|
11
11
|
import { webAutofillField } from '../../lib/web-autofill';
|
|
12
12
|
import { fieldAccessibilityProps, useFormFieldState } from '../form-field/form-field';
|
|
13
13
|
const inputVariants = tv({
|
|
@@ -47,7 +47,7 @@ const Input = React.forwardRef(({ className, prefix, suffix, inputClassName, var
|
|
|
47
47
|
...typographyStyle(visual, 'label'),
|
|
48
48
|
minHeight: visual.controlHeightDefault - 2,
|
|
49
49
|
paddingVertical: visual.controlPaddingYDefault,
|
|
50
|
-
}, editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, placeholderTextColor: placeholderTextColor ?? colors.placeholder, onFocus: (event) => {
|
|
50
|
+
}, editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, placeholderTextColor: placeholderTextColor ?? colors.placeholder, ...textCursorColors(colors), onFocus: (event) => {
|
|
51
51
|
setFocused(true);
|
|
52
52
|
onFocus?.(event);
|
|
53
53
|
}, onBlur: (event) => {
|
|
@@ -7,7 +7,7 @@ import { tv } from '../../lib/tv';
|
|
|
7
7
|
import { useThemeColors } from '../../lib/theme-colors';
|
|
8
8
|
import { useInteractionTokens, withInteractivePressableStyle } from '../../lib/interaction-tokens';
|
|
9
9
|
import { useVisualTokens } from '../../lib/visual-tokens';
|
|
10
|
-
import { typographyStyle } from '../../lib/recipes';
|
|
10
|
+
import { textCursorColors, typographyStyle } from '../../lib/recipes';
|
|
11
11
|
import { webAutofillField } from '../../lib/web-autofill';
|
|
12
12
|
import { fieldAccessibilityProps, useFormFieldState } from '../form-field/form-field';
|
|
13
13
|
const numericInputVariants = tv({
|
|
@@ -177,7 +177,7 @@ const NumericInput = React.forwardRef(({ className, inputClassName, prefix, suff
|
|
|
177
177
|
}, onBlur: (event) => {
|
|
178
178
|
setFocused(false);
|
|
179
179
|
model.handleBlur(event);
|
|
180
|
-
}, keyboardType: "decimal-pad", editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, placeholderTextColor: placeholderTextColor ?? colors.placeholder, ...props }) }), _jsx(NumericAffix, { side: "suffix", visual: visual, children: suffix }), _jsx(NumericSteppers, { visible: showSteppers, inset: stepInset, size: stepSize, radius: stepRadius, model: model, interaction: interaction, visual: visual })] }));
|
|
180
|
+
}, keyboardType: "decimal-pad", editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, placeholderTextColor: placeholderTextColor ?? colors.placeholder, ...textCursorColors(colors), ...props }) }), _jsx(NumericAffix, { side: "suffix", visual: visual, children: suffix }), _jsx(NumericSteppers, { visible: showSteppers, inset: stepInset, size: stepSize, radius: stepRadius, model: model, interaction: interaction, visual: visual })] }));
|
|
181
181
|
});
|
|
182
182
|
NumericInput.displayName = 'NumericInput';
|
|
183
183
|
export { NumericInput, numericInputVariants, numericInputTextVariants };
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import { Platform, StyleSheet } from 'react-native';
|
|
4
|
+
import { useComposedRefs } from '@rn-primitives/hooks';
|
|
4
5
|
import * as PopoverPrimitive from '@rn-primitives/popover';
|
|
5
6
|
import { cn } from '../../lib/cn';
|
|
7
|
+
import { useDismissKeyboardOnOpen } from '../../lib/dismiss-keyboard-on-open';
|
|
6
8
|
import { useFrostedSurface } from '../../lib/frosted-surface';
|
|
7
9
|
import { useNestedOverlay, useOverlayPortalTarget } from '../../lib/overlay-host';
|
|
8
10
|
import { useOverlayInsets } from '../../lib/overlay-insets';
|
|
@@ -12,7 +14,13 @@ import { shadowStyle, surfacePadding, surfaceRadius } from '../../lib/recipes';
|
|
|
12
14
|
function Popover({ onOpenChange, children }) {
|
|
13
15
|
return _jsx(PopoverPrimitive.Root, { onOpenChange: onOpenChange, children: children });
|
|
14
16
|
}
|
|
15
|
-
const PopoverTrigger = React.forwardRef(({ className, ...props }, ref) =>
|
|
17
|
+
const PopoverTrigger = React.forwardRef(({ className, ...props }, ref) => {
|
|
18
|
+
const { open, setTriggerPosition } = PopoverPrimitive.useRootContext();
|
|
19
|
+
const triggerRef = React.useRef(null);
|
|
20
|
+
const composedRef = useComposedRefs(triggerRef, ref);
|
|
21
|
+
useDismissKeyboardOnOpen(open, triggerRef, setTriggerPosition);
|
|
22
|
+
return _jsx(PopoverPrimitive.Trigger, { ref: composedRef, className: cn(className), ...props });
|
|
23
|
+
});
|
|
16
24
|
PopoverTrigger.displayName = 'PopoverTrigger';
|
|
17
25
|
const PopoverContent = React.forwardRef(({ className, align = 'center', sideOffset = 4, insets, portalHost, children, style, frosted = false, blurIntensity, blurTintToken, ...props }, ref) => {
|
|
18
26
|
const frostedSurface = useFrostedSurface({
|
|
@@ -9,6 +9,11 @@ declare const segmentedControlVariants: import("tailwind-variants").TVReturnType
|
|
|
9
9
|
true: "flex-wrap justify-center gap-1";
|
|
10
10
|
false: "flex-nowrap gap-1";
|
|
11
11
|
};
|
|
12
|
+
/** Sized to its segments inside a horizontal scroll view. */
|
|
13
|
+
scroll: {
|
|
14
|
+
true: "w-auto";
|
|
15
|
+
false: "";
|
|
16
|
+
};
|
|
12
17
|
size: {
|
|
13
18
|
default: "";
|
|
14
19
|
compact: "";
|
|
@@ -22,6 +27,11 @@ declare const segmentedControlVariants: import("tailwind-variants").TVReturnType
|
|
|
22
27
|
true: "flex-wrap justify-center gap-1";
|
|
23
28
|
false: "flex-nowrap gap-1";
|
|
24
29
|
};
|
|
30
|
+
/** Sized to its segments inside a horizontal scroll view. */
|
|
31
|
+
scroll: {
|
|
32
|
+
true: "w-auto";
|
|
33
|
+
false: "";
|
|
34
|
+
};
|
|
25
35
|
size: {
|
|
26
36
|
default: "";
|
|
27
37
|
compact: "";
|
|
@@ -35,6 +45,11 @@ declare const segmentedControlVariants: import("tailwind-variants").TVReturnType
|
|
|
35
45
|
true: "flex-wrap justify-center gap-1";
|
|
36
46
|
false: "flex-nowrap gap-1";
|
|
37
47
|
};
|
|
48
|
+
/** Sized to its segments inside a horizontal scroll view. */
|
|
49
|
+
scroll: {
|
|
50
|
+
true: "w-auto";
|
|
51
|
+
false: "";
|
|
52
|
+
};
|
|
38
53
|
size: {
|
|
39
54
|
default: "";
|
|
40
55
|
compact: "";
|
|
@@ -61,6 +76,11 @@ declare const segmentedControlItemVariants: import("tailwind-variants").TVReturn
|
|
|
61
76
|
true: "grow-0";
|
|
62
77
|
false: "flex-1";
|
|
63
78
|
};
|
|
79
|
+
/** As wide as its label, so segments overflow into the scroll view rather than shrink. */
|
|
80
|
+
scroll: {
|
|
81
|
+
true: "flex-none";
|
|
82
|
+
false: "";
|
|
83
|
+
};
|
|
64
84
|
}, undefined, "min-w-0 flex-row items-center justify-center", {
|
|
65
85
|
variant: {
|
|
66
86
|
default: "";
|
|
@@ -82,6 +102,11 @@ declare const segmentedControlItemVariants: import("tailwind-variants").TVReturn
|
|
|
82
102
|
true: "grow-0";
|
|
83
103
|
false: "flex-1";
|
|
84
104
|
};
|
|
105
|
+
/** As wide as its label, so segments overflow into the scroll view rather than shrink. */
|
|
106
|
+
scroll: {
|
|
107
|
+
true: "flex-none";
|
|
108
|
+
false: "";
|
|
109
|
+
};
|
|
85
110
|
}, undefined, import("tailwind-variants").TVReturnTypeLike<{
|
|
86
111
|
variant: {
|
|
87
112
|
default: "";
|
|
@@ -103,6 +128,11 @@ declare const segmentedControlItemVariants: import("tailwind-variants").TVReturn
|
|
|
103
128
|
true: "grow-0";
|
|
104
129
|
false: "flex-1";
|
|
105
130
|
};
|
|
131
|
+
/** As wide as its label, so segments overflow into the scroll view rather than shrink. */
|
|
132
|
+
scroll: {
|
|
133
|
+
true: "flex-none";
|
|
134
|
+
false: "";
|
|
135
|
+
};
|
|
106
136
|
}, undefined>>;
|
|
107
137
|
declare const segmentedControlItemTextVariants: import("tailwind-variants").TVReturnType<{
|
|
108
138
|
selected: {
|
|
@@ -138,7 +168,14 @@ export interface SegmentedControlProps extends ViewProps {
|
|
|
138
168
|
onValueChange?: (value: string) => void;
|
|
139
169
|
/** Pill = rounded-full items. */
|
|
140
170
|
variant?: 'default' | 'pill';
|
|
141
|
-
|
|
171
|
+
/**
|
|
172
|
+
* `true` wraps segments onto more rows; `false` keeps one row and shares its width.
|
|
173
|
+
* `'scroll'` keeps one row with each segment as wide as its label and scrolls
|
|
174
|
+
* sideways when they do not fit — for labels a phone cannot show in full.
|
|
175
|
+
* `'auto'` wraps above three segments.
|
|
176
|
+
* @default 'auto'
|
|
177
|
+
*/
|
|
178
|
+
wrap?: boolean | 'auto' | 'scroll';
|
|
142
179
|
size?: 'default' | 'compact';
|
|
143
180
|
disabled?: boolean;
|
|
144
181
|
className?: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
|
3
|
-
import { Pressable, Text, View, } from 'react-native';
|
|
3
|
+
import { Pressable, ScrollView, Text, View, } from 'react-native';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { focusRingInset } from '../../lib/focus-ring';
|
|
6
6
|
import { tv } from '../../lib/tv';
|
|
@@ -19,6 +19,11 @@ const segmentedControlVariants = tv({
|
|
|
19
19
|
true: 'flex-wrap justify-center gap-1',
|
|
20
20
|
false: 'flex-nowrap gap-1',
|
|
21
21
|
},
|
|
22
|
+
/** Sized to its segments inside a horizontal scroll view. */
|
|
23
|
+
scroll: {
|
|
24
|
+
true: 'w-auto',
|
|
25
|
+
false: '',
|
|
26
|
+
},
|
|
22
27
|
size: {
|
|
23
28
|
default: '',
|
|
24
29
|
compact: '',
|
|
@@ -27,6 +32,7 @@ const segmentedControlVariants = tv({
|
|
|
27
32
|
defaultVariants: {
|
|
28
33
|
variant: 'default',
|
|
29
34
|
wrap: true,
|
|
35
|
+
scroll: false,
|
|
30
36
|
size: 'default',
|
|
31
37
|
},
|
|
32
38
|
});
|
|
@@ -53,6 +59,11 @@ const segmentedControlItemVariants = tv({
|
|
|
53
59
|
true: 'grow-0',
|
|
54
60
|
false: 'flex-1',
|
|
55
61
|
},
|
|
62
|
+
/** As wide as its label, so segments overflow into the scroll view rather than shrink. */
|
|
63
|
+
scroll: {
|
|
64
|
+
true: 'flex-none',
|
|
65
|
+
false: '',
|
|
66
|
+
},
|
|
56
67
|
},
|
|
57
68
|
defaultVariants: {
|
|
58
69
|
variant: 'default',
|
|
@@ -60,6 +71,7 @@ const segmentedControlItemVariants = tv({
|
|
|
60
71
|
size: 'default',
|
|
61
72
|
disabled: false,
|
|
62
73
|
wrap: true,
|
|
74
|
+
scroll: false,
|
|
63
75
|
},
|
|
64
76
|
});
|
|
65
77
|
const segmentedControlItemTextVariants = tv({
|
|
@@ -105,6 +117,7 @@ const SegmentedControlContext = createContext({
|
|
|
105
117
|
variant: 'default',
|
|
106
118
|
size: 'default',
|
|
107
119
|
wrap: true,
|
|
120
|
+
scroll: false,
|
|
108
121
|
disabled: false,
|
|
109
122
|
});
|
|
110
123
|
const SegmentedControlItemContext = createContext({
|
|
@@ -117,7 +130,8 @@ function SegmentedControl({ value: controlledValue, defaultValue = '', onValueCh
|
|
|
117
130
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
118
131
|
const value = controlledValue ?? internalValue;
|
|
119
132
|
const itemCount = useMemo(() => React.Children.toArray(children).filter((child) => React.isValidElement(child)).length, [children]);
|
|
120
|
-
const
|
|
133
|
+
const scroll = wrap === 'scroll';
|
|
134
|
+
const shouldWrap = wrap === 'auto' ? itemCount > 3 : wrap === true;
|
|
121
135
|
const handleValueChange = useCallback((nextValue) => {
|
|
122
136
|
if (disabled || value === nextValue) {
|
|
123
137
|
return;
|
|
@@ -125,11 +139,20 @@ function SegmentedControl({ value: controlledValue, defaultValue = '', onValueCh
|
|
|
125
139
|
setInternalValue(nextValue);
|
|
126
140
|
onValueChange?.(nextValue);
|
|
127
141
|
}, [disabled, onValueChange, value]);
|
|
128
|
-
const contextValue = useMemo(() => ({
|
|
129
|
-
|
|
142
|
+
const contextValue = useMemo(() => ({
|
|
143
|
+
value,
|
|
144
|
+
onValueChange: handleValueChange,
|
|
145
|
+
variant,
|
|
146
|
+
size,
|
|
147
|
+
wrap: shouldWrap,
|
|
148
|
+
scroll,
|
|
149
|
+
disabled,
|
|
150
|
+
}), [disabled, handleValueChange, scroll, shouldWrap, size, value, variant]);
|
|
151
|
+
const group = (_jsx(View, { className: cn(segmentedControlVariants({ variant, wrap: shouldWrap, scroll, size }), className), style: [getSegmentedContainerStyle(visual, colors, variant), styleProp], accessibilityRole: "radiogroup", ...props, children: children }));
|
|
152
|
+
return (_jsx(SegmentedControlContext.Provider, { value: contextValue, children: scroll ? (_jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, children: group })) : (group) }));
|
|
130
153
|
}
|
|
131
154
|
function SegmentedControlItem({ value: itemValue, className, children, disabled: itemDisabled, style: styleProp, ...props }) {
|
|
132
|
-
const { value, onValueChange, variant, size, wrap, disabled } = useContext(SegmentedControlContext);
|
|
155
|
+
const { value, onValueChange, variant, size, wrap, scroll, disabled } = useContext(SegmentedControlContext);
|
|
133
156
|
const selected = value === itemValue;
|
|
134
157
|
const finalDisabled = disabled || Boolean(itemDisabled);
|
|
135
158
|
const interaction = useInteractionTokens();
|
|
@@ -150,6 +173,7 @@ function SegmentedControlItem({ value: itemValue, className, children, disabled:
|
|
|
150
173
|
size,
|
|
151
174
|
disabled: finalDisabled,
|
|
152
175
|
wrap,
|
|
176
|
+
scroll,
|
|
153
177
|
}), focusRingInset, className), onPress: () => onValueChange(itemValue), accessibilityRole: "radio",
|
|
154
178
|
// A radio is checked, not selected. `selected` maps to `aria-selected`, which is
|
|
155
179
|
// not a valid attribute on `role="radio"` — assistive technology could not tell
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import { Platform, ScrollView, StyleSheet, useWindowDimensions, View, } from 'react-native';
|
|
4
|
+
import { useComposedRefs } from '@rn-primitives/hooks';
|
|
4
5
|
import * as SelectPrimitive from '@rn-primitives/select';
|
|
5
6
|
import { FullWindowOverlay as RNFullWindowOverlay } from 'react-native-screens';
|
|
6
7
|
import { Text } from '../text';
|
|
7
8
|
import { cn } from '../../lib/cn';
|
|
9
|
+
import { useDismissKeyboardOnOpen } from '../../lib/dismiss-keyboard-on-open';
|
|
8
10
|
import { focusRing } from '../../lib/focus-ring';
|
|
9
11
|
import { useInteractionTokens } from '../../lib/interaction-tokens';
|
|
10
12
|
import { useFrostedSurface } from '../../lib/frosted-surface';
|
|
@@ -26,7 +28,11 @@ const SelectTrigger = React.forwardRef(({ className, children, style: styleProp,
|
|
|
26
28
|
const visual = useVisualTokens();
|
|
27
29
|
const field = useFormFieldState();
|
|
28
30
|
const effectiveDisabled = disabled ?? field.disabled;
|
|
29
|
-
|
|
31
|
+
const { open, setTriggerPosition } = SelectPrimitive.useRootContext();
|
|
32
|
+
const triggerRef = React.useRef(null);
|
|
33
|
+
const composedRef = useComposedRefs(triggerRef, ref);
|
|
34
|
+
useDismissKeyboardOnOpen(open, triggerRef, setTriggerPosition);
|
|
35
|
+
return (_jsxs(SelectPrimitive.Trigger, { ref: composedRef, className: cn('border-border bg-muted flex w-full flex-row items-center justify-between gap-2 shadow-sm active:bg-state-pressed', field.invalid && 'border-destructive', focusRing, effectiveDisabled && '', className),
|
|
30
36
|
// Flattened, not an array. On web this primitive is merged through a Slot that
|
|
31
37
|
// spreads `style`, and spreading an array yields { 0: …, 1: … } — numeric keys
|
|
32
38
|
// that reach the DOM as indexed style properties and throw. The Select rendered
|
|
@@ -7,7 +7,7 @@ import { tv } from '../../lib/tv';
|
|
|
7
7
|
import { useThemeColors } from '../../lib/theme-colors';
|
|
8
8
|
import { useInteractionTokens } from '../../lib/interaction-tokens';
|
|
9
9
|
import { useVisualTokens } from '../../lib/visual-tokens';
|
|
10
|
-
import { typographyStyle } from '../../lib/recipes';
|
|
10
|
+
import { textCursorColors, typographyStyle } from '../../lib/recipes';
|
|
11
11
|
import { webAutofillField } from '../../lib/web-autofill';
|
|
12
12
|
import { fieldAccessibilityProps, useFormFieldState } from '../form-field/form-field';
|
|
13
13
|
const textareaVariants = tv({
|
|
@@ -48,7 +48,7 @@ const Textarea = React.forwardRef(({ className, size = 'default', placeholderTex
|
|
|
48
48
|
paddingVertical: visual.controlPaddingYDefault,
|
|
49
49
|
...(!effectiveEditable ? { opacity: interaction.disabledOpacity } : null),
|
|
50
50
|
};
|
|
51
|
-
return (_jsx(TextInput, { ref: ref, ...fieldAccessibilityProps(field, props), className: cn(textareaVariants({ size }), field.invalid && 'border-destructive', focusField, webAutofillField, className), style: style, multiline: true, textAlignVertical: "top", placeholderTextColor: placeholderTextColor ?? colors.placeholder, editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, ...props }));
|
|
51
|
+
return (_jsx(TextInput, { ref: ref, ...fieldAccessibilityProps(field, props), className: cn(textareaVariants({ size }), field.invalid && 'border-destructive', focusField, webAutofillField, className), style: style, multiline: true, textAlignVertical: "top", placeholderTextColor: placeholderTextColor ?? colors.placeholder, ...textCursorColors(colors), editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, ...props }));
|
|
52
52
|
});
|
|
53
53
|
Textarea.displayName = 'Textarea';
|
|
54
54
|
export { Textarea, textareaVariants };
|
package/dist/index.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export { OTPInput, otpCellVariants, type OTPInputProps } from './components/otp-
|
|
|
30
30
|
export { FormField, FormLabel, FormControl, FormHint, FormMessage, useFormFieldState, formFieldVariants, formControlVariants, type FormFieldProps, type FormLabelProps, type FormControlProps, type FormHintProps, type FormMessageProps, type FormFieldState, } from './components/form-field';
|
|
31
31
|
export { LabeledField, LabeledFieldControl, type LabeledFieldProps, type LabeledFieldControlProps, } from './components/labeled-field';
|
|
32
32
|
export { Badge, BadgeText, badgeVariants, badgeTextVariants, type BadgeProps, type BadgeTextProps, } from './components/badge';
|
|
33
|
-
export { FAB, FABGroup, fabVariants, type FabProps, type FabGroupProps } from './components/fab';
|
|
33
|
+
export { FAB, FABGroup, fabClearance, fabVariants, type FabProps, type FabGroupProps, } from './components/fab';
|
|
34
34
|
export { Alert, AlertTitle, AlertDescription, alertVariants, type AlertProps, type AlertTitleProps, type AlertDescriptionProps, } from './components/alert';
|
|
35
35
|
export { Switch, type SwitchProps } from './components/switch';
|
|
36
36
|
export { Checkbox, type CheckboxProps } from './components/checkbox';
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,7 @@ export { OTPInput, otpCellVariants } from './components/otp-input';
|
|
|
30
30
|
export { FormField, FormLabel, FormControl, FormHint, FormMessage, useFormFieldState, formFieldVariants, formControlVariants, } from './components/form-field';
|
|
31
31
|
export { LabeledField, LabeledFieldControl, } from './components/labeled-field';
|
|
32
32
|
export { Badge, BadgeText, badgeVariants, badgeTextVariants, } from './components/badge';
|
|
33
|
-
export { FAB, FABGroup, fabVariants } from './components/fab';
|
|
33
|
+
export { FAB, FABGroup, fabClearance, fabVariants, } from './components/fab';
|
|
34
34
|
export { Alert, AlertTitle, AlertDescription, alertVariants, } from './components/alert';
|
|
35
35
|
export { Switch } from './components/switch';
|
|
36
36
|
export { Checkbox } from './components/checkbox';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import type { LayoutPosition } from '@rn-primitives/hooks';
|
|
3
|
+
interface Measurable {
|
|
4
|
+
measure: (callback: (x: number, y: number, width: number, height: number, pageX: number, pageY: number) => void) => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Closes the keyboard when a portal overlay opens, then measures its trigger again.
|
|
8
|
+
*
|
|
9
|
+
* `Select` and `Popover` draw in a portal rather than a Modal, so iOS and Android leave the
|
|
10
|
+
* keyboard up and it covers the lower half of the content. Closing it moves any
|
|
11
|
+
* keyboard-avoiding screen, which leaves the trigger away from where it was measured on
|
|
12
|
+
* press, so the position is taken again once the keyboard has gone. Modal-hosted overlays
|
|
13
|
+
* need none of this: the platform closes the keyboard when the Modal's window takes focus.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useDismissKeyboardOnOpen(open: boolean, triggerRef: RefObject<Measurable | null>, setTriggerPosition: (position: LayoutPosition) => void): void;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { Keyboard, Platform } from 'react-native';
|
|
3
|
+
/**
|
|
4
|
+
* Closes the keyboard when a portal overlay opens, then measures its trigger again.
|
|
5
|
+
*
|
|
6
|
+
* `Select` and `Popover` draw in a portal rather than a Modal, so iOS and Android leave the
|
|
7
|
+
* keyboard up and it covers the lower half of the content. Closing it moves any
|
|
8
|
+
* keyboard-avoiding screen, which leaves the trigger away from where it was measured on
|
|
9
|
+
* press, so the position is taken again once the keyboard has gone. Modal-hosted overlays
|
|
10
|
+
* need none of this: the platform closes the keyboard when the Modal's window takes focus.
|
|
11
|
+
*/
|
|
12
|
+
export function useDismissKeyboardOnOpen(open, triggerRef, setTriggerPosition) {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!open || Platform.OS === 'web' || !Keyboard.isVisible())
|
|
15
|
+
return;
|
|
16
|
+
const subscription = Keyboard.addListener('keyboardDidHide', () => {
|
|
17
|
+
triggerRef.current?.measure((_x, _y, width, height, pageX, pageY) => setTriggerPosition({ width, height, pageX, pageY }));
|
|
18
|
+
});
|
|
19
|
+
Keyboard.dismiss();
|
|
20
|
+
return () => subscription.remove();
|
|
21
|
+
}, [open, triggerRef, setTriggerPosition]);
|
|
22
|
+
}
|
package/dist/lib/recipes.d.ts
CHANGED
|
@@ -22,6 +22,26 @@ export declare function surfacePadding(visual: VisualTokens, padding?: SurfacePa
|
|
|
22
22
|
export declare function surfaceRadius(visual: VisualTokens, radius?: 'sm' | 'md' | 'lg' | 'xl'): number;
|
|
23
23
|
export declare function surfaceRecipe(visual: VisualTokens, options?: SurfaceRecipeOptions): ViewStyle;
|
|
24
24
|
export declare function shadowStyle(visual: VisualTokens, colors: ThemeColors, tier: ShadowTier): ViewStyle;
|
|
25
|
+
/**
|
|
26
|
+
* Caret and selection colours for a text field: the theme's primary, where React Native
|
|
27
|
+
* would otherwise use the platform blue. iOS tints the caret, handles and highlight from
|
|
28
|
+
* `selectionColor` alone. Android paints `selectionColor` as the highlight at full
|
|
29
|
+
* strength, which would hide the selected text, so it gets a translucent highlight and
|
|
30
|
+
* the caret and handles take the solid colour. The web keeps the browser's caret.
|
|
31
|
+
*/
|
|
32
|
+
export declare function textCursorColors(colors: ThemeColors): {
|
|
33
|
+
cursorColor: string;
|
|
34
|
+
selectionHandleColor: string;
|
|
35
|
+
selectionColor: string;
|
|
36
|
+
} | {
|
|
37
|
+
selectionColor: string;
|
|
38
|
+
cursorColor?: undefined;
|
|
39
|
+
selectionHandleColor?: undefined;
|
|
40
|
+
} | {
|
|
41
|
+
cursorColor?: undefined;
|
|
42
|
+
selectionHandleColor?: undefined;
|
|
43
|
+
selectionColor?: undefined;
|
|
44
|
+
};
|
|
25
45
|
export declare function typographyStyle(visual: VisualTokens, role: TypographyRole): TextStyle;
|
|
26
46
|
export declare function useControlRecipe(size?: ControlSize): ViewStyle;
|
|
27
47
|
export declare function useFieldRecipe(size?: ControlSize): ViewStyle;
|
package/dist/lib/recipes.js
CHANGED
|
@@ -108,17 +108,34 @@ export function shadowStyle(visual, colors, tier) {
|
|
|
108
108
|
offsetX: 0,
|
|
109
109
|
offsetY,
|
|
110
110
|
blurRadius: radius * 2,
|
|
111
|
-
color:
|
|
111
|
+
color: colorWithOpacity(colors.shadow, opacity),
|
|
112
112
|
},
|
|
113
113
|
],
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
|
-
function
|
|
116
|
+
function colorWithOpacity(color, opacity) {
|
|
117
117
|
const parsed = parse(color);
|
|
118
118
|
if (!parsed)
|
|
119
119
|
return color;
|
|
120
120
|
return formatRgb({ ...parsed, alpha: opacity });
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Caret and selection colours for a text field: the theme's primary, where React Native
|
|
124
|
+
* would otherwise use the platform blue. iOS tints the caret, handles and highlight from
|
|
125
|
+
* `selectionColor` alone. Android paints `selectionColor` as the highlight at full
|
|
126
|
+
* strength, which would hide the selected text, so it gets a translucent highlight and
|
|
127
|
+
* the caret and handles take the solid colour. The web keeps the browser's caret.
|
|
128
|
+
*/
|
|
129
|
+
export function textCursorColors(colors) {
|
|
130
|
+
if (Platform.OS === 'android') {
|
|
131
|
+
return {
|
|
132
|
+
cursorColor: colors.primary,
|
|
133
|
+
selectionHandleColor: colors.primary,
|
|
134
|
+
selectionColor: colorWithOpacity(colors.primary, 0.3),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return Platform.OS === 'ios' ? { selectionColor: colors.primary } : {};
|
|
138
|
+
}
|
|
122
139
|
// Component-owned text sizes. Font family/weight stays on className
|
|
123
140
|
// (font-medium etc.) so app font mappings keep working.
|
|
124
141
|
export function typographyStyle(visual, role) {
|
|
@@ -14,10 +14,19 @@ export interface UseThemeBridgeOptions {
|
|
|
14
14
|
adapter?: ThemeBridgeAdapter;
|
|
15
15
|
}
|
|
16
16
|
export interface UseThemeBridgeResult {
|
|
17
|
+
/** The chosen mode, `system` included. */
|
|
17
18
|
mode: ThemeMode;
|
|
19
|
+
/** The theme on screen: `light`, `dark`, or a registered custom theme. */
|
|
18
20
|
resolvedTheme: string;
|
|
19
21
|
setMode: (mode: ThemeMode) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Steps light → dark → system. For a control that shows all three modes; a two-state
|
|
24
|
+
* sun/moon button wants `toggleTheme`, because a step into `system` that resolves to
|
|
25
|
+
* the theme already on screen looks like a tap that did nothing.
|
|
26
|
+
*/
|
|
20
27
|
cycleMode: () => void;
|
|
28
|
+
/** Switches to the opposite of the theme on screen: dark from light, light from anything else. */
|
|
29
|
+
toggleTheme: () => void;
|
|
21
30
|
}
|
|
22
31
|
export declare function applyThemeMode(mode: ThemeMode): void;
|
|
23
32
|
export declare function resolveThemeMode(adapter?: ThemeBridgeAdapter, fallback?: ThemeMode): Promise<ThemeMode>;
|
package/dist/lib/theme-bridge.js
CHANGED
|
@@ -101,12 +101,14 @@ export function useThemeBridge({ initialMode = 'system', adapter, } = {}) {
|
|
|
101
101
|
dispatch({ type: 'select', mode });
|
|
102
102
|
}, []);
|
|
103
103
|
const cycleMode = useCallback(() => dispatch({ type: 'cycle' }), []);
|
|
104
|
+
const toggleTheme = useCallback(() => dispatch({ type: 'select', mode: theme === 'light' ? 'dark' : 'light' }), [theme]);
|
|
104
105
|
return useMemo(() => ({
|
|
105
106
|
mode: state.mode,
|
|
106
107
|
resolvedTheme: theme,
|
|
107
108
|
setMode,
|
|
108
109
|
cycleMode,
|
|
109
|
-
|
|
110
|
+
toggleTheme,
|
|
111
|
+
}), [cycleMode, setMode, state.mode, theme, toggleTheme]);
|
|
110
112
|
}
|
|
111
113
|
function ControlledThemeBridge({ mode, adapter, }) {
|
|
112
114
|
useEffect(() => {
|