@delacour/react-native-ui 0.1.0-alpha.20260925053522
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 +193 -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 +434 -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 +242 -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 +246 -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 +150 -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 +105 -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 +293 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +220 -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 +112 -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 +47 -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 +95 -0
- package/src/components/field/field.tsx +157 -0
- package/src/components/field/field.types.ts +15 -0
- package/src/components/field/field.variants.test.ts +240 -0
- package/src/components/field/field.variants.ts +156 -0
- package/src/components/field/index.ts +22 -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 +74 -0
- package/src/components/pressable/index.ts +9 -0
- package/src/components/pressable/pressable.tsx +261 -0
- package/src/components/pressable/pressable.variants.test.ts +128 -0
- package/src/components/pressable/pressable.variants.ts +80 -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 +245 -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 +580 -0
- package/src/components/radio/radio.variants.ts +271 -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 +50 -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 +193 -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 +856 -0
- package/src/components/slider/slider.variants.ts +661 -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 +287 -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 +670 -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,192 @@
|
|
|
1
|
+
import { Children, type ReactElement, type ReactNode, useMemo } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { IconDefaultsProvider } from "../icon";
|
|
4
|
+
import { Pressable, type PressableProps } from "../pressable";
|
|
5
|
+
import { TextClassProvider } from "../text/text.context";
|
|
6
|
+
import { type BadgeContextValue, BadgeProvider } from "./badge.context";
|
|
7
|
+
import {
|
|
8
|
+
BADGE_FOREGROUND_TOKEN,
|
|
9
|
+
type BadgeColor,
|
|
10
|
+
type BadgeSize,
|
|
11
|
+
type BadgeVariant,
|
|
12
|
+
badgeVariants,
|
|
13
|
+
resolveBadgeInteractive,
|
|
14
|
+
} from "./badge.variants";
|
|
15
|
+
import { BadgeCloseButton } from "./badge-close-button";
|
|
16
|
+
import { BadgeEndContent } from "./badge-end-content";
|
|
17
|
+
import { BadgeLabel } from "./badge-label";
|
|
18
|
+
import { BadgeStartContent } from "./badge-start-content";
|
|
19
|
+
|
|
20
|
+
export type BadgeProps = Omit<PressableProps, "asChild" | "busy" | "children" | "disabled"> & {
|
|
21
|
+
/** How the surface is painted. */
|
|
22
|
+
variant?: BadgeVariant;
|
|
23
|
+
/** What the surface means. */
|
|
24
|
+
color?: BadgeColor;
|
|
25
|
+
size?: BadgeSize;
|
|
26
|
+
isDisabled?: boolean;
|
|
27
|
+
/** Composes a trailing dismiss control in. Its press never reaches `onPress`. */
|
|
28
|
+
onClose?: () => void;
|
|
29
|
+
/** Name a screen reader gives the dismiss control. Defaults to `Remove`. */
|
|
30
|
+
closeAccessibilityLabel?: string;
|
|
31
|
+
children?: ReactNode;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function BadgeRoot({
|
|
35
|
+
variant = "solid",
|
|
36
|
+
color = "default",
|
|
37
|
+
size = "md",
|
|
38
|
+
isDisabled = false,
|
|
39
|
+
onClose,
|
|
40
|
+
closeAccessibilityLabel,
|
|
41
|
+
onPress,
|
|
42
|
+
onLongPress,
|
|
43
|
+
haptic,
|
|
44
|
+
feedback = "scale",
|
|
45
|
+
pressedScale,
|
|
46
|
+
pressedOpacity,
|
|
47
|
+
className,
|
|
48
|
+
children,
|
|
49
|
+
...props
|
|
50
|
+
}: BadgeProps): ReactElement {
|
|
51
|
+
const context = useMemo<BadgeContextValue>(
|
|
52
|
+
() => ({ variant, color, size, isDisabled }),
|
|
53
|
+
[variant, color, size, isDisabled]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const slots = badgeVariants({ color, isDisabled, size, variant });
|
|
57
|
+
|
|
58
|
+
// Icons composed into the badge adopt these unless told otherwise.
|
|
59
|
+
const iconClassName = slots.icon();
|
|
60
|
+
const foregroundToken = BADGE_FOREGROUND_TOKEN[variant][color];
|
|
61
|
+
const iconDefaults = useMemo(
|
|
62
|
+
() => ({ className: iconClassName, color: foregroundToken }),
|
|
63
|
+
[iconClassName, foregroundToken]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const content = useMemo(() => wrapTextChildren(children), [children]);
|
|
67
|
+
const rootClassName = slots.root({ className });
|
|
68
|
+
|
|
69
|
+
const inner = (
|
|
70
|
+
<IconDefaultsProvider value={iconDefaults}>
|
|
71
|
+
<TextClassProvider value={slots.label()}>
|
|
72
|
+
{content}
|
|
73
|
+
{onClose ? <BadgeCloseButton accessibilityLabel={closeAccessibilityLabel} onPress={onClose} /> : null}
|
|
74
|
+
</TextClassProvider>
|
|
75
|
+
</IconDefaultsProvider>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// A badge with nothing to do stays a plain box — see `resolveBadgeInteractive`.
|
|
79
|
+
if (!resolveBadgeInteractive({ onPress, onLongPress })) {
|
|
80
|
+
return (
|
|
81
|
+
<BadgeProvider value={context}>
|
|
82
|
+
<View className={rootClassName} {...props}>
|
|
83
|
+
{inner}
|
|
84
|
+
</View>
|
|
85
|
+
</BadgeProvider>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<BadgeProvider value={context}>
|
|
91
|
+
<Pressable
|
|
92
|
+
accessibilityRole="button"
|
|
93
|
+
className={rootClassName}
|
|
94
|
+
disabled={isDisabled}
|
|
95
|
+
feedback={feedback}
|
|
96
|
+
haptic={haptic}
|
|
97
|
+
onLongPress={onLongPress}
|
|
98
|
+
onPress={onPress}
|
|
99
|
+
pressedOpacity={pressedOpacity}
|
|
100
|
+
pressedScale={pressedScale}
|
|
101
|
+
{...props}
|
|
102
|
+
>
|
|
103
|
+
{inner}
|
|
104
|
+
</Pressable>
|
|
105
|
+
</BadgeProvider>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Wraps bare text children in a `Badge.Label`.
|
|
111
|
+
*
|
|
112
|
+
* React Native cannot render a string outside a `<Text>`, so `<Badge>New</Badge>`
|
|
113
|
+
* would otherwise crash. Consecutive strings and numbers are collected into a
|
|
114
|
+
* single label rather than one each — `{count} left` is one piece of text, and
|
|
115
|
+
* wrapping the parts separately would space them apart by the badge's gap.
|
|
116
|
+
*/
|
|
117
|
+
function wrapTextChildren(children: ReactNode): ReactNode {
|
|
118
|
+
const items = Children.toArray(children);
|
|
119
|
+
const output: ReactNode[] = [];
|
|
120
|
+
let run: (string | number)[] = [];
|
|
121
|
+
|
|
122
|
+
const flushRun = () => {
|
|
123
|
+
if (run.length === 0) return;
|
|
124
|
+
output.push(<BadgeLabel key={`label-${output.length}`}>{run.join("")}</BadgeLabel>);
|
|
125
|
+
run = [];
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
for (const child of items) {
|
|
129
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
130
|
+
run.push(child);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
flushRun();
|
|
134
|
+
output.push(child);
|
|
135
|
+
}
|
|
136
|
+
flushRun();
|
|
137
|
+
|
|
138
|
+
return output;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A compact label for status, category or count — composed from parts rather
|
|
143
|
+
* than configured by flags.
|
|
144
|
+
*
|
|
145
|
+
* Two axes describe the surface: `variant` says how it is painted (`solid`,
|
|
146
|
+
* `soft`, `outline`, `ghost`) and `color` says what it means (`default`,
|
|
147
|
+
* `primary`, `success`, `warning`, `destructive`, `info`). They are orthogonal, so
|
|
148
|
+
* every one of the twenty-four pairings is reachable without the library naming
|
|
149
|
+
* each combination.
|
|
150
|
+
*
|
|
151
|
+
* **A badge is content until it is given something to do.** With no `onPress` or
|
|
152
|
+
* `onLongPress` it renders a plain `View` — no gesture detector, and nothing
|
|
153
|
+
* announced to assistive technology as a control. Supply either and the root
|
|
154
|
+
* becomes a `Pressable`, inheriting `feedback`, `haptic` and the rest; only the
|
|
155
|
+
* default differs, `scale`.
|
|
156
|
+
*
|
|
157
|
+
* `onClose` composes a dismiss control in at the end. It is a pressable of its
|
|
158
|
+
* own, so dismissing never also fires `onPress`.
|
|
159
|
+
*
|
|
160
|
+
* Plain string or number children are wrapped in a `Badge.Label` automatically;
|
|
161
|
+
* pass the compound parts when a layout needs more control.
|
|
162
|
+
*
|
|
163
|
+
* Icons are composed in, not passed as props. Anything in the subtree inherits
|
|
164
|
+
* the badge's icon size and its surface's colour, so an `Icon` needs nothing but
|
|
165
|
+
* the glyph. Text works the same way — a bare `<Text>` comes out at the label's
|
|
166
|
+
* colour and type scale without being told.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* <Badge color="success" variant="soft">Active</Badge>
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* <Badge color="warning" variant="outline">
|
|
173
|
+
* <Icon icon={IconStar} />
|
|
174
|
+
* <Badge.Label>Premium</Badge.Label>
|
|
175
|
+
* </Badge>
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* <Badge onClose={() => remove(tag)} onPress={() => filter(tag)}>
|
|
179
|
+
* {tag}
|
|
180
|
+
* </Badge>
|
|
181
|
+
*/
|
|
182
|
+
export const Badge = Object.assign(BadgeRoot, {
|
|
183
|
+
/** The badge's text. Picks its colour and type scale from the badge's variant, colour and size. */
|
|
184
|
+
Label: BadgeLabel,
|
|
185
|
+
/** A centred wrapper for leading content that is not an `Icon` — a status dot, an avatar. */
|
|
186
|
+
StartContent: BadgeStartContent,
|
|
187
|
+
/** A centred wrapper for trailing content that is not an `Icon`. */
|
|
188
|
+
EndContent: BadgeEndContent,
|
|
189
|
+
/** The trailing dismiss control. Composed in automatically whenever `onClose` is set. */
|
|
190
|
+
CloseButton: BadgeCloseButton,
|
|
191
|
+
displayName: "DelacourUI.Badge",
|
|
192
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ViewProps } from "react-native";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The shape of a badge's leading and trailing slots.
|
|
5
|
+
*
|
|
6
|
+
* Shared by `Badge.StartContent` and `Badge.EndContent`, which are identical
|
|
7
|
+
* apart from where the root places them, so it lives in a leaf rather than in
|
|
8
|
+
* one of the two files arbitrarily.
|
|
9
|
+
*/
|
|
10
|
+
export type BadgeSlotProps = ViewProps & { className?: string };
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { declaredTokens } from "../../styles/theme-tokens.test";
|
|
3
|
+
import { ICON_SIZE_TOKENS } from "../../styles/tokens";
|
|
4
|
+
import {
|
|
5
|
+
BADGE_COLORS,
|
|
6
|
+
BADGE_FOREGROUND_TOKEN,
|
|
7
|
+
BADGE_SIZES,
|
|
8
|
+
BADGE_VARIANTS,
|
|
9
|
+
badgeVariants,
|
|
10
|
+
resolveBadgeInteractive,
|
|
11
|
+
} from "./badge.variants";
|
|
12
|
+
|
|
13
|
+
const LIGHT = declaredTokens("light");
|
|
14
|
+
const DARK = declaredTokens("dark");
|
|
15
|
+
|
|
16
|
+
/** Tailwind's spacing step a class string sets its horizontal padding from. */
|
|
17
|
+
function paddingX(cls: string): number {
|
|
18
|
+
return Number(cls.match(/\bpx-([\d.]+)\b/)?.[1]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Tailwind's spacing step a class string sets its vertical padding from. */
|
|
22
|
+
function paddingY(cls: string): number {
|
|
23
|
+
return Number(cls.match(/\bpy-([\d.]+)\b/)?.[1]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Tailwind's spacing step a class string sets its gap from. */
|
|
27
|
+
function gapOf(cls: string): number {
|
|
28
|
+
return Number(cls.match(/\bgap-([\d.]+)\b/)?.[1]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Position of a label's font size on the type scale, largest last.
|
|
33
|
+
*
|
|
34
|
+
* Compares by step rather than by points so the test says what it means and
|
|
35
|
+
* survives a size being retuned in `tokens.css`.
|
|
36
|
+
*/
|
|
37
|
+
const TEXT_SCALE = ["text-xs", "text-sm", "text-base", "text-lg"] as const;
|
|
38
|
+
function textStep(cls: string): number {
|
|
39
|
+
return TEXT_SCALE.findIndex((step) => new RegExp(`\\b${step}\\b`).test(cls));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Position of a slot's `size-icon-*` token on the shared icon scale.
|
|
44
|
+
*
|
|
45
|
+
* The same helper `button.variants.test.ts` uses — a badge's glyph has to sit on
|
|
46
|
+
* the scale `Icon` and `Spinner` share, not carry a private number.
|
|
47
|
+
*/
|
|
48
|
+
function iconStep(cls: string): number {
|
|
49
|
+
const token = cls.match(/\bsize-(icon-[\w-]+)\b/)?.[1];
|
|
50
|
+
return ICON_SIZE_TOKENS.indexOf(token as (typeof ICON_SIZE_TOKENS)[number]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The `text-*` colour token a label class string resolves to, minus the prefix. */
|
|
54
|
+
function labelColorToken(cls: string): string | undefined {
|
|
55
|
+
return cls.match(/\btext-((?:[a-z]+-)*(?:foreground|background))\b/)?.[1];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("the theme.css reader", () => {
|
|
59
|
+
// The token assertions below are only worth anything if the parse found something.
|
|
60
|
+
test("finds both variants", () => {
|
|
61
|
+
expect(LIGHT.size).toBeGreaterThan(0);
|
|
62
|
+
expect(DARK.size).toBeGreaterThan(0);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("badgeVariants root slot", () => {
|
|
67
|
+
test("defaults to the solid default md badge", () => {
|
|
68
|
+
const cls = badgeVariants().root();
|
|
69
|
+
expect(cls).toContain("bg-secondary");
|
|
70
|
+
expect(cls).toContain("px-2.5");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// A badge is sized by its content. Inside a gap column every child is
|
|
74
|
+
// stretch-aligned by default, so without this a one-word badge spans the
|
|
75
|
+
// whole screen.
|
|
76
|
+
test("never stretches, at every size", () => {
|
|
77
|
+
for (const size of BADGE_SIZES) {
|
|
78
|
+
expect(badgeVariants({ size }).root()).toContain("self-start");
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("is a capsule at every size", () => {
|
|
83
|
+
for (const size of BADGE_SIZES) {
|
|
84
|
+
expect(badgeVariants({ size }).root()).toContain("rounded-full");
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// The border is always in the box, transparent until `outline` colours it.
|
|
89
|
+
// Declaring it only on `outline` would make the badge two points wider the
|
|
90
|
+
// moment a caller switched variant.
|
|
91
|
+
test("reserves the border on every variant, so outline does not resize the box", () => {
|
|
92
|
+
for (const variant of BADGE_VARIANTS) {
|
|
93
|
+
expect(badgeVariants({ variant }).root()).toMatch(/\bborder\b/);
|
|
94
|
+
}
|
|
95
|
+
expect(badgeVariants({ variant: "solid" }).root()).toContain("border-transparent");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("clips, so a pressed badge fades to the edge of its own box", () => {
|
|
99
|
+
expect(badgeVariants().root()).toContain("overflow-hidden");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("only outline names a border colour", () => {
|
|
103
|
+
for (const color of BADGE_COLORS) {
|
|
104
|
+
expect(badgeVariants({ variant: "outline", color }).root()).not.toContain("border-transparent");
|
|
105
|
+
for (const variant of ["solid", "soft", "ghost"] as const) {
|
|
106
|
+
expect(badgeVariants({ variant, color }).root()).toContain("border-transparent");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("ghost paints no background, solid and soft do", () => {
|
|
112
|
+
for (const color of BADGE_COLORS) {
|
|
113
|
+
expect(badgeVariants({ variant: "ghost", color }).root()).toContain("bg-transparent");
|
|
114
|
+
expect(badgeVariants({ variant: "outline", color }).root()).toContain("bg-transparent");
|
|
115
|
+
expect(badgeVariants({ variant: "solid", color }).root()).not.toContain("bg-transparent");
|
|
116
|
+
expect(badgeVariants({ variant: "soft", color }).root()).not.toContain("bg-transparent");
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Every cell of the matrix has to be reachable. Two cells collapsing means a
|
|
121
|
+
// caller can set an axis and see nothing change.
|
|
122
|
+
test("gives all twenty-four variant and colour pairs a distinct treatment", () => {
|
|
123
|
+
const seen = new Set<string>();
|
|
124
|
+
|
|
125
|
+
for (const variant of BADGE_VARIANTS) {
|
|
126
|
+
for (const color of BADGE_COLORS) {
|
|
127
|
+
const slots = badgeVariants({ variant, color });
|
|
128
|
+
seen.add(`${slots.root()}|${slots.label()}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
expect(seen.size).toBe(BADGE_VARIANTS.length * BADGE_COLORS.length);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("adds the disabled treatment only when disabled", () => {
|
|
136
|
+
expect(badgeVariants({ isDisabled: true }).root()).toContain("opacity-50");
|
|
137
|
+
expect(badgeVariants({ isDisabled: false }).root()).not.toContain("opacity-50");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("merges an incoming className last", () => {
|
|
141
|
+
expect(badgeVariants().root({ className: "bg-info" })).toContain("bg-info");
|
|
142
|
+
expect(badgeVariants().root({ className: "bg-info" })).not.toContain("bg-secondary");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("badgeVariants size axis", () => {
|
|
147
|
+
test("scales horizontal padding, vertical padding and gap together", () => {
|
|
148
|
+
const roots = BADGE_SIZES.map((size) => badgeVariants({ size }).root());
|
|
149
|
+
const x = roots.map(paddingX);
|
|
150
|
+
const y = roots.map(paddingY);
|
|
151
|
+
const gaps = roots.map(gapOf);
|
|
152
|
+
|
|
153
|
+
for (const scale of [x, y, gaps]) {
|
|
154
|
+
expect(scale.every(Number.isFinite)).toBe(true);
|
|
155
|
+
expect(new Set(scale).size).toBe(BADGE_SIZES.length);
|
|
156
|
+
expect([...scale]).toEqual([...scale].sort((a, b) => a - b));
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Padding, never a height. `Text` respects OS font scaling, and a fixed
|
|
161
|
+
// height clips the label at a large accessibility step instead of growing
|
|
162
|
+
// with it. Unlike `h-button-*`, a badge lines up against no chrome.
|
|
163
|
+
test("takes no height, so the label grows with OS font scaling", () => {
|
|
164
|
+
for (const size of BADGE_SIZES) {
|
|
165
|
+
expect(badgeVariants({ size }).root()).not.toMatch(/\bh-/);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("badgeVariants label slot", () => {
|
|
171
|
+
test("carries the text colour, which the root must not", () => {
|
|
172
|
+
for (const variant of BADGE_VARIANTS) {
|
|
173
|
+
for (const color of BADGE_COLORS) {
|
|
174
|
+
const slots = badgeVariants({ variant, color });
|
|
175
|
+
expect(labelColorToken(slots.label())).toBeDefined();
|
|
176
|
+
expect(slots.root()).not.toMatch(/\btext-(?!center\b)/);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("pairs every solid surface with its own foreground", () => {
|
|
182
|
+
for (const color of ["success", "warning", "destructive", "info"] as const) {
|
|
183
|
+
expect(badgeVariants({ variant: "solid", color }).label()).toContain(`text-${color}-foreground`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("pairs every soft surface with its own soft foreground", () => {
|
|
188
|
+
for (const color of ["success", "warning", "destructive", "info"] as const) {
|
|
189
|
+
const slots = badgeVariants({ variant: "soft", color });
|
|
190
|
+
expect(slots.root()).toContain(`bg-${color}-soft`);
|
|
191
|
+
expect(slots.label()).toContain(`text-${color}-soft-foreground`);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("scales label text with size", () => {
|
|
196
|
+
const steps = BADGE_SIZES.map((size) => textStep(badgeVariants({ size }).label()));
|
|
197
|
+
expect(steps.every((step) => step >= 0)).toBe(true);
|
|
198
|
+
expect([...steps]).toEqual([...steps].sort((a, b) => a - b));
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("is medium weight at every size", () => {
|
|
202
|
+
for (const size of BADGE_SIZES) {
|
|
203
|
+
expect(badgeVariants({ size }).label()).toContain("font-medium");
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("badgeVariants icon slot", () => {
|
|
209
|
+
test("gives every size a distinct icon token, increasing with it", () => {
|
|
210
|
+
const steps = BADGE_SIZES.map((size) => iconStep(badgeVariants({ size }).icon()));
|
|
211
|
+
expect(steps.every((step) => step >= 0)).toBe(true);
|
|
212
|
+
expect(new Set(steps).size).toBe(BADGE_SIZES.length);
|
|
213
|
+
expect([...steps]).toEqual([...steps].sort((a, b) => a - b));
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Colour arrives through BADGE_FOREGROUND_TOKEN, not through this class —
|
|
217
|
+
// an SVG paint prop cannot be reached by a utility.
|
|
218
|
+
test("carries no colour", () => {
|
|
219
|
+
for (const size of BADGE_SIZES) {
|
|
220
|
+
expect(badgeVariants({ size }).icon()).not.toMatch(/\btext-/);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("BADGE_FOREGROUND_TOKEN", () => {
|
|
226
|
+
test("names a token for every variant and colour pair", () => {
|
|
227
|
+
for (const variant of BADGE_VARIANTS) {
|
|
228
|
+
for (const color of BADGE_COLORS) {
|
|
229
|
+
expect(BADGE_FOREGROUND_TOKEN[variant][color]).toBeTruthy();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// A composed Icon and the label sit side by side. Resolving them from two
|
|
235
|
+
// maps that can drift is how a badge ends up with a grey glyph beside white
|
|
236
|
+
// text on a coloured surface.
|
|
237
|
+
test("matches the colour its own label slot resolves to", () => {
|
|
238
|
+
for (const variant of BADGE_VARIANTS) {
|
|
239
|
+
for (const color of BADGE_COLORS) {
|
|
240
|
+
const label = badgeVariants({ variant, color }).label();
|
|
241
|
+
expect(labelColorToken(label)).toBe(BADGE_FOREGROUND_TOKEN[variant][color]);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("names only tokens both themes declare", () => {
|
|
247
|
+
for (const variant of BADGE_VARIANTS) {
|
|
248
|
+
for (const color of BADGE_COLORS) {
|
|
249
|
+
const token = BADGE_FOREGROUND_TOKEN[variant][color];
|
|
250
|
+
expect(LIGHT.has(token)).toBe(true);
|
|
251
|
+
expect(DARK.has(token)).toBe(true);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("resolveBadgeInteractive", () => {
|
|
258
|
+
const noop = () => {};
|
|
259
|
+
|
|
260
|
+
test("a badge with no handler stays a plain view", () => {
|
|
261
|
+
expect(resolveBadgeInteractive({})).toBe(false);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("either press handler upgrades it to a pressable", () => {
|
|
265
|
+
expect(resolveBadgeInteractive({ onPress: noop })).toBe(true);
|
|
266
|
+
expect(resolveBadgeInteractive({ onLongPress: noop })).toBe(true);
|
|
267
|
+
expect(resolveBadgeInteractive({ onPress: noop, onLongPress: noop })).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
});
|