@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,329 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
import { tv } from "../../lib/tv";
|
|
4
|
+
import type { ButtonGroupOrientation, ButtonGroupSlotPosition } from "../button/button.variants";
|
|
5
|
+
|
|
6
|
+
export const INPUT_VARIANTS = ["primary", "secondary"] as const;
|
|
7
|
+
|
|
8
|
+
export const INPUT_SIZES = ["sm", "md", "lg"] as const;
|
|
9
|
+
|
|
10
|
+
export type InputVariant = (typeof INPUT_VARIANTS)[number];
|
|
11
|
+
export type InputSize = (typeof INPUT_SIZES)[number];
|
|
12
|
+
|
|
13
|
+
/** Theme token an icon in a decorator inherits, and the one it takes when invalid. */
|
|
14
|
+
export const INPUT_DECORATOR_ICON_TOKEN = "muted-foreground";
|
|
15
|
+
export const INPUT_INVALID_DECORATOR_ICON_TOKEN = "destructive";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Default colours for the three `TextInput` props that take a colour rather
|
|
19
|
+
* than a style — the placeholder, and the caret and selection highlight.
|
|
20
|
+
*
|
|
21
|
+
* These are `accent-*` utilities, not `text-*` ones, and that is not a style
|
|
22
|
+
* choice. Uniwind bridges a className to one of these props by compiling it and
|
|
23
|
+
* reading `styles.accentColor`, so a class that sets any other property
|
|
24
|
+
* resolves to nothing: uniwind warns once in development and the prop is left
|
|
25
|
+
* undefined, which reads as "the platform default" rather than as an error.
|
|
26
|
+
*/
|
|
27
|
+
export const INPUT_PLACEHOLDER_ACCENT_CLASS = "accent-muted-foreground";
|
|
28
|
+
export const INPUT_SELECTION_ACCENT_CLASS = "accent-primary";
|
|
29
|
+
export const INPUT_INVALID_SELECTION_ACCENT_CLASS = "accent-destructive";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Styling for every part of a text field.
|
|
33
|
+
*
|
|
34
|
+
* One slotted `tv()` rather than a call per part, so `size` and `variant` are
|
|
35
|
+
* declared once — and, more importantly here, so the box is declared once. The
|
|
36
|
+
* `root` slot is the whole bordered box, and it lands on the `TextInput` when
|
|
37
|
+
* the field stands alone or on `Input.Group`'s row when it does not. That is
|
|
38
|
+
* what makes a grouped field identical to a lone one by construction rather
|
|
39
|
+
* than by two class strings happening to agree; `input.variants.test.ts` pins
|
|
40
|
+
* it as a property over every combination of the axes.
|
|
41
|
+
*
|
|
42
|
+
* `root` therefore holds no `text-*` utility. When the field is grouped that
|
|
43
|
+
* slot is worn by a `View`, and a React Native `View` does not cascade colour
|
|
44
|
+
* to a `Text` descendant — so the type scale and the colour live on `field`,
|
|
45
|
+
* which is always the `TextInput` itself. See AGENTS.md rule 1.
|
|
46
|
+
*
|
|
47
|
+
* A size names tokens rather than raw utilities: `h-input-md` for the box,
|
|
48
|
+
* `text-input-md` for the value, `size-icon-md` for a decorator's glyph. The
|
|
49
|
+
* values live in `tokens.css`, so a field and the icon inside it resolve
|
|
50
|
+
* against one scale rather than two numbers happening to agree. `cn` and `tv`
|
|
51
|
+
* both have to know these token names — see `lib/cn.ts` and `styles/tokens.ts`.
|
|
52
|
+
*
|
|
53
|
+
* Prefix and suffix share the one `decorator` slot. They are the same box in
|
|
54
|
+
* different places, and a second identical slot is a second definition that can
|
|
55
|
+
* drift.
|
|
56
|
+
*
|
|
57
|
+
* Free of React Native imports so it stays unit-testable — `bun test` cannot
|
|
58
|
+
* parse React Native's Flow-typed source. See AGENTS.md.
|
|
59
|
+
*/
|
|
60
|
+
export const inputVariants = tv({
|
|
61
|
+
slots: {
|
|
62
|
+
root: "overflow-hidden border",
|
|
63
|
+
/** Layout for the group's row. Never applied to a lone field, which is not a row. */
|
|
64
|
+
row: "flex-row items-center",
|
|
65
|
+
/*
|
|
66
|
+
* The one text surface in this package that is not a `Text`: a raw React
|
|
67
|
+
* Native `TextInput`, which inherits nothing. `font-sans` is restated for
|
|
68
|
+
* the same reason `TEXT_BASE_CLASS` carries it — without it a themed
|
|
69
|
+
* typeface would stop at the field's edge.
|
|
70
|
+
*/
|
|
71
|
+
field: "font-sans text-foreground",
|
|
72
|
+
decorator: "items-center justify-center",
|
|
73
|
+
/** Edge length an `Icon` in a decorator inherits. */
|
|
74
|
+
decoratorIcon: "",
|
|
75
|
+
/** Treatment a bare `Text` affix in a decorator inherits. */
|
|
76
|
+
decoratorText: "text-muted-foreground",
|
|
77
|
+
},
|
|
78
|
+
variants: {
|
|
79
|
+
variant: {
|
|
80
|
+
primary: { root: "border-input bg-card" },
|
|
81
|
+
secondary: { root: "border-transparent bg-secondary" },
|
|
82
|
+
},
|
|
83
|
+
size: {
|
|
84
|
+
sm: {
|
|
85
|
+
root: "px-2.5",
|
|
86
|
+
row: "gap-1.5",
|
|
87
|
+
field: "text-input-sm leading-tight",
|
|
88
|
+
decoratorIcon: "size-icon-sm",
|
|
89
|
+
decoratorText: "text-input-sm",
|
|
90
|
+
},
|
|
91
|
+
md: {
|
|
92
|
+
root: "px-3",
|
|
93
|
+
row: "gap-2",
|
|
94
|
+
field: "text-input-md leading-tight",
|
|
95
|
+
decoratorIcon: "size-icon-md",
|
|
96
|
+
decoratorText: "text-input-md",
|
|
97
|
+
},
|
|
98
|
+
lg: {
|
|
99
|
+
root: "px-3.5",
|
|
100
|
+
row: "gap-2.5",
|
|
101
|
+
field: "text-input-lg leading-tight",
|
|
102
|
+
decoratorIcon: "size-icon-lg",
|
|
103
|
+
decoratorText: "text-input-lg",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
// The empty `false` branches are load-bearing typing, not placeholders.
|
|
107
|
+
// `tv` derives the prop type from the declared keys, so a map with only
|
|
108
|
+
// `true` types the prop as `true` rather than `boolean` and rejects
|
|
109
|
+
// `inputVariants({ isFocused })`. See button.variants.ts for the same note.
|
|
110
|
+
isFocused: { true: { root: "border-ring" }, false: {} },
|
|
111
|
+
isInvalid: { true: { root: "border-destructive", decoratorText: "text-destructive" }, false: {} },
|
|
112
|
+
isDisabled: { true: { root: "opacity-50" }, false: {} },
|
|
113
|
+
isMultiline: { true: {}, false: {} },
|
|
114
|
+
// A grouped field shares its row, so it stretches — and it clears React
|
|
115
|
+
// Native's own padding, which on Android would otherwise inset the value
|
|
116
|
+
// from a gutter the row has already applied.
|
|
117
|
+
isGrouped: { true: { field: "flex-1 p-0" }, false: {} },
|
|
118
|
+
/** Which way an enclosing `Button.Group` runs, when there is one. */
|
|
119
|
+
orientation: { horizontal: {}, vertical: {} },
|
|
120
|
+
// A field standing on its own is `none` and keeps the corner off the
|
|
121
|
+
// generic ramp. Every other value means it is joined, and a joined
|
|
122
|
+
// control takes the group's corner rather than its own — see the note on
|
|
123
|
+
// the corner compounds below.
|
|
124
|
+
groupPosition: { none: {}, first: {}, middle: { root: "rounded-none" }, last: {}, only: {} },
|
|
125
|
+
isSeamed: { true: {}, false: {} },
|
|
126
|
+
},
|
|
127
|
+
compoundVariants: [
|
|
128
|
+
// The height and the vertical rhythm are declared together rather than
|
|
129
|
+
// fighting each other in the merge, the way a button's square footprint
|
|
130
|
+
// and its horizontal padding are.
|
|
131
|
+
//
|
|
132
|
+
// `py-0` on a single-line field is load-bearing on Android: React
|
|
133
|
+
// Native's `TextInput` inherits the platform's own vertical padding,
|
|
134
|
+
// which would push the value off centre inside a fixed height.
|
|
135
|
+
{ isMultiline: false, size: "sm", class: { root: "h-input-sm py-0" } },
|
|
136
|
+
{ isMultiline: false, size: "md", class: { root: "h-input-md py-0" } },
|
|
137
|
+
{ isMultiline: false, size: "lg", class: { root: "h-input-lg py-0" } },
|
|
138
|
+
// A multiline field grows with its text, so the height becomes a floor.
|
|
139
|
+
// The row aligns to the top with it: centred decorators would drift down
|
|
140
|
+
// the side of a paragraph instead of sitting on its first line.
|
|
141
|
+
{ isMultiline: true, size: "sm", class: { root: "min-h-input-sm py-2", row: "items-start" } },
|
|
142
|
+
{ isMultiline: true, size: "md", class: { root: "min-h-input-md py-2.5", row: "items-start" } },
|
|
143
|
+
{ isMultiline: true, size: "lg", class: { root: "min-h-input-lg py-3", row: "items-start" } },
|
|
144
|
+
// Focus is transient and invalid is reported, so invalid outranks it. A
|
|
145
|
+
// field that went grey the moment it was tapped would drop the only
|
|
146
|
+
// signal it has that its value is wrong, exactly while it is being fixed.
|
|
147
|
+
{ isFocused: true, isInvalid: true, class: { root: "border-destructive" } },
|
|
148
|
+
// The corner a lone field draws: the generic ramp, deliberately not the
|
|
149
|
+
// button's, because a field and the button beside it are meant to be
|
|
150
|
+
// retunable apart. `sm` steps down a notch; the other two share `lg`.
|
|
151
|
+
{ groupPosition: "none", size: "sm", class: { root: "rounded-md" } },
|
|
152
|
+
{ groupPosition: "none", size: "md", class: { root: "rounded-lg" } },
|
|
153
|
+
{ groupPosition: "none", size: "lg", class: { root: "rounded-lg" } },
|
|
154
|
+
// Joined, it takes the *group's* corner instead. A group owns the shape
|
|
155
|
+
// of its run, so a field capping one end of it has to draw the same arc
|
|
156
|
+
// the button capping the other end does — two ramps meeting in one row
|
|
157
|
+
// is the mismatch this replaces. `button.variants.test.ts` asserts these
|
|
158
|
+
// strings match the button's cell for cell.
|
|
159
|
+
{ groupPosition: "only", size: "sm", class: { root: "rounded-button-sm" } },
|
|
160
|
+
{ groupPosition: "only", size: "md", class: { root: "rounded-button-md" } },
|
|
161
|
+
{ groupPosition: "only", size: "lg", class: { root: "rounded-button-lg" } },
|
|
162
|
+
{
|
|
163
|
+
groupPosition: "first",
|
|
164
|
+
orientation: "horizontal",
|
|
165
|
+
size: "sm",
|
|
166
|
+
class: { root: "rounded-s-button-sm rounded-e-none" },
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
groupPosition: "first",
|
|
170
|
+
orientation: "horizontal",
|
|
171
|
+
size: "md",
|
|
172
|
+
class: { root: "rounded-s-button-md rounded-e-none" },
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
groupPosition: "first",
|
|
176
|
+
orientation: "horizontal",
|
|
177
|
+
size: "lg",
|
|
178
|
+
class: { root: "rounded-s-button-lg rounded-e-none" },
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
groupPosition: "last",
|
|
182
|
+
orientation: "horizontal",
|
|
183
|
+
size: "sm",
|
|
184
|
+
class: { root: "rounded-e-button-sm rounded-s-none" },
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
groupPosition: "last",
|
|
188
|
+
orientation: "horizontal",
|
|
189
|
+
size: "md",
|
|
190
|
+
class: { root: "rounded-e-button-md rounded-s-none" },
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
groupPosition: "last",
|
|
194
|
+
orientation: "horizontal",
|
|
195
|
+
size: "lg",
|
|
196
|
+
class: { root: "rounded-e-button-lg rounded-s-none" },
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
groupPosition: "first",
|
|
200
|
+
orientation: "vertical",
|
|
201
|
+
size: "sm",
|
|
202
|
+
class: { root: "rounded-t-button-sm rounded-b-none" },
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
groupPosition: "first",
|
|
206
|
+
orientation: "vertical",
|
|
207
|
+
size: "md",
|
|
208
|
+
class: { root: "rounded-t-button-md rounded-b-none" },
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
groupPosition: "first",
|
|
212
|
+
orientation: "vertical",
|
|
213
|
+
size: "lg",
|
|
214
|
+
class: { root: "rounded-t-button-lg rounded-b-none" },
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
groupPosition: "last",
|
|
218
|
+
orientation: "vertical",
|
|
219
|
+
size: "sm",
|
|
220
|
+
class: { root: "rounded-b-button-sm rounded-t-none" },
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
groupPosition: "last",
|
|
224
|
+
orientation: "vertical",
|
|
225
|
+
size: "md",
|
|
226
|
+
class: { root: "rounded-b-button-md rounded-t-none" },
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
groupPosition: "last",
|
|
230
|
+
orientation: "vertical",
|
|
231
|
+
size: "lg",
|
|
232
|
+
class: { root: "rounded-b-button-lg rounded-t-none" },
|
|
233
|
+
},
|
|
234
|
+
{ isSeamed: true, orientation: "horizontal", class: { root: "-ms-px" } },
|
|
235
|
+
{ isSeamed: true, orientation: "vertical", class: { root: "-mt-px" } },
|
|
236
|
+
],
|
|
237
|
+
defaultVariants: {
|
|
238
|
+
variant: "primary",
|
|
239
|
+
size: "md",
|
|
240
|
+
isFocused: false,
|
|
241
|
+
isInvalid: false,
|
|
242
|
+
isDisabled: false,
|
|
243
|
+
isMultiline: false,
|
|
244
|
+
isGrouped: false,
|
|
245
|
+
orientation: "horizontal",
|
|
246
|
+
groupPosition: "none",
|
|
247
|
+
isSeamed: false,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
/** The axes that decide how a field's box is drawn, wherever that box lands. */
|
|
252
|
+
export type InputBoxState = {
|
|
253
|
+
variant?: InputVariant;
|
|
254
|
+
size?: InputSize;
|
|
255
|
+
isDisabled?: boolean;
|
|
256
|
+
isFocused?: boolean;
|
|
257
|
+
isInvalid?: boolean;
|
|
258
|
+
isMultiline?: boolean;
|
|
259
|
+
/** Where the field sits in an enclosing `Button.Group`, if it is in one. */
|
|
260
|
+
groupPosition?: ButtonGroupSlotPosition;
|
|
261
|
+
/** Which way that group runs. Read only when `groupPosition` is not `none`. */
|
|
262
|
+
orientation?: ButtonGroupOrientation;
|
|
263
|
+
/** Overlap the member before it, so the shared edge is drawn once. */
|
|
264
|
+
isSeamed?: boolean;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The class the `TextInput` itself wears.
|
|
269
|
+
*
|
|
270
|
+
* Standalone, the field *is* the box, so it takes the chrome and the text
|
|
271
|
+
* treatment together. Inside an `Input.Group` the box belongs to the row and
|
|
272
|
+
* the field takes the text treatment alone — otherwise the two would draw two
|
|
273
|
+
* borders, one inside the other.
|
|
274
|
+
*
|
|
275
|
+
* This is the single decision that keeps a grouped field indistinguishable from
|
|
276
|
+
* a lone one: both read the same `root` slot, so there is no second class
|
|
277
|
+
* string that could drift from the first.
|
|
278
|
+
*
|
|
279
|
+
* Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
|
|
280
|
+
*/
|
|
281
|
+
export function resolveInputFieldClass({
|
|
282
|
+
className,
|
|
283
|
+
isGrouped = false,
|
|
284
|
+
...state
|
|
285
|
+
}: InputBoxState & { className?: string; isGrouped?: boolean }): string {
|
|
286
|
+
const slots = inputVariants({ ...state, isGrouped });
|
|
287
|
+
return isGrouped ? slots.field({ className }) : cn(slots.root(), slots.field({ className }));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The class `Input.Group`'s row wears: the same box a lone field would draw,
|
|
292
|
+
* laid out as a row so the decorators and the field can share it.
|
|
293
|
+
*
|
|
294
|
+
* Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
|
|
295
|
+
*/
|
|
296
|
+
export function resolveInputGroupClass({ className, ...state }: InputBoxState & { className?: string }): string {
|
|
297
|
+
const slots = inputVariants({ ...state, isGrouped: true });
|
|
298
|
+
return cn(slots.root(), slots.row({ className }));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The `accent-*` class uniwind resolves the placeholder colour from.
|
|
303
|
+
*
|
|
304
|
+
* Pure, so it is reachable from `bun test`. See AGENTS.md.
|
|
305
|
+
*/
|
|
306
|
+
export function resolvePlaceholderAccentClass(className?: string): string {
|
|
307
|
+
return cn(INPUT_PLACEHOLDER_ACCENT_CLASS, className);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The `accent-*` class uniwind resolves the caret and selection colour from.
|
|
312
|
+
*
|
|
313
|
+
* An invalid field tints them destructive along with its border, so the state is
|
|
314
|
+
* still legible while the value is being corrected — the moment the border is
|
|
315
|
+
* the only thing carrying it. A caller's class still wins.
|
|
316
|
+
*
|
|
317
|
+
* Pure, so it is reachable from `bun test`. See AGENTS.md.
|
|
318
|
+
*/
|
|
319
|
+
export function resolveSelectionAccentClass({
|
|
320
|
+
className,
|
|
321
|
+
isInvalid = false,
|
|
322
|
+
}: {
|
|
323
|
+
className?: string;
|
|
324
|
+
isInvalid?: boolean;
|
|
325
|
+
}): string {
|
|
326
|
+
return cn(isInvalid ? INPUT_INVALID_SELECTION_ACCENT_CLASS : INPUT_SELECTION_ACCENT_CLASS, className);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export type InputVariantProps = VariantProps<typeof inputVariants>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# ListGroup
|
|
2
|
+
|
|
3
|
+
A surface grouping related rows. Compound root plus `ListGroup.Item` and its
|
|
4
|
+
five slots: `ItemPrefix`, `ItemContent`, `ItemTitle`, `ItemDescription`,
|
|
5
|
+
`ItemSuffix`.
|
|
6
|
+
|
|
7
|
+
`import { ListGroup } from "@delacour/react-native-ui/list-group";`
|
|
8
|
+
|
|
9
|
+
## Files
|
|
10
|
+
|
|
11
|
+
| File | What it holds |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `index.ts` | → `@delacour/react-native-ui/list-group` |
|
|
14
|
+
| `list-group.tsx` | Root + the `Object.assign` compound surface |
|
|
15
|
+
| `list-group-item.tsx` | `ListGroup.Item`, and the bare-text wrap it owns |
|
|
16
|
+
| `list-group-item-prefix.tsx` | `ListGroup.ItemPrefix` |
|
|
17
|
+
| `list-group-item-content.tsx` | `ListGroup.ItemContent` |
|
|
18
|
+
| `list-group-item-title.tsx` | `ListGroup.ItemTitle` |
|
|
19
|
+
| `list-group-item-description.tsx` | `ListGroup.ItemDescription` |
|
|
20
|
+
| `list-group-item-suffix.tsx` | `ListGroup.ItemSuffix` |
|
|
21
|
+
| `list-group.context.tsx` | `ListGroupProvider`, `useListGroup()`, `useListGroupContext()`, `useListGroupPart()` |
|
|
22
|
+
| `list-group.types.ts` | Prop types shared by two or more parts |
|
|
23
|
+
| `list-group.variants.ts` | Pure `tv()` slots, no RN imports |
|
|
24
|
+
| `list-group.variants.test.ts` | |
|
|
25
|
+
|
|
26
|
+
## Design
|
|
27
|
+
|
|
28
|
+
- **Variants**: `default`, `secondary`, `tertiary`, `transparent`.
|
|
29
|
+
**Sizes**: `sm`, `md`, `lg`. Size is not decoration — it drives the row
|
|
30
|
+
metrics, the title and description type scale, both icon sizes *and* the
|
|
31
|
+
divider inset, which is why those five numbers live in one axis rather than
|
|
32
|
+
five magic values.
|
|
33
|
+
- **The corner is `rounded-md`, the 0.8 step of the shared ramp.** shadcn puts a
|
|
34
|
+
grouped surface a step or two below a card, so a group reads as a control
|
|
35
|
+
surface rather than as one. Size does not move it, and neither does a button's
|
|
36
|
+
own corner — `--radius-button-*` is a capsule at half its height and outside
|
|
37
|
+
this ramp. See the corner-scale note in
|
|
38
|
+
[the package AGENTS.md](../../../AGENTS.md).
|
|
39
|
+
- **Dividers are inserted, not written out.** The root walks its children and
|
|
40
|
+
puts a [`Separator`](../separator/AGENTS.md) between adjacent ones, inset to
|
|
41
|
+
line up with the rows' padding. A `Separator` placed by hand suppresses the
|
|
42
|
+
automatic one on either side of it, so a caller can make one gap full-bleed
|
|
43
|
+
without turning the feature off; `isDivided={false}` turns it off entirely.
|
|
44
|
+
`Children.toArray` drops the nulls a conditional child leaves behind, so a row
|
|
45
|
+
rendered only some of the time does not strand a divider.
|
|
46
|
+
- **The root clips.** `overflow-hidden` is load-bearing: a pressed row fades to
|
|
47
|
+
the edge of its own box, and the first and last rows would square off the
|
|
48
|
+
group's corners without it.
|
|
49
|
+
- **A row is a `Pressable`.** `ListGroupItemProps` extends `PressableProps`, so
|
|
50
|
+
`feedback`, `haptic`, `pressedScale` and the rest are inherited rather than
|
|
51
|
+
restated — the row owns no vocabulary of its own. Only the default differs:
|
|
52
|
+
`feedback` defaults to `fade`, because a full-bleed row that scales reads as
|
|
53
|
+
the whole card flexing rather than as one row responding. A prop is only
|
|
54
|
+
redeclared where it genuinely changes, as [`Button`](../button/AGENTS.md) does
|
|
55
|
+
with its narrower union; redeclaring one unchanged just to hang a doc comment
|
|
56
|
+
on it puts a second definition in the tree that can drift.
|
|
57
|
+
- **Icons are composed, never passed as props.** `ItemPrefix` wraps its subtree
|
|
58
|
+
in an `IconDefaultsProvider` carrying the `prefixIcon` slot — a step on the
|
|
59
|
+
shared icon scale, like every other glyph in the library — and `foreground`,
|
|
60
|
+
so a bare `<Icon icon={IconUser} />` needs nothing said at the call site.
|
|
61
|
+
`ItemSuffix` draws a chevron when it has no children of its own; `iconProps`
|
|
62
|
+
tunes that glyph and is ignored once it does.
|
|
63
|
+
- **String children** are wrapped in an `ItemContent` around an `ItemTitle`
|
|
64
|
+
automatically, consecutive strings collapsing into one — the same rule, and
|
|
65
|
+
the same reason, as [`Button`](../button/AGENTS.md).
|
|
66
|
+
- **Title colour goes on the title.** The `item` slot carries no `text-*`; a row
|
|
67
|
+
is a `View` and cannot cascade colour to a `Text`. The tests assert this.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { ListGroup, type ListGroupProps } from "./list-group";
|
|
2
|
+
export {
|
|
3
|
+
type ListGroupContextValue,
|
|
4
|
+
ListGroupProvider,
|
|
5
|
+
useListGroup,
|
|
6
|
+
useListGroupContext,
|
|
7
|
+
} from "./list-group.context";
|
|
8
|
+
export type { ListGroupSlotProps, ListGroupTextProps } from "./list-group.types";
|
|
9
|
+
export {
|
|
10
|
+
LIST_GROUP_FOREGROUND_TOKEN,
|
|
11
|
+
LIST_GROUP_SIZES,
|
|
12
|
+
LIST_GROUP_SUFFIX_ICON_TOKEN,
|
|
13
|
+
LIST_GROUP_VARIANTS,
|
|
14
|
+
type ListGroupSize,
|
|
15
|
+
type ListGroupVariant,
|
|
16
|
+
type ListGroupVariantProps,
|
|
17
|
+
listGroupVariants,
|
|
18
|
+
} from "./list-group.variants";
|
|
19
|
+
export type { ListGroupItemProps } from "./list-group-item";
|
|
20
|
+
export type { ListGroupIconProps, ListGroupItemSuffixProps } from "./list-group-item-suffix";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import type { ListGroupSlotProps } from "./list-group.types";
|
|
4
|
+
import { listGroupVariants } from "./list-group.variants";
|
|
5
|
+
|
|
6
|
+
/** The text column of a row, taking whatever width the prefix and suffix leave. */
|
|
7
|
+
export function ListGroupItemContent({ className, ...props }: ListGroupSlotProps): ReactElement {
|
|
8
|
+
return <View className={listGroupVariants().content({ className })} {...props} />;
|
|
9
|
+
}
|
|
10
|
+
ListGroupItemContent.displayName = "DelacourUI.ListGroup.ItemContent";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Text } from "../text";
|
|
3
|
+
import { useListGroupPart } from "./list-group.context";
|
|
4
|
+
import type { ListGroupTextProps } from "./list-group.types";
|
|
5
|
+
import { listGroupVariants } from "./list-group.variants";
|
|
6
|
+
|
|
7
|
+
/** The row's secondary line, a step down in scale and on the muted token. */
|
|
8
|
+
export function ListGroupItemDescription({ className, ...props }: ListGroupTextProps): ReactElement {
|
|
9
|
+
const { size } = useListGroupPart("ListGroup.ItemDescription");
|
|
10
|
+
return <Text className={listGroupVariants({ size }).description({ className })} {...props} />;
|
|
11
|
+
}
|
|
12
|
+
ListGroupItemDescription.displayName = "DelacourUI.ListGroup.ItemDescription";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ReactElement, useMemo } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { IconDefaultsProvider } from "../icon";
|
|
4
|
+
import { useListGroupPart } from "./list-group.context";
|
|
5
|
+
import type { ListGroupSlotProps } from "./list-group.types";
|
|
6
|
+
import { LIST_GROUP_FOREGROUND_TOKEN, listGroupVariants } from "./list-group.variants";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The leading slot of a row.
|
|
10
|
+
*
|
|
11
|
+
* Its subtree inherits the list group's icon size and the foreground token, so a
|
|
12
|
+
* bare `<Icon icon={IconWifi} />` comes out right with nothing said at the call
|
|
13
|
+
* site — the same cascade a `Button` gives its icons.
|
|
14
|
+
*/
|
|
15
|
+
export function ListGroupItemPrefix({ className, children, ...props }: ListGroupSlotProps): ReactElement {
|
|
16
|
+
const { size } = useListGroupPart("ListGroup.ItemPrefix");
|
|
17
|
+
const slots = listGroupVariants({ size });
|
|
18
|
+
const iconClassName = slots.prefixIcon();
|
|
19
|
+
const iconDefaults = useMemo(
|
|
20
|
+
() => ({ className: iconClassName, color: LIST_GROUP_FOREGROUND_TOKEN }),
|
|
21
|
+
[iconClassName]
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<View className={slots.prefix({ className })} {...props}>
|
|
26
|
+
<IconDefaultsProvider value={iconDefaults}>{children}</IconDefaultsProvider>
|
|
27
|
+
</View>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
ListGroupItemPrefix.displayName = "DelacourUI.ListGroup.ItemPrefix";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Children, type ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { IconChevronRight } from "../../icons/central";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import { Icon, type IconSize } from "../icon";
|
|
6
|
+
import { useListGroupPart } from "./list-group.context";
|
|
7
|
+
import type { ListGroupSlotProps } from "./list-group.types";
|
|
8
|
+
import { LIST_GROUP_SUFFIX_ICON_TOKEN, listGroupVariants } from "./list-group.variants";
|
|
9
|
+
|
|
10
|
+
export type ListGroupIconProps = {
|
|
11
|
+
/** A named size, or an edge length in points. Beats `className`. */
|
|
12
|
+
size?: IconSize | number;
|
|
13
|
+
/** A theme colour token or a literal. Defaults to `muted-foreground`. */
|
|
14
|
+
color?: string;
|
|
15
|
+
/** A `size-*` utility, beating the group's own suffix size. */
|
|
16
|
+
className?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ListGroupItemSuffixProps = ListGroupSlotProps & {
|
|
20
|
+
/** Tunes the default chevron. Ignored once the suffix has children of its own. */
|
|
21
|
+
iconProps?: ListGroupIconProps;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The trailing slot of a row, showing a chevron unless given content.
|
|
26
|
+
*
|
|
27
|
+
* The chevron is a hint that the row leads somewhere, so it sits on the muted
|
|
28
|
+
* token and a step below the leading icon rather than competing with either.
|
|
29
|
+
*/
|
|
30
|
+
export function ListGroupItemSuffix({
|
|
31
|
+
className,
|
|
32
|
+
iconProps,
|
|
33
|
+
children,
|
|
34
|
+
...props
|
|
35
|
+
}: ListGroupItemSuffixProps): ReactElement {
|
|
36
|
+
const { size } = useListGroupPart("ListGroup.ItemSuffix");
|
|
37
|
+
const slots = listGroupVariants({ size });
|
|
38
|
+
const hasContent = Children.toArray(children).length > 0;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View className={slots.suffix({ className })} {...props}>
|
|
42
|
+
{hasContent ? (
|
|
43
|
+
children
|
|
44
|
+
) : (
|
|
45
|
+
<Icon
|
|
46
|
+
className={cn(slots.suffixIcon(), iconProps?.className)}
|
|
47
|
+
color={iconProps?.color ?? LIST_GROUP_SUFFIX_ICON_TOKEN}
|
|
48
|
+
icon={IconChevronRight}
|
|
49
|
+
size={iconProps?.size}
|
|
50
|
+
/>
|
|
51
|
+
)}
|
|
52
|
+
</View>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
ListGroupItemSuffix.displayName = "DelacourUI.ListGroup.ItemSuffix";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Text } from "../text";
|
|
3
|
+
import { useListGroupPart } from "./list-group.context";
|
|
4
|
+
import type { ListGroupTextProps } from "./list-group.types";
|
|
5
|
+
import { listGroupVariants } from "./list-group.variants";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The row's primary line.
|
|
9
|
+
*
|
|
10
|
+
* Carries its own colour and type scale, read from the list group's context: a
|
|
11
|
+
* React Native `View` does not cascade colour to a `Text` descendant.
|
|
12
|
+
*/
|
|
13
|
+
export function ListGroupItemTitle({ className, ...props }: ListGroupTextProps): ReactElement {
|
|
14
|
+
const { size } = useListGroupPart("ListGroup.ItemTitle");
|
|
15
|
+
return <Text className={listGroupVariants({ size }).title({ className })} {...props} />;
|
|
16
|
+
}
|
|
17
|
+
ListGroupItemTitle.displayName = "DelacourUI.ListGroup.ItemTitle";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Children, type ReactElement, type ReactNode, useMemo } from "react";
|
|
2
|
+
import { Pressable, type PressableProps } from "../pressable";
|
|
3
|
+
import { useListGroupPart } from "./list-group.context";
|
|
4
|
+
import { listGroupVariants } from "./list-group.variants";
|
|
5
|
+
import { ListGroupItemContent } from "./list-group-item-content";
|
|
6
|
+
import { ListGroupItemTitle } from "./list-group-item-title";
|
|
7
|
+
|
|
8
|
+
export type ListGroupItemProps = Omit<PressableProps, "children" | "disabled"> & {
|
|
9
|
+
isDisabled?: boolean;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One row of a list group.
|
|
15
|
+
*
|
|
16
|
+
* Plain string or number children are wrapped in a title inside a content
|
|
17
|
+
* column; pass the compound parts when a row needs a prefix, a description or a
|
|
18
|
+
* suffix. React Native cannot render a string outside a `<Text>`, so the wrap is
|
|
19
|
+
* not a convenience — without it `<ListGroup.Item>Wi-Fi</ListGroup.Item>` would
|
|
20
|
+
* crash.
|
|
21
|
+
*
|
|
22
|
+
* A row is a `Pressable`, so `feedback`, `haptic` and the rest come from there
|
|
23
|
+
* rather than being restated here. Only the default differs: `fade`, because a
|
|
24
|
+
* full-bleed row that scales reads as the whole card flexing rather than as one
|
|
25
|
+
* row responding.
|
|
26
|
+
*/
|
|
27
|
+
export function ListGroupItem({
|
|
28
|
+
feedback = "fade",
|
|
29
|
+
isDisabled = false,
|
|
30
|
+
className,
|
|
31
|
+
children,
|
|
32
|
+
...props
|
|
33
|
+
}: ListGroupItemProps): ReactElement {
|
|
34
|
+
const { size } = useListGroupPart("ListGroup.Item");
|
|
35
|
+
const content = useMemo(() => wrapTextChildren(children), [children]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Pressable
|
|
39
|
+
className={listGroupVariants({ isDisabled, size }).item({ className })}
|
|
40
|
+
disabled={isDisabled}
|
|
41
|
+
feedback={feedback}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
{content}
|
|
45
|
+
</Pressable>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
ListGroupItem.displayName = "DelacourUI.ListGroup.Item";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Wraps bare text children in a title inside a content column.
|
|
52
|
+
*
|
|
53
|
+
* Consecutive strings and numbers are collected into a single title rather than
|
|
54
|
+
* one each — `Row {index}` is one piece of text, and wrapping the parts
|
|
55
|
+
* separately would space them apart by the row's gap.
|
|
56
|
+
*
|
|
57
|
+
* Lives here rather than with the root: it is the row that wraps its own text,
|
|
58
|
+
* and importing it from `./list-group` would close a cycle back through the
|
|
59
|
+
* root. See AGENTS.md rule 3.
|
|
60
|
+
*/
|
|
61
|
+
function wrapTextChildren(children: ReactNode): ReactNode {
|
|
62
|
+
const items = Children.toArray(children);
|
|
63
|
+
const output: ReactNode[] = [];
|
|
64
|
+
let run: (string | number)[] = [];
|
|
65
|
+
|
|
66
|
+
const flushRun = () => {
|
|
67
|
+
if (run.length === 0) return;
|
|
68
|
+
output.push(
|
|
69
|
+
<ListGroupItemContent key={`content-${output.length}`}>
|
|
70
|
+
<ListGroupItemTitle>{run.join("")}</ListGroupItemTitle>
|
|
71
|
+
</ListGroupItemContent>
|
|
72
|
+
);
|
|
73
|
+
run = [];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
for (const child of items) {
|
|
77
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
78
|
+
run.push(child);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
flushRun();
|
|
82
|
+
output.push(child);
|
|
83
|
+
}
|
|
84
|
+
flushRun();
|
|
85
|
+
|
|
86
|
+
return output;
|
|
87
|
+
}
|