@thewhileloop/whileui 3.0.0 → 4.1.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 +244 -80
- package/dist/blocks/chat/chat-conversation.d.ts +8 -0
- package/dist/blocks/chat/chat-conversation.js +36 -0
- package/dist/blocks/chat/chat.js +5 -11
- package/dist/blocks/chat/index.d.ts +1 -0
- package/dist/blocks/chat/index.js +1 -0
- 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.js +2 -1
- package/dist/code.d.ts +1 -0
- package/dist/code.js +1 -0
- package/dist/components/card/card.d.ts +1 -1
- package/dist/components/card/card.js +4 -25
- package/dist/components/code-block/code-block.d.ts +12 -0
- package/dist/components/code-block/code-block.js +13 -0
- package/dist/components/code-block/index.d.ts +1 -0
- package/dist/components/code-block/index.js +1 -0
- 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 +8 -0
- package/dist/components/form-field/form-field.js +3 -0
- package/dist/components/form-field/index.d.ts +1 -1
- package/dist/components/form-field/index.js +1 -1
- package/dist/components/input/input.d.ts +7 -0
- package/dist/components/input/input.js +26 -9
- package/dist/components/markdown/index.d.ts +1 -0
- package/dist/components/markdown/index.js +1 -0
- package/dist/components/markdown/markdown.d.ts +14 -0
- package/dist/components/markdown/markdown.js +94 -0
- package/dist/components/numeric-input/numeric-input.d.ts +1 -0
- package/dist/components/numeric-input/numeric-input.js +19 -6
- package/dist/components/select/select.d.ts +2 -1
- package/dist/components/select/select.js +18 -15
- package/dist/components/surface/index.d.ts +1 -0
- package/dist/components/surface/index.js +1 -0
- package/dist/components/surface/surface.d.ts +14 -0
- package/dist/components/surface/surface.js +35 -0
- package/dist/components/text/text.d.ts +42 -42
- package/dist/components/text/text.js +39 -17
- package/dist/components/textarea/textarea.d.ts +1 -0
- package/dist/components/textarea/textarea.js +8 -4
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- 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/theme-presets.d.ts +26 -0
- package/dist/lib/theme-presets.js +28 -0
- package/dist/lib/web-autofill.d.ts +9 -0
- package/dist/lib/web-autofill.js +9 -0
- package/dist/markdown.d.ts +1 -0
- package/dist/markdown.js +1 -0
- package/package.json +16 -2
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ bun add @thewhileloop/whileui
|
|
|
18
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
|
+
# Only if you import @thewhileloop/whileui/markdown:
|
|
22
|
+
bun add markdown-it
|
|
21
23
|
```
|
|
22
24
|
|
|
23
25
|
Use `npm install` instead of `bun add` if you prefer npm.
|
|
@@ -27,16 +29,20 @@ Use `npm install` instead of `bun add` if you prefer npm.
|
|
|
27
29
|
1. **metro.config.js** (wrap with withUniwindConfig):
|
|
28
30
|
|
|
29
31
|
```js
|
|
32
|
+
const { getDefaultConfig } = require('expo/metro-config');
|
|
30
33
|
const { withUniwindConfig } = require('uniwind/metro');
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
/** @type {import('expo/metro-config').MetroConfig} */
|
|
36
|
+
const config = getDefaultConfig(__dirname);
|
|
37
|
+
|
|
38
|
+
module.exports = withUniwindConfig(config, {
|
|
33
39
|
cssEntryFile: './global.css',
|
|
34
|
-
})({
|
|
35
|
-
// your metro config
|
|
36
40
|
});
|
|
37
41
|
```
|
|
38
42
|
|
|
39
|
-
> `withUniwindConfig`
|
|
43
|
+
> `withUniwindConfig(config, options)` takes the config first and returns the wrapped
|
|
44
|
+
> config — it is not curried. It must wrap the outermost config object, and
|
|
45
|
+
> `cssEntryFile` must be a relative path string.
|
|
40
46
|
|
|
41
47
|
2. **global.css** at app root:
|
|
42
48
|
|
|
@@ -44,6 +50,11 @@ module.exports = withUniwindConfig({
|
|
|
44
50
|
@import 'tailwindcss';
|
|
45
51
|
@import 'uniwind';
|
|
46
52
|
|
|
53
|
+
/* Required when WhileUI is installed from npm. Tailwind does not scan
|
|
54
|
+
node_modules, so without this its class names are never detected and every
|
|
55
|
+
component renders unstyled. */
|
|
56
|
+
@source './node_modules/@thewhileloop/whileui/dist';
|
|
57
|
+
|
|
47
58
|
/* WhileUI Noir theme - copy this or create your own */
|
|
48
59
|
@layer theme {
|
|
49
60
|
:root {
|
|
@@ -216,20 +227,38 @@ function MyScreen() {
|
|
|
216
227
|
- **PortalHost:** Add `<PortalHost />` at app root for Select, Popover, Tooltip, HoverCard.
|
|
217
228
|
- **Motion:** Wrap the root in `<MotionProvider>`; use `useMotionPolicy()` for semantic
|
|
218
229
|
durations, the responsive spring, and reduced-motion behavior.
|
|
219
|
-
- **Uniwind:** `withUniwindConfig`
|
|
230
|
+
- **Uniwind:** `withUniwindConfig(config, options)` wraps metro config — config first, not
|
|
231
|
+
curried. `global.css` at app root, imported in `App.tsx`. Installing from npm also needs
|
|
232
|
+
`@source './node_modules/@thewhileloop/whileui/dist';` or every component renders unstyled.
|
|
220
233
|
- **Reference:** Block props in `packages/ui/src/blocks` (core) and `apps/showcase/templates/` (auth, profile); flow patterns in README "Flow Patterns" section.
|
|
221
234
|
|
|
235
|
+
### Composition grammar
|
|
236
|
+
|
|
237
|
+
- Start with one page canvas and flat sections. Add a `Surface` only when grouping or
|
|
238
|
+
elevation communicates something; do not wrap every section in a card.
|
|
239
|
+
- Use `level="base"` for grouped content, `raised` for selectable or floating regions,
|
|
240
|
+
and `overlay` for content above the page. Avoid nesting more than one surfaced level.
|
|
241
|
+
- Express hierarchy through `Text` variants before custom font-size classes. Keep one
|
|
242
|
+
primary action per region and constrain long-form content with `Container`.
|
|
243
|
+
- Forms use `FormField`; its disabled and invalid state flows into `Input`, `Textarea`,
|
|
244
|
+
`NumericInput`, and `Select`. Pass a control prop only to override that inherited state.
|
|
245
|
+
- Chat mechanics belong to `ChatConversation` + `SmartInput`. Message payloads, tools,
|
|
246
|
+
reasoning, approvals, sources, and action rows are application-owned compositions.
|
|
247
|
+
- Import Markdown and code rendering from their optional subpaths. Do not make the root
|
|
248
|
+
package pay for content rendering when the screen does not use it.
|
|
249
|
+
|
|
222
250
|
## Components
|
|
223
251
|
|
|
224
252
|
### Primitives
|
|
225
253
|
|
|
226
|
-
| Component | Notes
|
|
227
|
-
| ------------- |
|
|
228
|
-
| **Text** | Themed text with variant support
|
|
229
|
-
| **
|
|
230
|
-
| **
|
|
231
|
-
| **
|
|
232
|
-
| **
|
|
254
|
+
| Component | Notes |
|
|
255
|
+
| ------------- | ------------------------------------------------ |
|
|
256
|
+
| **Text** | Themed text with variant support |
|
|
257
|
+
| **Surface** | Token-backed base, raised, and overlay container |
|
|
258
|
+
| **View** | Themed view wrapper |
|
|
259
|
+
| **Pressable** | Themed pressable wrapper |
|
|
260
|
+
| **Stack** | Vertical flex layout with gap |
|
|
261
|
+
| **Row** | Horizontal flex layout with gap |
|
|
233
262
|
|
|
234
263
|
### Root Providers
|
|
235
264
|
|
|
@@ -259,18 +288,20 @@ function MyScreen() {
|
|
|
259
288
|
|
|
260
289
|
### Display
|
|
261
290
|
|
|
262
|
-
| Component | Variants
|
|
263
|
-
| --------------- |
|
|
264
|
-
| **Card** | padding (none, sm, default, lg), unstyled
|
|
265
|
-
| **
|
|
266
|
-
| **
|
|
267
|
-
| **
|
|
268
|
-
| **
|
|
269
|
-
| **
|
|
270
|
-
| **
|
|
271
|
-
| **
|
|
272
|
-
| **
|
|
273
|
-
| **
|
|
291
|
+
| Component | Variants | Notes |
|
|
292
|
+
| --------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
293
|
+
| **Card** | padding (none, sm, default, lg), unstyled | Compound: Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter |
|
|
294
|
+
| **Surface** | level (base, raised, overlay) | Shared radius, padding, border, shadow, and optional frosted treatment |
|
|
295
|
+
| **Badge** | default, secondary, destructive, outline, success | BadgeText sub-component |
|
|
296
|
+
| **FAB** | default, secondary, destructive, surface; sm/default/lg | Floating action button. `label` required; pass your own icon as children |
|
|
297
|
+
| **Alert** | default, destructive, success, warning | AlertTitle & AlertDescription |
|
|
298
|
+
| **Avatar** | sm, default, lg | AvatarImage + AvatarFallback |
|
|
299
|
+
| **DataRow** | default, compact | DataRow, DataRowLeft/Center/Right, DataRowLabel/Description/Value |
|
|
300
|
+
| **Separator** | horizontal, vertical | Themed divider |
|
|
301
|
+
| **Progress** | sm, default, lg | Value-based progress bar with accessibility |
|
|
302
|
+
| **Spinner** | sm, default, lg | ActivityIndicator wrapper |
|
|
303
|
+
| **Skeleton** | pulse, shimmer | Loading placeholder (pulse = opacity fade, shimmer = sweep) |
|
|
304
|
+
| **AspectRatio** | — | Maintain aspect ratio container |
|
|
274
305
|
|
|
275
306
|
### Layout
|
|
276
307
|
|
|
@@ -352,11 +383,12 @@ Copy from `apps/showcase/templates/auth/`:
|
|
|
352
383
|
|
|
353
384
|
### Chat
|
|
354
385
|
|
|
355
|
-
| Block | Description
|
|
356
|
-
| --------------------- |
|
|
357
|
-
| **Chat** |
|
|
358
|
-
| **
|
|
359
|
-
| **
|
|
386
|
+
| Block | Description |
|
|
387
|
+
| --------------------- | --------------------------------------------------------------- |
|
|
388
|
+
| **Chat** | Opinionated messages, suggestions, and SmartInput composition |
|
|
389
|
+
| **ChatConversation** | Constrained conversation viewport that follows streaming output |
|
|
390
|
+
| **ChatMessageBubble** | Message bubble (user/assistant, big/small text) |
|
|
391
|
+
| **ChatSuggestions** | Suggestion chips when empty |
|
|
360
392
|
|
|
361
393
|
### Profile & Settings (Copy from showcase templates)
|
|
362
394
|
|
|
@@ -404,6 +436,7 @@ Copy from `apps/showcase/templates/profile/`:
|
|
|
404
436
|
| **DatePickerModal** | Bottom sheet modal with calendar, compact trigger |
|
|
405
437
|
| **DatePickerInline** | Inline calendar for forms or dashboards |
|
|
406
438
|
| **DateRangePickerModal** | Range selection modal with period marking |
|
|
439
|
+
| **TimePickerModal** | Hour and minute wheels in the same bottom sheet |
|
|
407
440
|
|
|
408
441
|
**Tokens without a renderer.** `@thewhileloop/whileui/tokens` exports the theme
|
|
409
442
|
and visual contracts, the presets, and `deriveThemeColors(base, scheme)` — give it
|
|
@@ -412,6 +445,60 @@ as the default presets. Safe in a build script (no React, RN or Uniwind
|
|
|
412
445
|
imports); `useThemeColors()` uses it too, so a missing `--color-primary-soft`
|
|
413
446
|
derives from the app's own primary instead of falling back to the default palette.
|
|
414
447
|
|
|
448
|
+
**Building a product preset.** Use `createThemePreset` rather than a `ThemePreset`
|
|
449
|
+
literal — it takes the 25 base colours per scheme and derives the rest:
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
import { createThemePreset, themePresetToCss } from '@thewhileloop/whileui/tokens';
|
|
453
|
+
|
|
454
|
+
const preset = createThemePreset({
|
|
455
|
+
name: 'acme',
|
|
456
|
+
description: 'Acme brand palette.',
|
|
457
|
+
light: { background: '...', foreground: '...', primary: '...' /* 25 base keys */ },
|
|
458
|
+
dark: { background: '...', foreground: '...', primary: '...' },
|
|
459
|
+
visual: { radiusLg: 12 }, // optional, partial
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
writeFileSync('theme.generated.css', themePresetToCss(preset));
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
The literal form asks for all 52 colours per scheme, including every active variant,
|
|
466
|
+
soft variant, overlay, surface, interaction state and shadow. Those are exactly what
|
|
467
|
+
`deriveThemeColors` computes, so hand-writing them produces a second derivation that
|
|
468
|
+
drifts from the library's. Override a derived value by assigning to the result.
|
|
469
|
+
|
|
470
|
+
`WHILEUI_PRESET_SOFT` is the airy default. `WHILEUI_PRESET_ATELIER` is the stronger
|
|
471
|
+
product preset: tighter geometry, opaque surfaces, firmer borders, real shadows, and no
|
|
472
|
+
blur. Use it as the acceptance check that a screen still has enough visual weight when
|
|
473
|
+
the soft treatment is removed.
|
|
474
|
+
|
|
475
|
+
### FAB
|
|
476
|
+
|
|
477
|
+
One primary action, raised above the content. It owns no icon dependency — pass the
|
|
478
|
+
glyph as children and colour it from `useIconColors()`, because icon libraries need a
|
|
479
|
+
concrete colour rather than a class.
|
|
480
|
+
|
|
481
|
+
```tsx
|
|
482
|
+
import { FAB, useIconColors } from '@thewhileloop/whileui';
|
|
483
|
+
import { Feather } from '@expo/vector-icons';
|
|
484
|
+
|
|
485
|
+
const icons = useIconColors();
|
|
486
|
+
|
|
487
|
+
<FAB label="New group" onPress={create}>
|
|
488
|
+
<Feather name="plus" size={24} color={icons.primaryForeground} />
|
|
489
|
+
</FAB>;
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
| Prop | Type | Default | Notes |
|
|
493
|
+
| ----------- | ------------------------------------------------ | ---------- | --------------------------------------- |
|
|
494
|
+
| `label` | `string` | — | Required. The accessible name |
|
|
495
|
+
| `variant` | `default \| secondary \| destructive \| surface` | `default` | |
|
|
496
|
+
| `size` | `sm \| default \| lg` | `default` | 48 / 56 / 64 — all clear touch minimums |
|
|
497
|
+
| `placement` | `floating \| floating-left \| inline` | `floating` | `inline` lets the caller position it |
|
|
498
|
+
|
|
499
|
+
`FABGroup` stacks secondary actions above one. Sizes start at 48dp, so a FAB never
|
|
500
|
+
needs `hitSlop`.
|
|
501
|
+
|
|
415
502
|
## Layout Primitives (Stack, Row)
|
|
416
503
|
|
|
417
504
|
Use `Stack` for vertical layouts, `Row` for horizontal layouts. Both support `gap`, `align`, and `justify` variants.
|
|
@@ -496,7 +583,7 @@ import { AppShell, Header, BottomNav, ScrollView } from '@thewhileloop/whileui';
|
|
|
496
583
|
| Forms | FormField + Input/NumericInput/LabeledField + DatePickerModal + FormModalScreen |
|
|
497
584
|
| Settings | ProfileHeader + SettingsSection + SettingsItem (+ FormModalScreen for edits) |
|
|
498
585
|
| E-commerce | ProductCard list → CheckoutSummary + ActionBar |
|
|
499
|
-
| Chat | Chat
|
|
586
|
+
| Chat | Chat, or ChatConversation + SmartInput for custom message/tool/source compositions |
|
|
500
587
|
| App shell | AppShell + Header + BottomNav + content |
|
|
501
588
|
| Loading | AppShell (`loading`) + PageSkeleton/ScreenSkeleton (keep header mounted) |
|
|
502
589
|
|
|
@@ -592,7 +679,7 @@ The WhileUI token contract is strict for cross-app reuse. Define these in **ever
|
|
|
592
679
|
- 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
680
|
- Optional status tokens: `success`, `success-foreground`, `warning`, `warning-foreground`, `info`, `info-foreground`
|
|
594
681
|
- 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
|
|
682
|
+
- 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
683
|
- Optional scale tokens: spacing (`--spacing`, `--spacing-*`), typography (`--text-*`, `--leading-*`, `--tracking-*`), radius (`--radius-*`), elevation (`--shadow-*`)
|
|
597
684
|
|
|
598
685
|
Minimal contract example:
|
|
@@ -710,53 +797,25 @@ const iconColors = useIconColors();
|
|
|
710
797
|
|
|
711
798
|
Input, Textarea, NumericInput, SmartInput, Spinner, and LoadingScreen default to theme colors when you omit `placeholderTextColor` or `spinnerColor`.
|
|
712
799
|
|
|
713
|
-
### Interaction
|
|
800
|
+
### Interaction and motion
|
|
714
801
|
|
|
715
|
-
|
|
802
|
+
Press opacity, disabled opacity, motion durations, the responsive spring, and the drawer
|
|
803
|
+
open and close durations are package constants on `INTERACTION_TOKENS`. They are not CSS
|
|
804
|
+
variables and they do not change with a preset. Delete any `--ui-press-*`,
|
|
805
|
+
`--ui-disabled-*`, `--ui-inactive-opacity`, `--ui-motion-*`, or `--ui-drawer-open-duration`
|
|
806
|
+
/ `--ui-drawer-close-duration` declarations from a theme: nothing reads them.
|
|
716
807
|
|
|
717
|
-
|
|
718
|
-
@theme {
|
|
719
|
-
--ui-press-opacity: 0.72;
|
|
720
|
-
--ui-press-opacity-strong: 0.9;
|
|
721
|
-
--ui-disabled-opacity: 0.5;
|
|
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
|
-
```
|
|
747
|
-
|
|
748
|
-
For custom components, use:
|
|
808
|
+
For a custom control, read that table:
|
|
749
809
|
|
|
750
810
|
```tsx
|
|
751
811
|
import { useInteractionTokens, withInteractivePressableStyle } from '@thewhileloop/whileui';
|
|
752
812
|
```
|
|
753
813
|
|
|
754
|
-
Wrap the application in `MotionProvider` to coordinate system reduced motion, explicit
|
|
755
|
-
preview
|
|
756
|
-
|
|
757
|
-
system setting
|
|
758
|
-
|
|
759
|
-
semantic durations to zero.
|
|
814
|
+
Wrap the application in `MotionProvider` to coordinate system reduced motion, an explicit
|
|
815
|
+
preview mode, and deterministic animation-free tests. The provider is optional. Without
|
|
816
|
+
it `useMotionPolicy` reports the durations and spring from `INTERACTION_TOKENS`, and
|
|
817
|
+
Reanimated follows the system setting. `reduced` preserves non-spatial feedback while asking Reanimated to skip motion;
|
|
818
|
+
`disabled` also resolves semantic durations to zero.
|
|
760
819
|
|
|
761
820
|
```tsx
|
|
762
821
|
import { MotionProvider, useMotionPolicy } from '@thewhileloop/whileui';
|
|
@@ -966,6 +1025,17 @@ import { Button, ButtonText, ButtonIcon } from '@thewhileloop/whileui';
|
|
|
966
1025
|
| loading | `boolean` | `false` | Show a spinner, expose busy state, and block input |
|
|
967
1026
|
| className | `string` | — | Additional Tailwind classes |
|
|
968
1027
|
|
|
1028
|
+
## Text
|
|
1029
|
+
|
|
1030
|
+
`Text` maps semantic roles to the active visual typography tokens; custom font-size
|
|
1031
|
+
classes are unnecessary for normal hierarchy. `heading` also sets
|
|
1032
|
+
`accessibilityRole="header"` unless the caller supplies another role.
|
|
1033
|
+
|
|
1034
|
+
| Prop | Type | Default |
|
|
1035
|
+
| ------- | --------------------------------------------------------------------------------------------- | ----------- |
|
|
1036
|
+
| variant | `'default' \| 'heading' \| 'title' \| 'subtitle' \| 'body' \| 'caption' \| 'label' \| 'code'` | `'default'` |
|
|
1037
|
+
| size | `'xs' \| 'sm' \| 'base' \| 'lg' \| 'xl' \| '2xl' \| '3xl'` | by variant |
|
|
1038
|
+
|
|
969
1039
|
## Input
|
|
970
1040
|
|
|
971
1041
|
```tsx
|
|
@@ -974,12 +1044,15 @@ import { Input } from '@thewhileloop/whileui';
|
|
|
974
1044
|
<Input placeholder="Email" variant="default" value={value} onChangeText={setValue} />;
|
|
975
1045
|
```
|
|
976
1046
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
|
981
|
-
|
|
|
982
|
-
|
|
|
1047
|
+
On web, filled Input, Textarea, NumericInput, and SmartInput surfaces preserve their
|
|
1048
|
+
themed background, foreground, and caret when the browser autofills them.
|
|
1049
|
+
|
|
1050
|
+
| Prop | Type | Default | Description |
|
|
1051
|
+
| -------------------- | --------------------------------- | ----------- | --------------------------------------------------------------------- |
|
|
1052
|
+
| variant | `'default' \| 'error' \| 'plain'` | `'default'` | Input style; `plain` drops border and fill for a large display figure |
|
|
1053
|
+
| placeholder | `string` | — | Placeholder text |
|
|
1054
|
+
| placeholderTextColor | `string` | — | Hex for placeholder; defaults to theme `mutedForeground` |
|
|
1055
|
+
| editable | `boolean` | `true` | Whether input is editable |
|
|
983
1056
|
|
|
984
1057
|
## NumericInput
|
|
985
1058
|
|
|
@@ -1054,6 +1127,30 @@ import {
|
|
|
1054
1127
|
</FormField>;
|
|
1055
1128
|
```
|
|
1056
1129
|
|
|
1130
|
+
`invalid` and `disabled` are inherited by `Input`, `Textarea`, `NumericInput`, and
|
|
1131
|
+
`Select`. An explicit control prop wins. `useFormFieldState()` exposes the same state for
|
|
1132
|
+
custom controls; labels, hints, messages, density, and required indicators remain part
|
|
1133
|
+
of the compound field rather than each input.
|
|
1134
|
+
|
|
1135
|
+
## Surface
|
|
1136
|
+
|
|
1137
|
+
```tsx
|
|
1138
|
+
import { Surface } from '@thewhileloop/whileui';
|
|
1139
|
+
|
|
1140
|
+
<Surface level="raised" padding="default">
|
|
1141
|
+
{/* grouped content */}
|
|
1142
|
+
</Surface>;
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
| Prop | Type | Default | Description |
|
|
1146
|
+
| -------- | ------------------------------------- | ----------- | ----------------------------------- |
|
|
1147
|
+
| level | `'base' \| 'raised' \| 'overlay'` | `'base'` | Semantic background and depth tier |
|
|
1148
|
+
| padding | `'none' \| 'sm' \| 'default' \| 'lg'` | `'default'` | Token-backed interior spacing |
|
|
1149
|
+
| radius | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'xl'` | Token-backed corner radius |
|
|
1150
|
+
| bordered | `boolean` | `true` | Draw the theme hairline border |
|
|
1151
|
+
| shadow | `'none' \| 'sm' \| 'md' \| 'lg'` | by level | Override the level's depth |
|
|
1152
|
+
| frosted | `boolean` | `false` | Enable the optional frosted overlay |
|
|
1153
|
+
|
|
1057
1154
|
## LabeledField
|
|
1058
1155
|
|
|
1059
1156
|
```tsx
|
|
@@ -1346,6 +1443,10 @@ import {
|
|
|
1346
1443
|
</Select>;
|
|
1347
1444
|
```
|
|
1348
1445
|
|
|
1446
|
+
Focused and pressed options use the subtle interaction-state tokens; the primary token
|
|
1447
|
+
marks the selected indicator without replacing the option's foreground colour. Long
|
|
1448
|
+
selected values stay on one line and truncate without displacing the trigger chevron.
|
|
1449
|
+
|
|
1349
1450
|
`SelectContent` supports optional frosted props:
|
|
1350
1451
|
|
|
1351
1452
|
| Prop | Type | Default |
|
|
@@ -1704,9 +1805,34 @@ import { SmartInput, Button, ButtonText } from '@thewhileloop/whileui';
|
|
|
1704
1805
|
/>;
|
|
1705
1806
|
```
|
|
1706
1807
|
|
|
1707
|
-
## Chat
|
|
1808
|
+
## ChatConversation / Chat
|
|
1809
|
+
|
|
1810
|
+
`ChatConversation` is the low-level viewport for custom conversation screens. Its content stays constrained on wide windows, follows new or streaming output while the reader is near the bottom, and stops following after the reader scrolls upward. Compose it with `SmartInput`; the application owns message data, streaming, tools, approvals, and sources.
|
|
1811
|
+
|
|
1812
|
+
It is a `ScrollView` for a bounded active transcript. Paginate or window historical messages; use a virtualized list when the full history can grow without a bound.
|
|
1813
|
+
|
|
1814
|
+
```tsx
|
|
1815
|
+
import { ChatConversation, SmartInput, View } from '@thewhileloop/whileui';
|
|
1816
|
+
|
|
1817
|
+
<View className="flex-1">
|
|
1818
|
+
<ChatConversation contentContainerClassName="gap-4 px-4 py-6">
|
|
1819
|
+
{messages.map((message) => (
|
|
1820
|
+
<ProductMessage key={message.id} message={message} />
|
|
1821
|
+
))}
|
|
1822
|
+
</ChatConversation>
|
|
1823
|
+
<SmartInput
|
|
1824
|
+
value={value}
|
|
1825
|
+
onChangeText={setValue}
|
|
1826
|
+
onSubmitEditing={send}
|
|
1827
|
+
submitBehavior="submit"
|
|
1828
|
+
leftSlot={<AttachButton />}
|
|
1829
|
+
centerSlot={<ModelSelect />}
|
|
1830
|
+
rightSlot={<SendButton />}
|
|
1831
|
+
/>
|
|
1832
|
+
</View>;
|
|
1833
|
+
```
|
|
1708
1834
|
|
|
1709
|
-
|
|
1835
|
+
`Chat` is the opinionated convenience composition: message bubbles, suggestion chips, and `SmartInput` with attach/send slots. It uses semantic tokens; theme via `global.css`. Use `renderMessage` for markdown, code, tool calls, reasoning, or sources, and `loadingIndicator` for streaming feedback.
|
|
1710
1836
|
|
|
1711
1837
|
```tsx
|
|
1712
1838
|
import { Chat, type ChatMessage } from '@thewhileloop/whileui';
|
|
@@ -1743,7 +1869,27 @@ const [value, setValue] = useState('');
|
|
|
1743
1869
|
| `inputSafeArea` | `boolean` | SmartInput safe-area (default `true`) |
|
|
1744
1870
|
| `keyboardVerticalOffset` | `number` | For header offset when keyboard opens |
|
|
1745
1871
|
|
|
1746
|
-
|
|
1872
|
+
`ChatConversation` extends React Native `ScrollViewProps`, forwards its ref, and adds `contentContainerClassName` to style the constrained inner column. Caller `onScroll` and `onContentSizeChange` callbacks are preserved.
|
|
1873
|
+
|
|
1874
|
+
### Optional Markdown and code
|
|
1875
|
+
|
|
1876
|
+
These are explicit subpaths so the root package remains independent of a Markdown
|
|
1877
|
+
parser. Install `markdown-it` only in applications that render Markdown.
|
|
1878
|
+
|
|
1879
|
+
```tsx
|
|
1880
|
+
import { Markdown } from '@thewhileloop/whileui/markdown';
|
|
1881
|
+
import { CodeBlock } from '@thewhileloop/whileui/code';
|
|
1882
|
+
|
|
1883
|
+
<Markdown onLinkPress={openLink}>{assistantText}</Markdown>
|
|
1884
|
+
<CodeBlock code={source} language="TypeScript" actions={<CopyButton />} />
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
`Markdown` supports headings, paragraphs, emphasis, links, lists, quotes, rules, inline
|
|
1888
|
+
code, and fenced code. It never opens links or writes to the clipboard itself:
|
|
1889
|
+
`onLinkPress` and `codeActions` keep those capabilities in the application. `CodeBlock`
|
|
1890
|
+
is dependency-free and accepts an application-owned `actions` slot.
|
|
1891
|
+
|
|
1892
|
+
## DatePickerModal / DatePickerInline / DateRangePickerModal / TimePickerModal
|
|
1747
1893
|
|
|
1748
1894
|
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.
|
|
1749
1895
|
|
|
@@ -1753,6 +1899,8 @@ Date selection blocks using react-native-calendars. Theme-aware via `useCalendar
|
|
|
1753
1899
|
|
|
1754
1900
|
**DateRangePickerModal** — Range selection with period marking. Use `DateRangePickerTrigger` as the trigger content.
|
|
1755
1901
|
|
|
1902
|
+
**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.
|
|
1903
|
+
|
|
1756
1904
|
```tsx
|
|
1757
1905
|
import {
|
|
1758
1906
|
DatePickerModal,
|
|
@@ -1760,6 +1908,8 @@ import {
|
|
|
1760
1908
|
DatePickerInline,
|
|
1761
1909
|
DateRangePickerModal,
|
|
1762
1910
|
DateRangePickerTrigger,
|
|
1911
|
+
TimePickerModal,
|
|
1912
|
+
TimePickerTrigger,
|
|
1763
1913
|
type DateRange,
|
|
1764
1914
|
} from '@thewhileloop/whileui';
|
|
1765
1915
|
|
|
@@ -1789,6 +1939,18 @@ const [range, setRange] = useState<DateRange | null>(null);
|
|
|
1789
1939
|
onOpenChange={setRangeOpen}
|
|
1790
1940
|
trigger={<DateRangePickerTrigger value={range} placeholder="Pick range" />}
|
|
1791
1941
|
/>;
|
|
1942
|
+
|
|
1943
|
+
// Time (modal) — "HH:MM", 24-hour, whatever the wheels show
|
|
1944
|
+
const [time, setTime] = useState<string | null>(null);
|
|
1945
|
+
|
|
1946
|
+
<TimePickerModal
|
|
1947
|
+
value={time}
|
|
1948
|
+
onValueChange={setTime}
|
|
1949
|
+
open={timeOpen}
|
|
1950
|
+
onOpenChange={setTimeOpen}
|
|
1951
|
+
minuteStep={5}
|
|
1952
|
+
trigger={<TimePickerTrigger value={time} placeholder="Pick a time" />}
|
|
1953
|
+
/>;
|
|
1792
1954
|
```
|
|
1793
1955
|
|
|
1794
1956
|
| Prop | Type | Description |
|
|
@@ -1799,6 +1961,8 @@ const [range, setRange] = useState<DateRange | null>(null);
|
|
|
1799
1961
|
| `trigger` | `ReactNode` | Custom trigger (modal variants) |
|
|
1800
1962
|
| `minDate` / `maxDate` | `string` | YYYY-MM-DD bounds |
|
|
1801
1963
|
| `theme` | `CalendarTheme` | Override calendar colors |
|
|
1964
|
+
| `hourCycle` | `'h12' \| 'h23'` | Time picker wheels; defaults to locale |
|
|
1965
|
+
| `minuteStep` | `number` | Minutes between options (default 1) |
|
|
1802
1966
|
| `frosted` | `boolean` | Enable frosted surface for modal panel |
|
|
1803
1967
|
| `blurIntensity` | `number` | Override blur amount |
|
|
1804
1968
|
| `blurTintToken` | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | Tint token for frosted panel |
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ScrollViewInstance, type ScrollViewProps } from 'react-native';
|
|
2
|
+
import type { ForwardRefComponent } from '../../lib/ref-types';
|
|
3
|
+
/** ScrollView viewport for a bounded active transcript; virtualize unbounded history. */
|
|
4
|
+
export interface ChatConversationProps extends ScrollViewProps {
|
|
5
|
+
contentContainerClassName?: string;
|
|
6
|
+
}
|
|
7
|
+
declare const ChatConversation: ForwardRefComponent<ScrollViewInstance, ChatConversationProps>;
|
|
8
|
+
export { ChatConversation };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useCallback, useRef } from 'react';
|
|
3
|
+
import { ScrollView, View, } from 'react-native';
|
|
4
|
+
import { cn } from '../../lib/cn';
|
|
5
|
+
const BOTTOM_THRESHOLD = 48;
|
|
6
|
+
function assignRef(ref, value) {
|
|
7
|
+
if (typeof ref === 'function') {
|
|
8
|
+
ref(value);
|
|
9
|
+
}
|
|
10
|
+
else if (ref) {
|
|
11
|
+
ref.current = value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const ChatConversation = React.forwardRef(({ className, contentContainerClassName, contentContainerStyle, keyboardShouldPersistTaps = 'handled', onContentSizeChange, onScroll, scrollEventThrottle = 64, children, ...props }, forwardedRef) => {
|
|
15
|
+
const scrollRef = useRef(null);
|
|
16
|
+
const followsOutputRef = useRef(true);
|
|
17
|
+
const setRef = useCallback((instance) => {
|
|
18
|
+
scrollRef.current = instance;
|
|
19
|
+
assignRef(forwardedRef, instance);
|
|
20
|
+
}, [forwardedRef]);
|
|
21
|
+
const handleScroll = useCallback((event) => {
|
|
22
|
+
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
|
|
23
|
+
const distanceFromBottom = contentSize.height - layoutMeasurement.height - contentOffset.y;
|
|
24
|
+
followsOutputRef.current = distanceFromBottom <= BOTTOM_THRESHOLD;
|
|
25
|
+
onScroll?.(event);
|
|
26
|
+
}, [onScroll]);
|
|
27
|
+
const handleContentSizeChange = useCallback((width, height) => {
|
|
28
|
+
onContentSizeChange?.(width, height);
|
|
29
|
+
if (followsOutputRef.current) {
|
|
30
|
+
scrollRef.current?.scrollToEnd({ animated: true });
|
|
31
|
+
}
|
|
32
|
+
}, [onContentSizeChange]);
|
|
33
|
+
return (_jsx(ScrollView, { ref: setRef, className: cn('flex-1', className), contentContainerStyle: { flexGrow: 1 }, keyboardShouldPersistTaps: keyboardShouldPersistTaps, onContentSizeChange: handleContentSizeChange, onScroll: handleScroll, scrollEventThrottle: scrollEventThrottle, ...props, children: _jsx(View, { className: cn('w-full max-w-3xl flex-1 self-center', contentContainerClassName), style: contentContainerStyle, children: children }) }));
|
|
34
|
+
});
|
|
35
|
+
ChatConversation.displayName = 'ChatConversation';
|
|
36
|
+
export { ChatConversation };
|
package/dist/blocks/chat/chat.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React
|
|
3
|
-
import { View,
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { View, Pressable, KeyboardAvoidingView, Platform, useWindowDimensions } from 'react-native';
|
|
4
4
|
import { Text } from '../../components/text';
|
|
5
5
|
import { SmartInput } from '../layout/smart-input';
|
|
6
|
+
import { ChatConversation } from './chat-conversation';
|
|
6
7
|
import { cn } from '../../lib/cn';
|
|
7
8
|
import { useInteractionTokens, withInteractivePressableStyle } from '../../lib/interaction-tokens';
|
|
8
9
|
import { useVisualTokens } from '../../lib/visual-tokens';
|
|
@@ -49,23 +50,16 @@ export function ChatSuggestions({ suggestions, onSelect, className }) {
|
|
|
49
50
|
}
|
|
50
51
|
const EMPTY_SUGGESTIONS = [];
|
|
51
52
|
export function Chat({ messages, value, onChangeText, onSend, placeholder = 'Message...', suggestions = EMPTY_SUGGESTIONS, onSuggestionPress, leftSlot, rightSlot, loading = false, header, emptyTitle, emptyDescription, exampleMessage, renderMessage, loadingIndicator, inputSafeArea = true, keyboardVerticalOffset = 0, className, messagesClassName, }) {
|
|
52
|
-
const scrollRef = useRef(null);
|
|
53
53
|
const visual = useVisualTokens();
|
|
54
54
|
const emptyTitleTypography = typographyStyle(visual, 'emphasis');
|
|
55
55
|
const emptyDescriptionTypography = typographyStyle(visual, 'label');
|
|
56
56
|
const isEmpty = messages.length === 0;
|
|
57
57
|
const showSuggestions = isEmpty && suggestions.length > 0;
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
if (!showSuggestions && messages.length > 0) {
|
|
60
|
-
scrollRef.current?.scrollToEnd({ animated: true });
|
|
61
|
-
}
|
|
62
|
-
}, [messages.length, showSuggestions]);
|
|
63
58
|
const defaultRightSlot = rightSlot ?? (_jsx(ChatSendButton, { onPress: onSend, disabled: loading || !value.trim() }));
|
|
64
|
-
return (_jsxs(KeyboardAvoidingView, { behavior: Platform.OS === 'ios' ? 'padding' : undefined, keyboardVerticalOffset: keyboardVerticalOffset, style: { flex: 1 }, className: className, children: [header, _jsx(
|
|
65
|
-
flexGrow: 1,
|
|
59
|
+
return (_jsxs(KeyboardAvoidingView, { behavior: Platform.OS === 'ios' ? 'padding' : undefined, keyboardVerticalOffset: keyboardVerticalOffset, style: { flex: 1 }, className: className, children: [header, _jsx(ChatConversation, { className: cn('flex-1', messagesClassName), contentContainerStyle: {
|
|
66
60
|
paddingBottom: 8,
|
|
67
61
|
justifyContent: showSuggestions ? 'center' : 'flex-end',
|
|
68
|
-
},
|
|
62
|
+
}, children: showSuggestions ? (_jsxs(View, { className: "flex-1 justify-center", children: [exampleMessage && (_jsx(View, { className: "mb-6 px-4", children: renderMessage ? (renderMessage(exampleMessage)) : (_jsx(ChatMessageBubble, { message: exampleMessage })) })), emptyTitle && (_jsx(Text, { className: "mb-1 text-center font-medium text-foreground", style: emptyTitleTypography, children: emptyTitle })), emptyDescription && (_jsx(Text, { className: "mb-4 text-center text-muted-foreground", style: emptyDescriptionTypography, children: emptyDescription })), _jsx(ChatSuggestions, { suggestions: suggestions, onSelect: onSuggestionPress ?? (() => { }) })] })) : (_jsxs(View, { className: "gap-3 px-4 pt-4", children: [messages.map((msg) => renderMessage ? (_jsx(React.Fragment, { children: renderMessage(msg) }, msg.id)) : (_jsx(ChatMessageBubble, { message: msg }, msg.id))), loading && loadingIndicator && _jsx(View, { className: "px-4", children: loadingIndicator })] })) }), _jsx(SmartInput, { value: value, onChangeText: onChangeText, placeholder: placeholder, leftSlot: leftSlot, rightSlot: defaultRightSlot, editable: !loading, safeArea: inputSafeArea })] }));
|
|
69
63
|
}
|
|
70
64
|
function ChatSendButton({ onPress, disabled }) {
|
|
71
65
|
const interaction = useInteractionTokens();
|
|
@@ -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';
|