@delacour/react-native-ui 0.1.0-alpha.3
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 +113 -0
- package/package.json +125 -0
- package/src/components/accordion/AGENTS.md +192 -0
- package/src/components/accordion/accordion-content.tsx +125 -0
- package/src/components/accordion/accordion-description.tsx +17 -0
- package/src/components/accordion/accordion-indicator.tsx +104 -0
- package/src/components/accordion/accordion-item.tsx +78 -0
- package/src/components/accordion/accordion-title.tsx +18 -0
- package/src/components/accordion/accordion-trigger.tsx +148 -0
- package/src/components/accordion/accordion.context.tsx +153 -0
- package/src/components/accordion/accordion.tsx +253 -0
- package/src/components/accordion/accordion.types.ts +11 -0
- package/src/components/accordion/accordion.variants.test.ts +435 -0
- package/src/components/accordion/accordion.variants.ts +358 -0
- package/src/components/accordion/index.ts +45 -0
- package/src/components/badge/AGENTS.md +83 -0
- package/src/components/badge/badge-close-button.tsx +48 -0
- package/src/components/badge/badge-end-content.tsx +15 -0
- package/src/components/badge/badge-label.tsx +24 -0
- package/src/components/badge/badge-start-content.tsx +16 -0
- package/src/components/badge/badge.context.tsx +64 -0
- package/src/components/badge/badge.tsx +192 -0
- package/src/components/badge/badge.types.ts +10 -0
- package/src/components/badge/badge.variants.test.ts +269 -0
- package/src/components/badge/badge.variants.ts +238 -0
- package/src/components/badge/index.ts +17 -0
- package/src/components/bottom-sheet/AGENTS.md +232 -0
- package/src/components/bottom-sheet/bottom-sheet-background.tsx +35 -0
- package/src/components/bottom-sheet/bottom-sheet-close.tsx +60 -0
- package/src/components/bottom-sheet/bottom-sheet-container.tsx +245 -0
- package/src/components/bottom-sheet/bottom-sheet-content.tsx +66 -0
- package/src/components/bottom-sheet/bottom-sheet-description.tsx +29 -0
- package/src/components/bottom-sheet/bottom-sheet-footer.tsx +148 -0
- package/src/components/bottom-sheet/bottom-sheet-handle.tsx +47 -0
- package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +180 -0
- package/src/components/bottom-sheet/bottom-sheet-portal.tsx +72 -0
- package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +95 -0
- package/src/components/bottom-sheet/bottom-sheet-title.tsx +24 -0
- package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +73 -0
- package/src/components/bottom-sheet/bottom-sheet.context.tsx +135 -0
- package/src/components/bottom-sheet/bottom-sheet.tsx +125 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +276 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +199 -0
- package/src/components/bottom-sheet/index.ts +40 -0
- package/src/components/bottom-sheet/use-bottom-sheet-input.ts +125 -0
- package/src/components/button/AGENTS.md +217 -0
- package/src/components/button/button-end-content.tsx +15 -0
- package/src/components/button/button-group-separator.tsx +37 -0
- package/src/components/button/button-group-text.tsx +101 -0
- package/src/components/button/button-group.tsx +158 -0
- package/src/components/button/button-label.tsx +24 -0
- package/src/components/button/button-start-content.tsx +15 -0
- package/src/components/button/button.context.tsx +187 -0
- package/src/components/button/button.tsx +273 -0
- package/src/components/button/button.types.ts +10 -0
- package/src/components/button/button.variants.test.ts +811 -0
- package/src/components/button/button.variants.ts +480 -0
- package/src/components/button/index.ts +50 -0
- package/src/components/chart/AGENTS.md +210 -0
- package/src/components/chart/chart-area.tsx +93 -0
- package/src/components/chart/chart-bar.tsx +103 -0
- package/src/components/chart/chart-bars.tsx +75 -0
- package/src/components/chart/chart-candlestick.tsx +60 -0
- package/src/components/chart/chart-grid.tsx +33 -0
- package/src/components/chart/chart-legend.tsx +41 -0
- package/src/components/chart/chart-line.tsx +50 -0
- package/src/components/chart/chart-scatter.tsx +35 -0
- package/src/components/chart/chart-tooltip-dot.tsx +70 -0
- package/src/components/chart/chart-tooltip-x.tsx +121 -0
- package/src/components/chart/chart-tooltip-y.tsx +55 -0
- package/src/components/chart/chart-tooltip.tsx +133 -0
- package/src/components/chart/chart-x-axis.tsx +27 -0
- package/src/components/chart/chart-y-axis.tsx +21 -0
- package/src/components/chart/chart.context.tsx +127 -0
- package/src/components/chart/chart.tsx +448 -0
- package/src/components/chart/chart.types.ts +108 -0
- package/src/components/chart/chart.variants.test.ts +893 -0
- package/src/components/chart/chart.variants.ts +726 -0
- package/src/components/chart/index.ts +103 -0
- package/src/components/chart/pie-chart-center.tsx +40 -0
- package/src/components/chart/pie-chart-label.tsx +56 -0
- package/src/components/chart/pie-chart-slice.tsx +45 -0
- package/src/components/chart/pie-chart-tooltip.tsx +85 -0
- package/src/components/chart/pie-chart.context.tsx +77 -0
- package/src/components/chart/pie-chart.tsx +247 -0
- package/src/components/chart/use-chart-font.ts +25 -0
- package/src/components/chart/use-chart-palette.ts +38 -0
- package/src/components/checkbox/AGENTS.md +185 -0
- package/src/components/checkbox/checkbox-box.tsx +152 -0
- package/src/components/checkbox/checkbox-group.tsx +107 -0
- package/src/components/checkbox/checkbox-label.tsx +34 -0
- package/src/components/checkbox/checkbox.context.tsx +136 -0
- package/src/components/checkbox/checkbox.tsx +258 -0
- package/src/components/checkbox/checkbox.types.ts +14 -0
- package/src/components/checkbox/checkbox.variants.test.ts +634 -0
- package/src/components/checkbox/checkbox.variants.ts +484 -0
- package/src/components/checkbox/index.ts +42 -0
- package/src/components/field/AGENTS.md +100 -0
- package/src/components/field/field-content.tsx +24 -0
- package/src/components/field/field-description.tsx +29 -0
- package/src/components/field/field-error.tsx +61 -0
- package/src/components/field/field-group.tsx +22 -0
- package/src/components/field/field-label.tsx +33 -0
- package/src/components/field/field-legend.tsx +34 -0
- package/src/components/field/field-separator.tsx +81 -0
- package/src/components/field/field-set.tsx +22 -0
- package/src/components/field/field.context.tsx +78 -0
- package/src/components/field/field.tsx +144 -0
- package/src/components/field/field.types.ts +15 -0
- package/src/components/field/field.variants.test.ts +216 -0
- package/src/components/field/field.variants.ts +136 -0
- package/src/components/field/index.ts +21 -0
- package/src/components/icon/AGENTS.md +104 -0
- package/src/components/icon/icon.context.tsx +29 -0
- package/src/components/icon/icon.tsx +92 -0
- package/src/components/icon/icon.variants.test.ts +111 -0
- package/src/components/icon/icon.variants.ts +79 -0
- package/src/components/icon/index.ts +11 -0
- package/src/components/input/AGENTS.md +126 -0
- package/src/components/input/index.ts +27 -0
- package/src/components/input/input-group-decorator.tsx +84 -0
- package/src/components/input/input-group-prefix.tsx +16 -0
- package/src/components/input/input-group-suffix.tsx +15 -0
- package/src/components/input/input-group.tsx +140 -0
- package/src/components/input/input.context.tsx +87 -0
- package/src/components/input/input.tsx +184 -0
- package/src/components/input/input.types.ts +10 -0
- package/src/components/input/input.variants.test.ts +369 -0
- package/src/components/input/input.variants.ts +329 -0
- package/src/components/list-group/AGENTS.md +67 -0
- package/src/components/list-group/index.ts +20 -0
- package/src/components/list-group/list-group-item-content.tsx +10 -0
- package/src/components/list-group/list-group-item-description.tsx +12 -0
- package/src/components/list-group/list-group-item-prefix.tsx +30 -0
- package/src/components/list-group/list-group-item-suffix.tsx +55 -0
- package/src/components/list-group/list-group-item-title.tsx +17 -0
- package/src/components/list-group/list-group-item.tsx +87 -0
- package/src/components/list-group/list-group.context.tsx +65 -0
- package/src/components/list-group/list-group.tsx +116 -0
- package/src/components/list-group/list-group.types.ts +14 -0
- package/src/components/list-group/list-group.variants.test.ts +223 -0
- package/src/components/list-group/list-group.variants.ts +107 -0
- package/src/components/pressable/AGENTS.md +64 -0
- package/src/components/pressable/index.ts +9 -0
- package/src/components/pressable/pressable.tsx +256 -0
- package/src/components/pressable/pressable.variants.test.ts +99 -0
- package/src/components/pressable/pressable.variants.ts +63 -0
- package/src/components/provider/AGENTS.md +90 -0
- package/src/components/provider/index.ts +1 -0
- package/src/components/provider/provider.tsx +105 -0
- package/src/components/radio/AGENTS.md +247 -0
- package/src/components/radio/index.ts +35 -0
- package/src/components/radio/radio-group.tsx +126 -0
- package/src/components/radio/radio-indicator.tsx +91 -0
- package/src/components/radio/radio-label.tsx +39 -0
- package/src/components/radio/radio.context.tsx +132 -0
- package/src/components/radio/radio.tsx +215 -0
- package/src/components/radio/radio.variants.test.ts +581 -0
- package/src/components/radio/radio.variants.ts +272 -0
- package/src/components/screen/AGENTS.md +287 -0
- package/src/components/screen/index.ts +85 -0
- package/src/components/screen/screen-chat-list.tsx +485 -0
- package/src/components/screen/screen-content.tsx +69 -0
- package/src/components/screen/screen-debug.ts +43 -0
- package/src/components/screen/screen-error.tsx +61 -0
- package/src/components/screen/screen-flat-list.tsx +88 -0
- package/src/components/screen/screen-footer-background.tsx +60 -0
- package/src/components/screen/screen-footer.tsx +170 -0
- package/src/components/screen/screen-header.tsx +36 -0
- package/src/components/screen/screen-legend-list.tsx +105 -0
- package/src/components/screen/screen-list-component.tsx +23 -0
- package/src/components/screen/screen-loading.tsx +50 -0
- package/src/components/screen/screen-navbar-back-button.tsx +79 -0
- package/src/components/screen/screen-navbar-background.tsx +45 -0
- package/src/components/screen/screen-navbar-subtitle.tsx +26 -0
- package/src/components/screen/screen-navbar-title.tsx +31 -0
- package/src/components/screen/screen-navbar.tsx +154 -0
- package/src/components/screen/screen-root.tsx +38 -0
- package/src/components/screen/screen-scroll-area.tsx +116 -0
- package/src/components/screen/screen-scroll-shadow.tsx +186 -0
- package/src/components/screen/screen-section-list.tsx +81 -0
- package/src/components/screen/screen-view.tsx +59 -0
- package/src/components/screen/screen.context.tsx +167 -0
- package/src/components/screen/screen.tsx +114 -0
- package/src/components/screen/screen.types.ts +66 -0
- package/src/components/screen/screen.variants.test.ts +602 -0
- package/src/components/screen/screen.variants.ts +475 -0
- package/src/components/screen/use-screen-scroll-insets.ts +218 -0
- package/src/components/separator/AGENTS.md +30 -0
- package/src/components/separator/index.ts +8 -0
- package/src/components/separator/separator.tsx +85 -0
- package/src/components/slider/AGENTS.md +274 -0
- package/src/components/slider/index.ts +48 -0
- package/src/components/slider/slider-fill.tsx +68 -0
- package/src/components/slider/slider-output.tsx +55 -0
- package/src/components/slider/slider-thumb.tsx +161 -0
- package/src/components/slider/slider-track.tsx +233 -0
- package/src/components/slider/slider.context.tsx +118 -0
- package/src/components/slider/slider.tsx +321 -0
- package/src/components/slider/slider.types.ts +26 -0
- package/src/components/slider/slider.variants.test.ts +804 -0
- package/src/components/slider/slider.variants.ts +607 -0
- package/src/components/spinner/AGENTS.md +73 -0
- package/src/components/spinner/index.ts +17 -0
- package/src/components/spinner/spinner-arc.tsx +90 -0
- package/src/components/spinner/spinner-content.tsx +63 -0
- package/src/components/spinner/spinner.context.tsx +52 -0
- package/src/components/spinner/spinner.tsx +128 -0
- package/src/components/spinner/spinner.variants.test.ts +273 -0
- package/src/components/spinner/spinner.variants.ts +187 -0
- package/src/components/switch/AGENTS.md +213 -0
- package/src/components/switch/index.ts +42 -0
- package/src/components/switch/switch-content.tsx +111 -0
- package/src/components/switch/switch-end-content.tsx +18 -0
- package/src/components/switch/switch-start-content.tsx +20 -0
- package/src/components/switch/switch-thumb.tsx +102 -0
- package/src/components/switch/switch.context.tsx +79 -0
- package/src/components/switch/switch.tsx +423 -0
- package/src/components/switch/switch.types.ts +14 -0
- package/src/components/switch/switch.variants.test.ts +570 -0
- package/src/components/switch/switch.variants.ts +511 -0
- package/src/components/tabs/AGENTS.md +286 -0
- package/src/components/tabs/index.ts +69 -0
- package/src/components/tabs/tabs-content.tsx +65 -0
- package/src/components/tabs/tabs-indicator.tsx +97 -0
- package/src/components/tabs/tabs-label.tsx +62 -0
- package/src/components/tabs/tabs-list.tsx +139 -0
- package/src/components/tabs/tabs-pager.tsx +59 -0
- package/src/components/tabs/tabs-scroll-view.tsx +171 -0
- package/src/components/tabs/tabs-separator.tsx +73 -0
- package/src/components/tabs/tabs-trigger.tsx +210 -0
- package/src/components/tabs/tabs.context.tsx +294 -0
- package/src/components/tabs/tabs.tsx +435 -0
- package/src/components/tabs/tabs.types.ts +13 -0
- package/src/components/tabs/tabs.variants.test.ts +1020 -0
- package/src/components/tabs/tabs.variants.ts +672 -0
- package/src/components/text/AGENTS.md +95 -0
- package/src/components/text/index.ts +25 -0
- package/src/components/text/text.context.tsx +60 -0
- package/src/components/text/text.tsx +251 -0
- package/src/components/text/text.variants.test.ts +422 -0
- package/src/components/text/text.variants.ts +282 -0
- package/src/display-name.test.ts +145 -0
- package/src/docs.test.ts +98 -0
- package/src/expo/navigation-theme.tsx +58 -0
- package/src/hooks/use-controllable-state.ts +45 -0
- package/src/hooks/use-keyboard-state-sync.tsx +147 -0
- package/src/hooks/use-navigation-theme.ts +78 -0
- package/src/hooks/use-theme-color.ts +44 -0
- package/src/icons/central.ts +1 -0
- package/src/lib/cn.test.ts +136 -0
- package/src/lib/cn.ts +29 -0
- package/src/lib/color.test.ts +80 -0
- package/src/lib/color.ts +79 -0
- package/src/lib/compose-refs.test.ts +64 -0
- package/src/lib/compose-refs.ts +37 -0
- package/src/lib/keyboard-animation.test.ts +33 -0
- package/src/lib/keyboard-animation.ts +28 -0
- package/src/lib/merge-props.test.ts +82 -0
- package/src/lib/merge-props.ts +47 -0
- package/src/lib/navigation-theme.test.ts +49 -0
- package/src/lib/navigation-theme.ts +53 -0
- package/src/lib/slot.tsx +45 -0
- package/src/lib/tv.ts +20 -0
- package/src/styles/base.css +2 -0
- package/src/styles/geometry.test.ts +117 -0
- package/src/styles/index.css +3 -0
- package/src/styles/theme-tokens.test.ts +252 -0
- package/src/styles/theme.css +447 -0
- package/src/styles/tokens.css +132 -0
- package/src/styles/tokens.test.ts +181 -0
- package/src/styles/tokens.ts +86 -0
- package/src/uniwind-env.d.ts +1 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Text
|
|
2
|
+
|
|
3
|
+
The library's type scale, and the one component that reproduces React Native's
|
|
4
|
+
own text inheritance through classNames. Compound root plus twelve presets:
|
|
5
|
+
`Display`, `Title`, `Header`, `Subheader`, `Paragraph`, `Label`, `Caption`,
|
|
6
|
+
`Overline`, and the four inline ones — `Strong`, `Emphasis`, `Link`, `Code`.
|
|
7
|
+
|
|
8
|
+
`import { Text } from "@delacour/react-native-ui/text";`
|
|
9
|
+
|
|
10
|
+
## Files
|
|
11
|
+
|
|
12
|
+
| File | What it holds |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `index.ts` | → `@delacour/react-native-ui/text` |
|
|
15
|
+
| `text.tsx` | Root, all twelve presets, the `Object.assign` surface — the pure-preset carve-out to one file per part, see [Compound component layout](../../../AGENTS.md#compound-component-layout) |
|
|
16
|
+
| `text.context.tsx` | `TextClassProvider`, `useTextClass()` |
|
|
17
|
+
| `text.variants.ts` | Pure `tv()` + `resolveTextClass`, no RN imports |
|
|
18
|
+
| `text.variants.test.ts` | |
|
|
19
|
+
|
|
20
|
+
## Design
|
|
21
|
+
|
|
22
|
+
- **Every `Text` publishes its resolved class to its subtree**, and merges the
|
|
23
|
+
one it inherited. Natively a nested `<Text>` inherits the parent's *style
|
|
24
|
+
object* and overrides only the keys it sets; Uniwind compiles each `className`
|
|
25
|
+
independently, so the parent's resolved class has to be threaded through and
|
|
26
|
+
beaten per-axis instead. `resolveTextClass` is that chain, weakest first:
|
|
27
|
+
`TEXT_BASE_CLASS`, the inherited class, this text's named axes, the caller's
|
|
28
|
+
`className`. It is the ladder `resolveIconSizeClass` already follows.
|
|
29
|
+
- **`textVariants` has no `defaultVariants`, and must not gain any.** An axis the
|
|
30
|
+
caller did not name has to emit *nothing* so it falls through to the enclosing
|
|
31
|
+
text's class — that fall-through is the whole feature. A default would emit
|
|
32
|
+
from inside the same call, ahead of the inherited class in the merge, and every
|
|
33
|
+
nested `Text` would snap back to it. `iconVariants` carries the same rule for
|
|
34
|
+
the same reason. The fallback lives in `TEXT_BASE_CLASS` instead.
|
|
35
|
+
- **`TEXT_BASE_CLASS` is applied unconditionally**, never guarded behind "am I
|
|
36
|
+
nested". Under true nesting it is a no-op — the parent's string already names
|
|
37
|
+
every axis, so tailwind-merge drops it. It only does work under a *partial*
|
|
38
|
+
provider, a [`Button`](../button/AGENTS.md) publishing nothing but `text-primary-foreground`, where
|
|
39
|
+
guarding it would leave a nested `Text` with a colour and no size at all,
|
|
40
|
+
collapsing to React Native's own 14pt default.
|
|
41
|
+
- **The correctness property is a fixpoint**: for every class this resolver can
|
|
42
|
+
produce, a nested `Text` with no props of its own resolves to *exactly* its
|
|
43
|
+
parent's string, at any depth. The tests assert it across the matrix. Anything
|
|
44
|
+
that breaks it makes inheritance lossy in a way only a screen would reveal.
|
|
45
|
+
- **A variant names type, never a line height.** tailwind-merge lists `leading`
|
|
46
|
+
among `font-size`'s conflicting groups, so a `text-lg` from the `size` axis
|
|
47
|
+
silently deletes a `leading-6` written beside a `text-base` — no error, just a
|
|
48
|
+
paragraph that loses its leading when someone resizes it. Tailwind v4's
|
|
49
|
+
`--text-*--line-height` companions survive `tokens.css`'s size-only overrides,
|
|
50
|
+
so every step already carries a paired leading. Prose needing its own gets a
|
|
51
|
+
`--text-paragraph` / `--text-paragraph--line-height` **pair**, registered in
|
|
52
|
+
`tokens.ts` — one utility carrying both.
|
|
53
|
+
- **`color` is page-level only — no `-foreground` family.** `X-foreground` means
|
|
54
|
+
"content drawn on an `X` surface" (rule 10), and mapping a surface variant to
|
|
55
|
+
its foreground is already each surface component's job
|
|
56
|
+
(`BUTTON_FOREGROUND_TOKEN`, `LIST_GROUP_FOREGROUND_TOKEN`). A second copy of
|
|
57
|
+
that map here could drift from it. Text on a coloured surface writes the
|
|
58
|
+
utility in a `className`.
|
|
59
|
+
- **`align` is `left`/`center`/`right`, never `start`/`end`.** React Native's
|
|
60
|
+
`textAlign` accepts `auto | left | right | center | justify` and nothing else,
|
|
61
|
+
so Tailwind v4's logical-property utilities resolve to a value RN rejects.
|
|
62
|
+
`transform: "none"` emits `normal-case` rather than an empty string, so it can
|
|
63
|
+
actually clear an `Overline`'s `uppercase`.
|
|
64
|
+
- **A nested `Text.Code` cannot be padded.** A nested `<Text>` is laid out by the
|
|
65
|
+
platform's text engine — an `NSAttributedString` run on iOS, a `Span` on
|
|
66
|
+
Android — and both ignore padding, margin and border radius on an inner
|
|
67
|
+
`<Text>`. Only the background survives. The compound variant applies the pill
|
|
68
|
+
treatment to the standalone case only; a code *block* is a `View` the caller
|
|
69
|
+
wraps around it.
|
|
70
|
+
- **Anything can publish into the cascade, not just a `Text`.** [`Button`](../button/AGENTS.md) wraps
|
|
71
|
+
its children in a `TextClassProvider` carrying the label's treatment beside the
|
|
72
|
+
`IconDefaultsProvider` it already renders, so a bare `<Text>` composed into one
|
|
73
|
+
needs nothing at the call site — the payoff [`Icon`](../icon/AGENTS.md) already had. Publish a
|
|
74
|
+
treatment only where **one** covers the whole subtree: a [`ListGroup`](../list-group/AGENTS.md) row
|
|
75
|
+
(title + description), a navbar (title + subtitle) and [`Screen.Error`](../screen/AGENTS.md) (title +
|
|
76
|
+
message) each carry two, and one provider cannot serve both, so their parts
|
|
77
|
+
keep per-part classes.
|
|
78
|
+
- **It renders `Animated.Text`.** Animated text styles work anywhere with no
|
|
79
|
+
opt-in, at the cost of a Reanimated wrapper per text node. If a long list ever
|
|
80
|
+
profiles badly, the escape hatch is to base the root on RN's `Text` and let app
|
|
81
|
+
code write `<Animated.Text className={useTextClass()} style={…} />` — the
|
|
82
|
+
cascade is on context, so that inherits correctly. Measure first.
|
|
83
|
+
- **OS font scaling is respected and capped.** `allowFontScaling` is left at
|
|
84
|
+
React Native's default; `maxFontSizeMultiplier` defaults to
|
|
85
|
+
`TEXT_MAX_FONT_SIZE_MULTIPLIER`. The cap exists for the fixed-height chrome —
|
|
86
|
+
`h-button-*` and `h-navbar-row` cannot grow, so an uncapped multiplier clips a
|
|
87
|
+
label rather than enlarging it. Both stay overridable per call site.
|
|
88
|
+
- **No `asChild`.** `Slot` throws on a non-element child and a `Text`'s child is
|
|
89
|
+
usually a string, so it would be unusable in the shape people would reach for.
|
|
90
|
+
There is also nothing to donate: `useTextClass()` hands the class over directly.
|
|
91
|
+
- **`TEXT_VARIANTS` is ordered — block presets first, largest first.** The tests
|
|
92
|
+
read that order to assert the type scale descends, so a new block role goes in
|
|
93
|
+
at its step rather than on the end; appended, it would pass a suite that is no
|
|
94
|
+
longer checking anything. `TEXT_INLINE_VARIANTS` is the tail of the same list
|
|
95
|
+
and carries no scale, so it has no such constraint.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export { Text, type TextPresetProps, type TextProps } from "./text";
|
|
2
|
+
export { TextClassProvider, useTextClass } from "./text.context";
|
|
3
|
+
export {
|
|
4
|
+
canNestText,
|
|
5
|
+
isInlineTextVariant,
|
|
6
|
+
resolveTextClass,
|
|
7
|
+
TEXT_ALIGNS,
|
|
8
|
+
TEXT_BASE_CLASS,
|
|
9
|
+
TEXT_COLORS,
|
|
10
|
+
TEXT_INLINE_VARIANTS,
|
|
11
|
+
TEXT_MAX_FONT_SIZE_MULTIPLIER,
|
|
12
|
+
TEXT_SIZES,
|
|
13
|
+
TEXT_TRANSFORMS,
|
|
14
|
+
TEXT_VARIANTS,
|
|
15
|
+
TEXT_WEIGHTS,
|
|
16
|
+
type TextAlign,
|
|
17
|
+
type TextColor,
|
|
18
|
+
type TextInlineVariant,
|
|
19
|
+
type TextSize,
|
|
20
|
+
type TextTransform,
|
|
21
|
+
type TextVariant,
|
|
22
|
+
type TextVariantProps,
|
|
23
|
+
type TextWeight,
|
|
24
|
+
textVariants,
|
|
25
|
+
} from "./text.variants";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createContext, type ReactElement, type ReactNode, use } from "react";
|
|
2
|
+
|
|
3
|
+
const TextClassContext = createContext<string | undefined>(undefined);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Supplies the classes a `Text` in this subtree inherits.
|
|
7
|
+
*
|
|
8
|
+
* Two things publish through it, and one context rather than two is the point.
|
|
9
|
+
* A `Text` publishes its own resolved class, which is what makes a nested
|
|
10
|
+
* `<Text>` behave the way React Native's native text inheritance does — the
|
|
11
|
+
* child adopts the parent's treatment and overrides only the axes it names. And
|
|
12
|
+
* any component can publish a class of its own, which is what lets a text style
|
|
13
|
+
* be *composed into* a component rather than passed as a prop: a bare `<Text>`
|
|
14
|
+
* inside a `<Button>` comes out at the button's label colour and type scale
|
|
15
|
+
* with nothing said at the call site.
|
|
16
|
+
*
|
|
17
|
+
* That is the same cascade `IconDefaultsProvider` gives icons, and deliberately
|
|
18
|
+
* the same *single* path. `Spinner` once read two contexts to recompute one
|
|
19
|
+
* value; see AGENTS.md rule 3.
|
|
20
|
+
*
|
|
21
|
+
* The value is a class string rather than an object because a `Text`'s colour
|
|
22
|
+
* *is* a class — the asymmetry that forces `IconDefaults` to carry a colour
|
|
23
|
+
* token alongside its className does not exist here. Its being a primitive is
|
|
24
|
+
* also why nothing has to memoise it: React compares context by value, so an
|
|
25
|
+
* unchanged string re-renders no consumer. That matters when every `Text` in
|
|
26
|
+
* the tree renders one of these.
|
|
27
|
+
*
|
|
28
|
+
* `undefined` resets the cascade — wrap a subtree that must not inherit an
|
|
29
|
+
* enclosing `Text`'s treatment in `<TextClassProvider value={undefined}>`.
|
|
30
|
+
*
|
|
31
|
+
* Lives in its own module, importing nothing but React, so a component in
|
|
32
|
+
* another folder can publish into it without closing a cycle. See AGENTS.md
|
|
33
|
+
* rule 3.
|
|
34
|
+
*/
|
|
35
|
+
export function TextClassProvider({
|
|
36
|
+
value,
|
|
37
|
+
children,
|
|
38
|
+
}: {
|
|
39
|
+
value: string | undefined;
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
}): ReactElement {
|
|
42
|
+
return <TextClassContext value={value}>{children}</TextClassContext>;
|
|
43
|
+
}
|
|
44
|
+
TextClassProvider.displayName = "DelacourUI.Text.ClassProvider";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The classes the nearest enclosing `Text` or `TextClassProvider` published, or
|
|
48
|
+
* `undefined` outside one.
|
|
49
|
+
*
|
|
50
|
+
* Public, so a component this library does not own can join the cascade — an
|
|
51
|
+
* animated label is `<Animated.Text className={useTextClass()} style={…} />`
|
|
52
|
+
* and inherits correctly with nothing else wired up.
|
|
53
|
+
*
|
|
54
|
+
* `undefined` rather than a `null` sentinel: there is no difference worth
|
|
55
|
+
* modelling between "no provider" and "a provider with nothing to say", and
|
|
56
|
+
* `cn` already treats `undefined` as an absent link in the chain.
|
|
57
|
+
*/
|
|
58
|
+
export function useTextClass(): string | undefined {
|
|
59
|
+
return use(TextClassContext);
|
|
60
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { ComponentProps, ReactElement } from "react";
|
|
2
|
+
import Animated from "react-native-reanimated";
|
|
3
|
+
import { TextClassProvider, useTextClass } from "./text.context";
|
|
4
|
+
import {
|
|
5
|
+
canNestText,
|
|
6
|
+
resolveTextClass,
|
|
7
|
+
TEXT_MAX_FONT_SIZE_MULTIPLIER,
|
|
8
|
+
type TextAlign,
|
|
9
|
+
type TextColor,
|
|
10
|
+
type TextSize,
|
|
11
|
+
type TextTransform,
|
|
12
|
+
type TextVariant,
|
|
13
|
+
type TextWeight,
|
|
14
|
+
} from "./text.variants";
|
|
15
|
+
|
|
16
|
+
export type TextProps = Omit<ComponentProps<typeof Animated.Text>, "className"> & {
|
|
17
|
+
/** The named role. Omit it and the text inherits, falling back to the base treatment. */
|
|
18
|
+
variant?: TextVariant;
|
|
19
|
+
/** A step on the type scale. Beats the variant's own size. */
|
|
20
|
+
size?: TextSize;
|
|
21
|
+
weight?: TextWeight;
|
|
22
|
+
/**
|
|
23
|
+
* A page-level text colour. For text drawn on a coloured surface — a button,
|
|
24
|
+
* a badge — use a `className`: that mapping belongs to the surface.
|
|
25
|
+
*/
|
|
26
|
+
color?: TextColor;
|
|
27
|
+
align?: TextAlign;
|
|
28
|
+
transform?: TextTransform;
|
|
29
|
+
/**
|
|
30
|
+
* Merged last, so it beats every named axis and anything inherited.
|
|
31
|
+
*
|
|
32
|
+
* An arbitrary font size has to be written as a length — `text-[17px]` or
|
|
33
|
+
* `text-(length:--x)`. A bare `text-[var(--x)]` is ambiguous to
|
|
34
|
+
* tailwind-merge, which files it under colour, so it will not override the
|
|
35
|
+
* inherited size.
|
|
36
|
+
*/
|
|
37
|
+
className?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** A preset's props: every axis except the role it already is. */
|
|
41
|
+
export type TextPresetProps = Omit<TextProps, "variant">;
|
|
42
|
+
|
|
43
|
+
function TextRoot({
|
|
44
|
+
align,
|
|
45
|
+
children,
|
|
46
|
+
className,
|
|
47
|
+
color,
|
|
48
|
+
maxFontSizeMultiplier = TEXT_MAX_FONT_SIZE_MULTIPLIER,
|
|
49
|
+
size,
|
|
50
|
+
transform,
|
|
51
|
+
variant,
|
|
52
|
+
weight,
|
|
53
|
+
...props
|
|
54
|
+
}: TextProps): ReactElement {
|
|
55
|
+
const inherited = useTextClass();
|
|
56
|
+
|
|
57
|
+
const resolved = resolveTextClass({
|
|
58
|
+
align,
|
|
59
|
+
className,
|
|
60
|
+
color,
|
|
61
|
+
inherited,
|
|
62
|
+
isNested: inherited !== undefined,
|
|
63
|
+
size,
|
|
64
|
+
transform,
|
|
65
|
+
variant,
|
|
66
|
+
weight,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const element = (
|
|
70
|
+
<Animated.Text className={resolved} maxFontSizeMultiplier={maxFontSizeMultiplier} {...props}>
|
|
71
|
+
{children}
|
|
72
|
+
</Animated.Text>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// A string or number child has no descendant that could read the context, so
|
|
76
|
+
// the provider is skipped rather than rendered once per label. `Text` is the
|
|
77
|
+
// most-instantiated component in an app; this is the common case.
|
|
78
|
+
return canNestText(children) ? <TextClassProvider value={resolved}>{element}</TextClassProvider> : element;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A screen's one hero line — 30pt bold.
|
|
83
|
+
*
|
|
84
|
+
* Announced as a heading, so a screen reader's rotor stops on it rather than
|
|
85
|
+
* reading it as one more line of body copy.
|
|
86
|
+
*/
|
|
87
|
+
function TextDisplay(props: TextPresetProps): ReactElement {
|
|
88
|
+
return <TextRoot accessibilityRole="header" variant="display" {...props} />;
|
|
89
|
+
}
|
|
90
|
+
TextDisplay.displayName = "DelacourUI.Text.Display";
|
|
91
|
+
|
|
92
|
+
/** A screen or card title — 24pt bold. Announced as a heading. */
|
|
93
|
+
function TextTitle(props: TextPresetProps): ReactElement {
|
|
94
|
+
return <TextRoot accessibilityRole="header" variant="title" {...props} />;
|
|
95
|
+
}
|
|
96
|
+
TextTitle.displayName = "DelacourUI.Text.Title";
|
|
97
|
+
|
|
98
|
+
/** A section heading — 20pt semibold. Announced as a heading. */
|
|
99
|
+
function TextHeader(props: TextPresetProps): ReactElement {
|
|
100
|
+
return <TextRoot accessibilityRole="header" variant="header" {...props} />;
|
|
101
|
+
}
|
|
102
|
+
TextHeader.displayName = "DelacourUI.Text.Header";
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The secondary line under a title — 18pt on the muted token.
|
|
106
|
+
*
|
|
107
|
+
* Deliberately not a fourth heading level, and deliberately not announced as
|
|
108
|
+
* one: it restates the title rather than introducing a new section.
|
|
109
|
+
*/
|
|
110
|
+
function TextSubheader(props: TextPresetProps): ReactElement {
|
|
111
|
+
return <TextRoot variant="subheader" {...props} />;
|
|
112
|
+
}
|
|
113
|
+
TextSubheader.displayName = "DelacourUI.Text.Subheader";
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A block of prose — 16pt on the foreground token.
|
|
117
|
+
*
|
|
118
|
+
* The same treatment a bare `<Text>` already renders. It exists so a paragraph
|
|
119
|
+
* *reads* as one at the call site, the way `Button.Label` exists even though a
|
|
120
|
+
* bare string child already produces one.
|
|
121
|
+
*/
|
|
122
|
+
function TextParagraph(props: TextPresetProps): ReactElement {
|
|
123
|
+
return <TextRoot variant="paragraph" {...props} />;
|
|
124
|
+
}
|
|
125
|
+
TextParagraph.displayName = "DelacourUI.Text.Paragraph";
|
|
126
|
+
|
|
127
|
+
/** A control label or compact UI string — 14pt medium. */
|
|
128
|
+
function TextLabel(props: TextPresetProps): ReactElement {
|
|
129
|
+
return <TextRoot variant="label" {...props} />;
|
|
130
|
+
}
|
|
131
|
+
TextLabel.displayName = "DelacourUI.Text.Label";
|
|
132
|
+
|
|
133
|
+
/** Supporting copy under a label or a title — 14pt on the muted token. */
|
|
134
|
+
function TextCaption(props: TextPresetProps): ReactElement {
|
|
135
|
+
return <TextRoot variant="caption" {...props} />;
|
|
136
|
+
}
|
|
137
|
+
TextCaption.displayName = "DelacourUI.Text.Caption";
|
|
138
|
+
|
|
139
|
+
/** An eyebrow above a section — 12pt semibold, uppercase, muted. */
|
|
140
|
+
function TextOverline(props: TextPresetProps): ReactElement {
|
|
141
|
+
return <TextRoot variant="overline" {...props} />;
|
|
142
|
+
}
|
|
143
|
+
TextOverline.displayName = "DelacourUI.Text.Overline";
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Inline emphasis by weight.
|
|
147
|
+
*
|
|
148
|
+
* Emits `font-semibold` and nothing else, so mid-paragraph it comes out at the
|
|
149
|
+
* surrounding text's size and colour. Standing on its own it falls back to the
|
|
150
|
+
* base treatment — React Native's own default colour does not follow the theme.
|
|
151
|
+
*/
|
|
152
|
+
function TextStrong(props: TextPresetProps): ReactElement {
|
|
153
|
+
return <TextRoot variant="strong" {...props} />;
|
|
154
|
+
}
|
|
155
|
+
TextStrong.displayName = "DelacourUI.Text.Strong";
|
|
156
|
+
|
|
157
|
+
/** Inline emphasis by slant. Inherits size, weight and colour from the text around it. */
|
|
158
|
+
function TextEmphasis(props: TextPresetProps): ReactElement {
|
|
159
|
+
return <TextRoot variant="emphasis" {...props} />;
|
|
160
|
+
}
|
|
161
|
+
TextEmphasis.displayName = "DelacourUI.Text.Emphasis";
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* A tappable run of text. `onPress` rides through from React Native's own
|
|
165
|
+
* `TextProps` and works on a nested `<Text>` on both platforms.
|
|
166
|
+
*
|
|
167
|
+
* Painted with `info`, not `primary`: `--primary` is the primary surface
|
|
168
|
+
* colour and sits within a shade of `foreground` in both themes.
|
|
169
|
+
*/
|
|
170
|
+
function TextLink(props: TextPresetProps): ReactElement {
|
|
171
|
+
return <TextRoot accessibilityRole="link" variant="link" {...props} />;
|
|
172
|
+
}
|
|
173
|
+
TextLink.displayName = "DelacourUI.Text.Link";
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Inline code, monospaced on a muted background.
|
|
177
|
+
*
|
|
178
|
+
* Padded and rounded only when it stands alone. A nested `<Text>` is laid out
|
|
179
|
+
* by the platform's text engine rather than by Yoga — an `NSAttributedString`
|
|
180
|
+
* run on iOS, a `Span` on Android — and both ignore padding, margin and border
|
|
181
|
+
* radius outright, so only the background survives. For a code *block*, wrap it:
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* <View className="rounded-lg bg-muted p-3">
|
|
185
|
+
* <Text.Code className="bg-transparent" size="sm">bun run gen-exports</Text.Code>
|
|
186
|
+
* </View>
|
|
187
|
+
*/
|
|
188
|
+
function TextCode(props: TextPresetProps): ReactElement {
|
|
189
|
+
return <TextRoot variant="code" {...props} />;
|
|
190
|
+
}
|
|
191
|
+
TextCode.displayName = "DelacourUI.Text.Code";
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* A piece of text, and the type scale the whole library draws from.
|
|
195
|
+
*
|
|
196
|
+
* Every `Text` publishes its own resolved classes to its subtree, so a nested
|
|
197
|
+
* `<Text>` inherits the treatment around it and overrides only the axes it
|
|
198
|
+
* names — which is what React Native does natively with a nested `<Text>`'s
|
|
199
|
+
* style, and what Uniwind breaks by compiling each `className` independently.
|
|
200
|
+
*
|
|
201
|
+
* Precedence, weakest first: the base treatment, the inherited class, this
|
|
202
|
+
* text's own named axes, then its `className`.
|
|
203
|
+
*
|
|
204
|
+
* Anything can publish into that cascade, not just a `Text` — `Button` wraps its
|
|
205
|
+
* children in a `TextClassProvider` carrying the label's treatment, so a bare
|
|
206
|
+
* `<Text>` composed into one needs nothing at the call site. The same idea as
|
|
207
|
+
* `IconDefaultsProvider`, and the same single path.
|
|
208
|
+
*
|
|
209
|
+
* OS font scaling is respected and capped at {@link TEXT_MAX_FONT_SIZE_MULTIPLIER};
|
|
210
|
+
* both `allowFontScaling` and `maxFontSizeMultiplier` remain overridable.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* <Text.Title>Total <Text color="muted">USD</Text></Text.Title>
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* <Text.Paragraph>
|
|
217
|
+
* Run <Text.Code>bun test</Text.Code> before you <Text.Strong>commit</Text.Strong>.
|
|
218
|
+
* </Text.Paragraph>
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* // No `asChild`: a Text's child is usually a string, which `Slot` cannot take
|
|
222
|
+
* // props. Read the cascade directly instead.
|
|
223
|
+
* <Animated.Text className={useTextClass()} style={fadeStyle} />
|
|
224
|
+
*/
|
|
225
|
+
export const Text = Object.assign(TextRoot, {
|
|
226
|
+
/** A screen's one hero line — 30pt bold. Announced as a heading. */
|
|
227
|
+
Display: TextDisplay,
|
|
228
|
+
/** A screen or card title — 24pt bold. Announced as a heading. */
|
|
229
|
+
Title: TextTitle,
|
|
230
|
+
/** A section heading — 20pt semibold. Announced as a heading. */
|
|
231
|
+
Header: TextHeader,
|
|
232
|
+
/** The secondary line under a title — 18pt muted. Not a fourth heading level. */
|
|
233
|
+
Subheader: TextSubheader,
|
|
234
|
+
/** A block of prose — 16pt. The same treatment a bare `<Text>` renders. */
|
|
235
|
+
Paragraph: TextParagraph,
|
|
236
|
+
/** A control label or compact UI string — 14pt medium. */
|
|
237
|
+
Label: TextLabel,
|
|
238
|
+
/** Supporting copy — 14pt on the muted token. */
|
|
239
|
+
Caption: TextCaption,
|
|
240
|
+
/** An eyebrow above a section — 12pt semibold, uppercase, muted. */
|
|
241
|
+
Overline: TextOverline,
|
|
242
|
+
/** Inline emphasis by weight. Inherits size and colour from the text around it. */
|
|
243
|
+
Strong: TextStrong,
|
|
244
|
+
/** Inline emphasis by slant. Inherits size, weight and colour from the text around it. */
|
|
245
|
+
Emphasis: TextEmphasis,
|
|
246
|
+
/** A tappable run of text, on the `info` token. Takes `onPress`. */
|
|
247
|
+
Link: TextLink,
|
|
248
|
+
/** Inline code, monospaced. Padded only when it stands alone. */
|
|
249
|
+
Code: TextCode,
|
|
250
|
+
displayName: "DelacourUI.Text",
|
|
251
|
+
});
|