@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,24 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type { TextProps } from "react-native";
|
|
3
|
+
import { Text } from "../text";
|
|
4
|
+
import { useButtonPart } from "./button.context";
|
|
5
|
+
import { buttonVariants } from "./button.variants";
|
|
6
|
+
|
|
7
|
+
export type ButtonLabelProps = TextProps & { className?: string };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The button's text.
|
|
11
|
+
*
|
|
12
|
+
* Carries its own colour and type scale, read from the button's context: a
|
|
13
|
+
* React Native `View` does not cascade colour to a `Text` descendant, so a
|
|
14
|
+
* colour set on the root would be lost.
|
|
15
|
+
*
|
|
16
|
+
* The class is passed rather than inherited even though the button publishes the
|
|
17
|
+
* same one through `TextClassProvider` — the two agree, and stating it here
|
|
18
|
+
* keeps the label correct if it is ever rendered somewhere the provider is not.
|
|
19
|
+
*/
|
|
20
|
+
export function ButtonLabel({ className, ...props }: ButtonLabelProps): ReactElement {
|
|
21
|
+
const { variant, size } = useButtonPart("Button.Label");
|
|
22
|
+
return <Text className={buttonVariants({ size, variant }).label({ className })} {...props} />;
|
|
23
|
+
}
|
|
24
|
+
ButtonLabel.displayName = "DelacourUI.Button.Label";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import type { ButtonSlotProps } from "./button.types";
|
|
4
|
+
import { buttonVariants } from "./button.variants";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A centred wrapper for leading content that is not an `Icon`.
|
|
8
|
+
*
|
|
9
|
+
* An `Icon` needs no wrapper — it inherits the button's icon size and its
|
|
10
|
+
* variant's colour from the root's `IconDefaultsProvider`.
|
|
11
|
+
*/
|
|
12
|
+
export function ButtonStartContent({ className, ...props }: ButtonSlotProps): ReactElement {
|
|
13
|
+
return <View className={buttonVariants().startContent({ className })} {...props} />;
|
|
14
|
+
}
|
|
15
|
+
ButtonStartContent.displayName = "DelacourUI.Button.StartContent";
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { createContext, type ReactElement, type ReactNode, use } from "react";
|
|
2
|
+
import type { PressableFeedback } from "../pressable/pressable.variants";
|
|
3
|
+
import type { ButtonGroupOrientation, ButtonGroupPosition, ButtonSize, ButtonVariant } from "./button.variants";
|
|
4
|
+
|
|
5
|
+
export type ButtonContextValue = {
|
|
6
|
+
/** Size of the button. */
|
|
7
|
+
size: ButtonSize;
|
|
8
|
+
/** Visual variant of the button. */
|
|
9
|
+
variant: ButtonVariant;
|
|
10
|
+
/** Whether the button is disabled. */
|
|
11
|
+
isDisabled: boolean;
|
|
12
|
+
/** Whether the button has work in flight. */
|
|
13
|
+
isLoading: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ButtonContext = createContext<ButtonContextValue | null>(null);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Supplies the enclosing button's variant, size and state to its subtree.
|
|
20
|
+
*
|
|
21
|
+
* Lives in its own module, importing nothing but `button.variants`, so a
|
|
22
|
+
* component the button itself renders — `Spinner` — can read it without
|
|
23
|
+
* importing `../button`. That import would close a cycle through
|
|
24
|
+
* `button.tsx`, and Metro serves a partially initialised module for a cycle,
|
|
25
|
+
* leaving the context `undefined` at import time.
|
|
26
|
+
*/
|
|
27
|
+
export function ButtonProvider({ value, children }: { value: ButtonContextValue; children: ReactNode }): ReactElement {
|
|
28
|
+
return <ButtonContext value={value}>{children}</ButtonContext>;
|
|
29
|
+
}
|
|
30
|
+
ButtonProvider.displayName = "DelacourUI.Button.Provider";
|
|
31
|
+
|
|
32
|
+
/** The enclosing button's context, or null outside a `<Button>`. */
|
|
33
|
+
export function useButtonContext(): ButtonContextValue | null {
|
|
34
|
+
return use(ButtonContext);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Reads the enclosing button's variant, size and state.
|
|
39
|
+
*
|
|
40
|
+
* Lets a custom child style itself to match without the button having to pass
|
|
41
|
+
* props down through every slot. Throws outside a `<Button>` — use
|
|
42
|
+
* {@link useButtonContext} where the enclosing button is optional.
|
|
43
|
+
*/
|
|
44
|
+
export function useButton(): ButtonContextValue {
|
|
45
|
+
const context = useButtonContext();
|
|
46
|
+
if (!context) {
|
|
47
|
+
throw new Error("useButton must be called inside a <Button>.");
|
|
48
|
+
}
|
|
49
|
+
return context;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The enclosing button's context, for a compound part that cannot work without
|
|
54
|
+
* one.
|
|
55
|
+
*
|
|
56
|
+
* Internal: deliberately not re-exported from `index.ts`. A caller outside the
|
|
57
|
+
* library wants {@link useButton}, whose error message names the hook rather
|
|
58
|
+
* than a part.
|
|
59
|
+
*/
|
|
60
|
+
export function useButtonPart(component: string): ButtonContextValue {
|
|
61
|
+
const context = useButtonContext();
|
|
62
|
+
if (!context) {
|
|
63
|
+
throw new Error(`${component} must be rendered inside a <Button>.`);
|
|
64
|
+
}
|
|
65
|
+
return context;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type ButtonGroupContextValue = {
|
|
69
|
+
/** Which way the group runs. Members square the pair of corners crossing it. */
|
|
70
|
+
orientation: ButtonGroupOrientation;
|
|
71
|
+
/** The group owns this outright — members of different heights do not join. */
|
|
72
|
+
size: ButtonSize;
|
|
73
|
+
/** A default a member may override. */
|
|
74
|
+
variant?: ButtonVariant;
|
|
75
|
+
/** A default a member may override, so one option can disable itself. */
|
|
76
|
+
isDisabled?: boolean;
|
|
77
|
+
/** Press treatment for members. Unset falls back to `fade` — see `resolveButtonFeedback`. */
|
|
78
|
+
feedback?: PressableFeedback;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type ButtonGroupItemContextValue = ButtonGroupContextValue & {
|
|
82
|
+
/** Where this member sits among the group's members, separators skipped. */
|
|
83
|
+
position: ButtonGroupPosition;
|
|
84
|
+
/** Overlap the member before it, so the shared edge is drawn once. */
|
|
85
|
+
isSeamed: boolean;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const ButtonGroupContext = createContext<ButtonGroupContextValue | null>(null);
|
|
89
|
+
|
|
90
|
+
const ButtonGroupItemContext = createContext<ButtonGroupItemContextValue | null>(null);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Supplies the enclosing group's axes to everything inside it.
|
|
94
|
+
*
|
|
95
|
+
* A second context beside {@link ButtonGroupItemProvider} rather than a field on
|
|
96
|
+
* it, because `Button.Group.Separator` needs the orientation and is deliberately
|
|
97
|
+
* *not* a member — so it has no item context to read one from.
|
|
98
|
+
*
|
|
99
|
+
* The optional axes are published raw. `undefined` means "the group said
|
|
100
|
+
* nothing", which is what lets a member's `??` ladder see past a group that
|
|
101
|
+
* named none, and what lets one member disable itself inside a group that did
|
|
102
|
+
* not.
|
|
103
|
+
*/
|
|
104
|
+
export function ButtonGroupProvider({
|
|
105
|
+
value,
|
|
106
|
+
children,
|
|
107
|
+
}: {
|
|
108
|
+
value: ButtonGroupContextValue;
|
|
109
|
+
children: ReactNode;
|
|
110
|
+
}): ReactElement {
|
|
111
|
+
return <ButtonGroupContext value={value}>{children}</ButtonGroupContext>;
|
|
112
|
+
}
|
|
113
|
+
ButtonGroupProvider.displayName = "DelacourUI.Button.Group.Provider";
|
|
114
|
+
|
|
115
|
+
/** The enclosing group's axes, or null outside a `<Button.Group>`. */
|
|
116
|
+
export function useButtonGroupContext(): ButtonGroupContextValue | null {
|
|
117
|
+
return use(ButtonGroupContext);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Reads the enclosing group's axes.
|
|
122
|
+
*
|
|
123
|
+
* Throws outside a `<Button.Group>` — use {@link useButtonGroupContext} where
|
|
124
|
+
* the enclosing group is optional, as a `Button` itself does.
|
|
125
|
+
*/
|
|
126
|
+
export function useButtonGroup(): ButtonGroupContextValue {
|
|
127
|
+
const context = useButtonGroupContext();
|
|
128
|
+
if (!context) {
|
|
129
|
+
throw new Error("useButtonGroup must be called inside a <Button.Group>.");
|
|
130
|
+
}
|
|
131
|
+
return context;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The enclosing group's axes, for a compound part that cannot work without one.
|
|
136
|
+
*
|
|
137
|
+
* Internal: deliberately not re-exported from `index.ts`. A caller outside the
|
|
138
|
+
* library wants {@link useButtonGroup}, whose error message names the hook
|
|
139
|
+
* rather than a part.
|
|
140
|
+
*/
|
|
141
|
+
export function useButtonGroupPart(component: string): ButtonGroupContextValue {
|
|
142
|
+
const context = useButtonGroupContext();
|
|
143
|
+
if (!context) {
|
|
144
|
+
throw new Error(`${component} must be rendered inside a <Button.Group>.`);
|
|
145
|
+
}
|
|
146
|
+
return context;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Supplies one member its place in the group.
|
|
151
|
+
*
|
|
152
|
+
* The group wraps each member in one of these rather than cloning props onto it.
|
|
153
|
+
* A provider is not a host component, so no view is created and Yoga sees
|
|
154
|
+
* exactly the children the group's own `View` already had — while a cloned prop
|
|
155
|
+
* would reach only a direct child, and never one a caller wrapped in a `View` or
|
|
156
|
+
* produced from a helper.
|
|
157
|
+
*/
|
|
158
|
+
export function ButtonGroupItemProvider({
|
|
159
|
+
value,
|
|
160
|
+
children,
|
|
161
|
+
}: {
|
|
162
|
+
value: ButtonGroupItemContextValue;
|
|
163
|
+
children: ReactNode;
|
|
164
|
+
}): ReactElement {
|
|
165
|
+
return <ButtonGroupItemContext value={value}>{children}</ButtonGroupItemContext>;
|
|
166
|
+
}
|
|
167
|
+
ButtonGroupItemProvider.displayName = "DelacourUI.Button.Group.ItemProvider";
|
|
168
|
+
|
|
169
|
+
/** This member's place in the group, or null for a control standing on its own. */
|
|
170
|
+
export function useButtonGroupItemContext(): ButtonGroupItemContextValue | null {
|
|
171
|
+
return use(ButtonGroupItemContext);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Reads this member's place in the group.
|
|
176
|
+
*
|
|
177
|
+
* For a custom control that has to join a group the way a `Button` does. Throws
|
|
178
|
+
* outside one — use {@link useButtonGroupItemContext} where the group is
|
|
179
|
+
* optional, as the button itself does.
|
|
180
|
+
*/
|
|
181
|
+
export function useButtonGroupItem(): ButtonGroupItemContextValue {
|
|
182
|
+
const context = useButtonGroupItemContext();
|
|
183
|
+
if (!context) {
|
|
184
|
+
throw new Error("useButtonGroupItem must be called inside a <Button.Group>.");
|
|
185
|
+
}
|
|
186
|
+
return context;
|
|
187
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { Children, isValidElement, type ReactElement, type ReactNode, useMemo } from "react";
|
|
2
|
+
import { Icon, IconDefaultsProvider } from "../icon";
|
|
3
|
+
import { Pressable, type PressableProps } from "../pressable";
|
|
4
|
+
import { Spinner } from "../spinner";
|
|
5
|
+
import { TextClassProvider } from "../text/text.context";
|
|
6
|
+
import { type ButtonContextValue, ButtonProvider, useButtonGroupItemContext } from "./button.context";
|
|
7
|
+
import {
|
|
8
|
+
BUTTON_FOREGROUND_TOKEN,
|
|
9
|
+
type ButtonLayout,
|
|
10
|
+
type ButtonSize,
|
|
11
|
+
type ButtonSpinnerPlacement,
|
|
12
|
+
type ButtonVariant,
|
|
13
|
+
buttonVariants,
|
|
14
|
+
resolveButtonFeedback,
|
|
15
|
+
resolveButtonLayout,
|
|
16
|
+
resolveGroupedButtonSize,
|
|
17
|
+
resolveSpinnerSwapIndex,
|
|
18
|
+
} from "./button.variants";
|
|
19
|
+
import { ButtonEndContent } from "./button-end-content";
|
|
20
|
+
import { ButtonGroup } from "./button-group";
|
|
21
|
+
import { ButtonLabel } from "./button-label";
|
|
22
|
+
import { ButtonStartContent } from "./button-start-content";
|
|
23
|
+
|
|
24
|
+
export type ButtonProps = Omit<PressableProps, "busy" | "children" | "disabled" | "pressedOpacity" | "pressedScale"> & {
|
|
25
|
+
variant?: ButtonVariant;
|
|
26
|
+
/** Height, label step, icon step, corner and padding on one axis. `icon-*` is a square. */
|
|
27
|
+
size?: ButtonSize;
|
|
28
|
+
isDisabled?: boolean;
|
|
29
|
+
/** Work is in flight: a spinner is composed in and presses are blocked. */
|
|
30
|
+
isLoading?: boolean;
|
|
31
|
+
/** Where the spinner sits. `only` replaces the content, keeping the footprint. */
|
|
32
|
+
spinnerPlacement?: ButtonSpinnerPlacement;
|
|
33
|
+
/** Fade the button while loading, the way `isDisabled` does. Off by default. */
|
|
34
|
+
isDimmedWhileLoading?: boolean;
|
|
35
|
+
children?: ReactNode;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function ButtonRoot({
|
|
39
|
+
variant,
|
|
40
|
+
size,
|
|
41
|
+
isDisabled,
|
|
42
|
+
isLoading = false,
|
|
43
|
+
isDimmedWhileLoading = false,
|
|
44
|
+
spinnerPlacement = "start",
|
|
45
|
+
feedback,
|
|
46
|
+
accessibilityLabel,
|
|
47
|
+
className,
|
|
48
|
+
children,
|
|
49
|
+
...props
|
|
50
|
+
}: ButtonProps): ReactElement {
|
|
51
|
+
// Deliberately not defaulted in the destructure above: a default there would
|
|
52
|
+
// swallow the group before it was ever consulted. The group owns a member's
|
|
53
|
+
// *step* — controls of different heights do not join — but not its shape, so
|
|
54
|
+
// a square member keeps its width inside a run. `variant` and `isDisabled`
|
|
55
|
+
// are defaults it may override, so one member can disable itself inside a
|
|
56
|
+
// group that did not.
|
|
57
|
+
const item = useButtonGroupItemContext();
|
|
58
|
+
const resolvedSize = resolveGroupedButtonSize(size, item?.size);
|
|
59
|
+
const resolvedVariant = variant ?? item?.variant ?? "primary";
|
|
60
|
+
const resolvedIsDisabled = isDisabled ?? item?.isDisabled ?? false;
|
|
61
|
+
|
|
62
|
+
const context = useMemo<ButtonContextValue>(
|
|
63
|
+
() => ({ variant: resolvedVariant, size: resolvedSize, isDisabled: resolvedIsDisabled, isLoading }),
|
|
64
|
+
[resolvedVariant, resolvedSize, resolvedIsDisabled, isLoading]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const layout = useMemo(() => resolveButtonLayout({ isLoading, spinnerPlacement }), [isLoading, spinnerPlacement]);
|
|
68
|
+
|
|
69
|
+
const content = useMemo(() => composeContent(children, layout), [children, layout]);
|
|
70
|
+
|
|
71
|
+
const slots = buttonVariants({
|
|
72
|
+
groupPosition: item?.position ?? "none",
|
|
73
|
+
isDimmedWhileLoading,
|
|
74
|
+
isDisabled: resolvedIsDisabled,
|
|
75
|
+
isLoading,
|
|
76
|
+
isSeamed: item?.isSeamed ?? false,
|
|
77
|
+
orientation: item?.orientation ?? "horizontal",
|
|
78
|
+
size: resolvedSize,
|
|
79
|
+
variant: resolvedVariant,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Icons and spinners composed into the button adopt these unless told otherwise.
|
|
83
|
+
const iconClassName = slots.icon();
|
|
84
|
+
const iconDefaults = useMemo(
|
|
85
|
+
() => ({ className: iconClassName, color: BUTTON_FOREGROUND_TOKEN[resolvedVariant] }),
|
|
86
|
+
[iconClassName, resolvedVariant]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const label = accessibilityLabel ?? (layout.isSpinnerOnly ? textOf(children) : undefined);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<ButtonProvider value={context}>
|
|
93
|
+
<Pressable
|
|
94
|
+
accessibilityLabel={label}
|
|
95
|
+
accessibilityRole="button"
|
|
96
|
+
busy={isLoading}
|
|
97
|
+
className={slots.root({ className })}
|
|
98
|
+
disabled={resolvedIsDisabled}
|
|
99
|
+
feedback={resolveButtonFeedback(feedback, item?.feedback, item !== null)}
|
|
100
|
+
{...props}
|
|
101
|
+
>
|
|
102
|
+
<IconDefaultsProvider value={iconDefaults}>
|
|
103
|
+
<TextClassProvider value={slots.label()}>{content}</TextClassProvider>
|
|
104
|
+
</IconDefaultsProvider>
|
|
105
|
+
</Pressable>
|
|
106
|
+
</ButtonProvider>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Composes the spinner into the button's children.
|
|
112
|
+
*
|
|
113
|
+
* A loading button **replaces** its icon rather than showing both. Adding a
|
|
114
|
+
* spinner alongside would push the label sideways the moment work started and
|
|
115
|
+
* pull it back when it finished; swapping costs no layout at all, because the
|
|
116
|
+
* spinner and the icon are drawn at the same `size-icon-*` token — the button
|
|
117
|
+
* publishes one class and both read it.
|
|
118
|
+
*
|
|
119
|
+
* Only the icon at the named edge is swapped — the first child at `start`, the
|
|
120
|
+
* last at `end`. An icon on the other side stays where it is and the spinner is
|
|
121
|
+
* inserted at the edge instead, spaced off the label by the root's own `gap`:
|
|
122
|
+
* `spinnerPlacement` is where the caller wants the spinner, and a button
|
|
123
|
+
* holding one leading icon must not answer `end` by spinning at the start.
|
|
124
|
+
*
|
|
125
|
+
* `only` drops the children: the spinner stands in for the whole content, and
|
|
126
|
+
* the button keeps whatever footprint its `size` gave it.
|
|
127
|
+
*
|
|
128
|
+
* The spinner is emitted bare — it reads its size and colour from the button's
|
|
129
|
+
* context, so nothing is passed here.
|
|
130
|
+
*/
|
|
131
|
+
function composeContent(children: ReactNode, layout: ButtonLayout): ReactNode {
|
|
132
|
+
if (layout.isSpinnerOnly) return <Spinner />;
|
|
133
|
+
if (layout.spinnerSide === null) return wrapTextChildren(children);
|
|
134
|
+
|
|
135
|
+
const items = Children.toArray(wrapTextChildren(children));
|
|
136
|
+
const swapAt = resolveSpinnerSwapIndex(items.map(isIcon), layout.spinnerSide);
|
|
137
|
+
|
|
138
|
+
if (swapAt !== null) {
|
|
139
|
+
return items.map((child, index) => (index === swapAt ? <Spinner key="spinner" /> : child));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return layout.spinnerSide === "start" ? (
|
|
143
|
+
<>
|
|
144
|
+
<Spinner />
|
|
145
|
+
{items}
|
|
146
|
+
</>
|
|
147
|
+
) : (
|
|
148
|
+
<>
|
|
149
|
+
{items}
|
|
150
|
+
<Spinner />
|
|
151
|
+
</>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Whether a child is an `Icon` the spinner can stand in for.
|
|
157
|
+
*
|
|
158
|
+
* Only a bare `Icon` qualifies. A `Button.StartContent` wraps arbitrary content
|
|
159
|
+
* whose height the button does not know, so replacing one could resize the
|
|
160
|
+
* button — the thing this swap exists to avoid.
|
|
161
|
+
*/
|
|
162
|
+
function isIcon(child: ReactNode): boolean {
|
|
163
|
+
return isValidElement(child) && child.type === Icon;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The button's own text, for use as an accessibility label.
|
|
168
|
+
*
|
|
169
|
+
* `spinnerPlacement="only"` takes the label out of the tree, so the name a
|
|
170
|
+
* screen reader was reading has to be carried onto the root instead. Reaches
|
|
171
|
+
* one level into a `Button.Label`; anything more deeply nested needs an
|
|
172
|
+
* explicit `accessibilityLabel`.
|
|
173
|
+
*/
|
|
174
|
+
function textOf(children: ReactNode): string | undefined {
|
|
175
|
+
const parts: string[] = [];
|
|
176
|
+
|
|
177
|
+
for (const child of Children.toArray(children)) {
|
|
178
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
179
|
+
parts.push(String(child));
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!isValidElement(child) || child.type !== ButtonLabel) continue;
|
|
183
|
+
const nested = (child.props as { children?: ReactNode }).children;
|
|
184
|
+
if (typeof nested === "string" || typeof nested === "number") parts.push(String(nested));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return parts.length > 0 ? parts.join("") : undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Wraps bare text children in a `Button.Label`.
|
|
192
|
+
*
|
|
193
|
+
* React Native cannot render a string outside a `<Text>`, so `<Button>Save</Button>`
|
|
194
|
+
* would otherwise crash. Consecutive strings and numbers are collected into a
|
|
195
|
+
* single label rather than one each — `Row {index}` is one piece of text, and
|
|
196
|
+
* wrapping the parts separately would space them apart by the button's gap.
|
|
197
|
+
*/
|
|
198
|
+
function wrapTextChildren(children: ReactNode): ReactNode {
|
|
199
|
+
const items = Children.toArray(children);
|
|
200
|
+
const output: ReactNode[] = [];
|
|
201
|
+
let run: (string | number)[] = [];
|
|
202
|
+
|
|
203
|
+
const flushRun = () => {
|
|
204
|
+
if (run.length === 0) return;
|
|
205
|
+
output.push(<ButtonLabel key={`label-${output.length}`}>{run.join("")}</ButtonLabel>);
|
|
206
|
+
run = [];
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
for (const child of items) {
|
|
210
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
211
|
+
run.push(child);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
flushRun();
|
|
215
|
+
output.push(child);
|
|
216
|
+
}
|
|
217
|
+
flushRun();
|
|
218
|
+
|
|
219
|
+
return output;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* A pressable action, composed from parts rather than configured by flags.
|
|
224
|
+
*
|
|
225
|
+
* `variant`, `size` and the button's state reach the sub-components through
|
|
226
|
+
* context, so `Button.Label` picks its own text colour. That indirection is not
|
|
227
|
+
* incidental: a React Native `View` does not cascade colour to a `Text`
|
|
228
|
+
* descendant the way a DOM element would, so a colour set on the root is lost.
|
|
229
|
+
*
|
|
230
|
+
* Plain string or number children are wrapped in a `Button.Label`
|
|
231
|
+
* automatically; pass the compound parts when a layout needs more control.
|
|
232
|
+
*
|
|
233
|
+
* Icons are composed in, not passed as props. Anything in the subtree inherits
|
|
234
|
+
* the button's icon size and its variant's colour, so an `Icon` needs nothing
|
|
235
|
+
* but the glyph — and a `Spinner` needs nothing at all.
|
|
236
|
+
*
|
|
237
|
+
* Text works the same way: the button publishes its label treatment through a
|
|
238
|
+
* `TextClassProvider`, so a bare `<Text>` composed in comes out at the label's
|
|
239
|
+
* colour and type scale without being told to.
|
|
240
|
+
*
|
|
241
|
+
* `isLoading` blocks presses and announces the button as busy, but does not dim
|
|
242
|
+
* it: a spinner already says the press landed. `isDimmedWhileLoading` opts into
|
|
243
|
+
* the faded treatment.
|
|
244
|
+
*
|
|
245
|
+
* A button is a `Pressable`, so `feedback`, `haptic` and the rest are inherited
|
|
246
|
+
* rather than restated here. Only the default differs: `scale`, the spring the
|
|
247
|
+
* rest of the library's controls press with.
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* <Button haptic="selection" onPress={next}>
|
|
251
|
+
* <Button.Label>Continue</Button.Label>
|
|
252
|
+
* <Icon icon={IconArrowRight} />
|
|
253
|
+
* </Button>
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* <Button isLoading={isSaving} onPress={save}>Save</Button>
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* <Button accessibilityLabel="Favourite" size="icon-md" variant="ghost">
|
|
260
|
+
* <Icon icon={IconHeart} />
|
|
261
|
+
* </Button>
|
|
262
|
+
*/
|
|
263
|
+
export const Button = Object.assign(ButtonRoot, {
|
|
264
|
+
/** The button's text. Picks its colour and type scale from the button's variant and size. */
|
|
265
|
+
Label: ButtonLabel,
|
|
266
|
+
/** A centred wrapper for leading content that is not an `Icon`. */
|
|
267
|
+
StartContent: ButtonStartContent,
|
|
268
|
+
/** A centred wrapper for trailing content that is not an `Icon`. */
|
|
269
|
+
EndContent: ButtonEndContent,
|
|
270
|
+
/** Several controls joined into one run, sharing a corner and a seam. */
|
|
271
|
+
Group: ButtonGroup,
|
|
272
|
+
displayName: "DelacourUI.Button",
|
|
273
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ViewProps } from "react-native";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The shape of a button's leading and trailing slots.
|
|
5
|
+
*
|
|
6
|
+
* Shared by `Button.StartContent` and `Button.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 ButtonSlotProps = ViewProps & { className?: string };
|