@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,42 @@
|
|
|
1
|
+
export { Switch, type SwitchProps } from "./switch";
|
|
2
|
+
export { type SwitchContextValue, SwitchProvider, useSwitch, useSwitchContext } from "./switch.context";
|
|
3
|
+
export type { SwitchContentProps } from "./switch.types";
|
|
4
|
+
export {
|
|
5
|
+
hasThumbChild,
|
|
6
|
+
resolveSwitchAxes,
|
|
7
|
+
resolveSwitchContentTreatment,
|
|
8
|
+
resolveSwitchRelease,
|
|
9
|
+
resolveSwitchThumbTokens,
|
|
10
|
+
resolveSwitchTrackTokens,
|
|
11
|
+
SWITCH_COLORS,
|
|
12
|
+
SWITCH_CONTENT_ICON_STEP,
|
|
13
|
+
SWITCH_CONTENT_REST_TEXT_CLASS,
|
|
14
|
+
SWITCH_CONTENT_REST_TOKEN,
|
|
15
|
+
SWITCH_CONTENT_TEXT_CLASS,
|
|
16
|
+
SWITCH_DEFAULT_COLOR,
|
|
17
|
+
SWITCH_DEFAULT_SIZE,
|
|
18
|
+
SWITCH_FLING_VELOCITY,
|
|
19
|
+
SWITCH_INVALID_CONTENT_TEXT_CLASS,
|
|
20
|
+
SWITCH_INVALID_THUMB_TOKEN,
|
|
21
|
+
SWITCH_INVALID_TRACK_TOKEN,
|
|
22
|
+
SWITCH_PRESS_ANIMATION,
|
|
23
|
+
SWITCH_PRESS_SPRING,
|
|
24
|
+
SWITCH_SIZES,
|
|
25
|
+
SWITCH_TAP_SLOP,
|
|
26
|
+
SWITCH_THUMB_INSET,
|
|
27
|
+
SWITCH_THUMB_REST_TOKEN,
|
|
28
|
+
SWITCH_THUMB_SPRING,
|
|
29
|
+
SWITCH_THUMB_TOKEN,
|
|
30
|
+
SWITCH_TRACK_REST_TOKEN,
|
|
31
|
+
SWITCH_TRACK_TOKEN,
|
|
32
|
+
type SwitchAxes,
|
|
33
|
+
type SwitchColor,
|
|
34
|
+
type SwitchContentPlacement,
|
|
35
|
+
type SwitchFieldAxes,
|
|
36
|
+
type SwitchOwnAxes,
|
|
37
|
+
type SwitchSize,
|
|
38
|
+
type SwitchVariantProps,
|
|
39
|
+
switchTravel,
|
|
40
|
+
switchVariants,
|
|
41
|
+
} from "./switch.variants";
|
|
42
|
+
export type { SwitchThumbProps } from "./switch-thumb";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Children, type ReactElement, type ReactNode, useMemo } from "react";
|
|
2
|
+
import Animated, { useAnimatedStyle } from "react-native-reanimated";
|
|
3
|
+
import { useThemeColor } from "../../hooks/use-theme-color";
|
|
4
|
+
import { IconDefaultsProvider } from "../icon";
|
|
5
|
+
import { Text } from "../text";
|
|
6
|
+
import { TextClassProvider } from "../text/text.context";
|
|
7
|
+
import { useSwitchPart } from "./switch.context";
|
|
8
|
+
import type { SwitchContentProps } from "./switch.types";
|
|
9
|
+
import { resolveSwitchContentTreatment, type SwitchContentPlacement, switchVariants } from "./switch.variants";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The shared implementation behind `Switch.StartContent` and `.EndContent`.
|
|
13
|
+
*
|
|
14
|
+
* The two are the same box at opposite ends of the track — which end, and which
|
|
15
|
+
* way their opacity runs, are the only differences — so they share this leaf
|
|
16
|
+
* rather than duplicating {@link wrapTextChildren} and an animated style into
|
|
17
|
+
* two part files. `Input.Group`'s two decorators make the same trade.
|
|
18
|
+
*
|
|
19
|
+
* **The crossfade is the component's, not the caller's.** `StartContent` sits at
|
|
20
|
+
* the leading edge, which the thumb vacates as the switch turns **on**, so it
|
|
21
|
+
* fades in with `progress`; `EndContent` sits at the trailing edge and fades out
|
|
22
|
+
* with it. That is what lets both be written once with no conditionals — the
|
|
23
|
+
* thumb reads as uncovering the other end rather than as sliding over content
|
|
24
|
+
* that was always there.
|
|
25
|
+
*
|
|
26
|
+
* The layer is exactly the thumb's footprint at its own end, so a glyph is
|
|
27
|
+
* centred on the space the knob will vacate rather than beside it.
|
|
28
|
+
*
|
|
29
|
+
* Not a part itself: it takes the caller-facing name so the error thrown outside
|
|
30
|
+
* a switch names `Switch.StartContent` rather than something private.
|
|
31
|
+
*/
|
|
32
|
+
export function SwitchContent({
|
|
33
|
+
part,
|
|
34
|
+
placement,
|
|
35
|
+
className,
|
|
36
|
+
children,
|
|
37
|
+
...props
|
|
38
|
+
}: SwitchContentProps & { part: string; placement: SwitchContentPlacement }): ReactElement {
|
|
39
|
+
const { color, size, isDisabled, isInvalid, progress } = useSwitchPart(part);
|
|
40
|
+
const slots = switchVariants({ isDisabled, size });
|
|
41
|
+
|
|
42
|
+
const treatment = resolveSwitchContentTreatment({ color, isInvalid, placement });
|
|
43
|
+
const glyphColor = useThemeColor(treatment.color);
|
|
44
|
+
const glyphClassName = slots.glyph();
|
|
45
|
+
const iconDefaults = useMemo(
|
|
46
|
+
() => ({ className: glyphClassName, color: glyphColor ?? "" }),
|
|
47
|
+
[glyphClassName, glyphColor]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const isStart = placement === "start";
|
|
51
|
+
const contentStyle = useAnimatedStyle(() => ({
|
|
52
|
+
opacity: isStart ? progress.value : 1 - progress.value,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
const textClassName = slots.contentText({ className: treatment.textClass });
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Animated.View
|
|
59
|
+
className={isStart ? slots.startContent({ className }) : slots.endContent({ className })}
|
|
60
|
+
// Never the element a screen reader lands on. The switch announces itself
|
|
61
|
+
// as one control with a checked state, and a decorative glyph either side
|
|
62
|
+
// of the knob has nothing to add to that.
|
|
63
|
+
accessible={false}
|
|
64
|
+
importantForAccessibility="no-hide-descendants"
|
|
65
|
+
pointerEvents="none"
|
|
66
|
+
style={contentStyle}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
<IconDefaultsProvider value={iconDefaults}>
|
|
70
|
+
<TextClassProvider value={textClassName}>{wrapTextChildren(children)}</TextClassProvider>
|
|
71
|
+
</IconDefaultsProvider>
|
|
72
|
+
</Animated.View>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
SwitchContent.displayName = "DelacourUI.Switch.Content";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Wraps bare text children in a `Text`.
|
|
79
|
+
*
|
|
80
|
+
* A content layer renders a `View`, and React Native cannot render a string
|
|
81
|
+
* outside a `<Text>` — so `<Switch.EndContent>OFF</Switch.EndContent>`, which is
|
|
82
|
+
* the shortest thing anyone will write, would otherwise crash. Consecutive
|
|
83
|
+
* strings and numbers collapse into one `Text` rather than one each, the same
|
|
84
|
+
* rule and the same reason as `Input`'s decorators.
|
|
85
|
+
*
|
|
86
|
+
* The wrapped text needs no className: the layer has already published its
|
|
87
|
+
* treatment through `TextClassProvider`.
|
|
88
|
+
*/
|
|
89
|
+
function wrapTextChildren(children: ReactNode): ReactNode {
|
|
90
|
+
const items = Children.toArray(children);
|
|
91
|
+
const output: ReactNode[] = [];
|
|
92
|
+
let run: (string | number)[] = [];
|
|
93
|
+
|
|
94
|
+
const flushRun = () => {
|
|
95
|
+
if (run.length === 0) return;
|
|
96
|
+
output.push(<Text key={`content-${output.length}`}>{run.join("")}</Text>);
|
|
97
|
+
run = [];
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
for (const child of items) {
|
|
101
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
102
|
+
run.push(child);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
flushRun();
|
|
106
|
+
output.push(child);
|
|
107
|
+
}
|
|
108
|
+
flushRun();
|
|
109
|
+
|
|
110
|
+
return output;
|
|
111
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type { SwitchContentProps } from "./switch.types";
|
|
3
|
+
import { SwitchContent } from "./switch-content";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Content at the trailing edge of the track, behind the thumb.
|
|
7
|
+
*
|
|
8
|
+
* Revealed as the switch turns **off** — this is the end the knob vacates — so
|
|
9
|
+
* it is where the "off" mark goes: a cross, a moon, the word OFF. The fade is
|
|
10
|
+
* the component's, so write it once and unconditionally.
|
|
11
|
+
*
|
|
12
|
+
* It sits on the resting track rather than the coloured one, so its glyph takes
|
|
13
|
+
* a muted foreground rather than the switch's colour.
|
|
14
|
+
*/
|
|
15
|
+
export function SwitchEndContent(props: SwitchContentProps): ReactElement {
|
|
16
|
+
return <SwitchContent part="Switch.EndContent" placement="end" {...props} />;
|
|
17
|
+
}
|
|
18
|
+
SwitchEndContent.displayName = "DelacourUI.Switch.EndContent";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type { SwitchContentProps } from "./switch.types";
|
|
3
|
+
import { SwitchContent } from "./switch-content";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Content at the leading edge of the track, behind the thumb.
|
|
7
|
+
*
|
|
8
|
+
* Revealed as the switch turns **on** — this is the end the knob vacates — so it
|
|
9
|
+
* is where the "on" mark goes: a tick, a sun, the word ON. The fade is the
|
|
10
|
+
* component's, so write it once and unconditionally; there is no `isSelected &&`
|
|
11
|
+
* to remember.
|
|
12
|
+
*
|
|
13
|
+
* An `Icon` needs nothing said at the call site: it inherits the switch's glyph
|
|
14
|
+
* step and the foreground of the colour the track fades to. A bare string is
|
|
15
|
+
* wrapped in a `Text` that inherits the same treatment.
|
|
16
|
+
*/
|
|
17
|
+
export function SwitchStartContent(props: SwitchContentProps): ReactElement {
|
|
18
|
+
return <SwitchContent part="Switch.StartContent" placement="start" {...props} />;
|
|
19
|
+
}
|
|
20
|
+
SwitchStartContent.displayName = "DelacourUI.Switch.StartContent";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode, useCallback, useMemo } from "react";
|
|
2
|
+
import type { LayoutChangeEvent, ViewProps } from "react-native";
|
|
3
|
+
import Animated, { interpolateColor, useAnimatedStyle } from "react-native-reanimated";
|
|
4
|
+
import { useThemeColor } from "../../hooks/use-theme-color";
|
|
5
|
+
import { IconDefaultsProvider } from "../icon";
|
|
6
|
+
import { useSwitchPart } from "./switch.context";
|
|
7
|
+
import {
|
|
8
|
+
resolveSwitchThumbTokens,
|
|
9
|
+
resolveSwitchTrackTokens,
|
|
10
|
+
SWITCH_THUMB_INSET,
|
|
11
|
+
switchTravel,
|
|
12
|
+
switchVariants,
|
|
13
|
+
} from "./switch.variants";
|
|
14
|
+
|
|
15
|
+
export type SwitchThumbProps = Omit<ViewProps, "children" | "style"> & {
|
|
16
|
+
className?: string;
|
|
17
|
+
/** Drawn inside the knob. An `Icon` needs nothing said at the call site. */
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The knob that slides between the two ends of the track.
|
|
23
|
+
*
|
|
24
|
+
* Composed in automatically when the children hold none, so `<Switch />` is
|
|
25
|
+
* already a complete control — `Radio`'s rule for its indicator. Write it out by
|
|
26
|
+
* hand only to restyle it, or to put a glyph inside it.
|
|
27
|
+
*
|
|
28
|
+
* **It holds no gesture of its own.** One `Gesture.Pan()` on the root drives it,
|
|
29
|
+
* because a touch anywhere on the pill should move the knob it is about to
|
|
30
|
+
* move — and a gesture here would nest a descendant recogniser inside the root's,
|
|
31
|
+
* leaving two to negotiate for one drag. `Slider.Thumb` makes the same trade for
|
|
32
|
+
* the same reason.
|
|
33
|
+
*
|
|
34
|
+
* **One animated style, one node.** The position and the colour are entries in a
|
|
35
|
+
* single `useAnimatedStyle`, because two calls on one view fight for the same
|
|
36
|
+
* props and the later one silently wins — the rule `Radio.Indicator` states.
|
|
37
|
+
*
|
|
38
|
+
* **The colour is not a class.** It fades between two token *values* as the
|
|
39
|
+
* switch travels, and a colour being interpolated cannot be a class — the rule
|
|
40
|
+
* `Checkbox`'s border already follows. The `thumb` slot keeps `bg-background` as
|
|
41
|
+
* the resting appearance this style starts from, and nothing else names a
|
|
42
|
+
* background; two sources for one surface is how a class and a style end up
|
|
43
|
+
* disagreeing for a frame on every toggle.
|
|
44
|
+
*
|
|
45
|
+
* **It is invisible until both layouts have reported.** The travel is measured,
|
|
46
|
+
* not tabulated — `size-icon-xl` cannot be read from JavaScript — and a knob
|
|
47
|
+
* drawn before then sits at a garbage offset for a frame, which reads as a
|
|
48
|
+
* flicker on every mount.
|
|
49
|
+
*
|
|
50
|
+
* **It does not scale when grabbed, and the track does instead.** The track
|
|
51
|
+
* clips, so a scaled knob would be cut off by its own capsule — a bite taken out
|
|
52
|
+
* of the knob rather than an acknowledgement of the press. The outermost node is
|
|
53
|
+
* the one thing nothing can crop, so that is where the press feedback lives.
|
|
54
|
+
*/
|
|
55
|
+
export function SwitchThumb({ className, children, ...props }: SwitchThumbProps): ReactElement {
|
|
56
|
+
const { color, size, isInvalid, isDisabled, progress, trackWidth, thumbWidth } = useSwitchPart("Switch.Thumb");
|
|
57
|
+
|
|
58
|
+
const handleLayout = useCallback(
|
|
59
|
+
(event: LayoutChangeEvent) => {
|
|
60
|
+
thumbWidth.value = event.nativeEvent.layout.width;
|
|
61
|
+
},
|
|
62
|
+
[thumbWidth]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const tokens = resolveSwitchThumbTokens({ color, isInvalid });
|
|
66
|
+
const restColor = useThemeColor(tokens.rest) ?? "transparent";
|
|
67
|
+
const activeColor = useThemeColor(tokens.active) ?? "transparent";
|
|
68
|
+
|
|
69
|
+
const thumbStyle = useAnimatedStyle(() => {
|
|
70
|
+
const measured = trackWidth.value;
|
|
71
|
+
const travel = switchTravel({
|
|
72
|
+
inset: SWITCH_THUMB_INSET,
|
|
73
|
+
thumbWidth: thumbWidth.value,
|
|
74
|
+
trackWidth: measured,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
backgroundColor: interpolateColor(progress.value, [0, 1], [restColor, activeColor]),
|
|
79
|
+
opacity: measured > 0 ? 1 : 0,
|
|
80
|
+
transform: [{ translateX: progress.value * travel }],
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// A glyph on the knob takes the *track*'s colour, not the knob's own: it is
|
|
85
|
+
// drawn on the knob, so it has to contrast with it, and the one value that
|
|
86
|
+
// reads at both ends is the colour the track fades to. Static rather than
|
|
87
|
+
// interpolated because an `Icon`'s colour is a prop, not a style.
|
|
88
|
+
const slots = switchVariants({ isDisabled, size });
|
|
89
|
+
const glyphColor = useThemeColor(resolveSwitchTrackTokens({ color, isInvalid }).active);
|
|
90
|
+
const glyphClassName = slots.glyph();
|
|
91
|
+
const iconDefaults = useMemo(
|
|
92
|
+
() => ({ className: glyphClassName, color: glyphColor ?? "" }),
|
|
93
|
+
[glyphClassName, glyphColor]
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Animated.View className={slots.thumb({ className })} onLayout={handleLayout} style={thumbStyle} {...props}>
|
|
98
|
+
{children ? <IconDefaultsProvider value={iconDefaults}>{children}</IconDefaultsProvider> : null}
|
|
99
|
+
</Animated.View>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
SwitchThumb.displayName = "DelacourUI.Switch.Thumb";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createContext, type ReactElement, type ReactNode, use } from "react";
|
|
2
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
3
|
+
import type { SwitchColor, SwitchSize } from "./switch.variants";
|
|
4
|
+
|
|
5
|
+
export type SwitchContextValue = {
|
|
6
|
+
color: SwitchColor;
|
|
7
|
+
size: SwitchSize;
|
|
8
|
+
/** Whether the switch is on, on the **JS thread** — what accessibility reads. */
|
|
9
|
+
isSelected: boolean;
|
|
10
|
+
isDisabled: boolean;
|
|
11
|
+
isInvalid: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Where the thumb is, on the **UI thread**: `0` at the leading edge, `1` at
|
|
14
|
+
* the trailing one.
|
|
15
|
+
*
|
|
16
|
+
* The one value every animated style on this control reads — the thumb's
|
|
17
|
+
* position and colour, the track's colour, and both content layers' opacity.
|
|
18
|
+
* One source rather than four, so they cannot drift out of step by a frame.
|
|
19
|
+
*
|
|
20
|
+
* It agrees with {@link SwitchContextValue.isSelected} at rest and disagrees
|
|
21
|
+
* for the whole of a drag, which is the entire reason the drag never waits on
|
|
22
|
+
* React.
|
|
23
|
+
*/
|
|
24
|
+
progress: SharedValue<number>;
|
|
25
|
+
/** The track's own width, from its `onLayout`. `0` until it has been measured. */
|
|
26
|
+
trackWidth: SharedValue<number>;
|
|
27
|
+
/** The thumb's width, from its `onLayout`. `0` until it has been measured. */
|
|
28
|
+
thumbWidth: SharedValue<number>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const SwitchContext = createContext<SwitchContextValue | null>(null);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Supplies one switch's settled state and shared values to its own parts.
|
|
35
|
+
*
|
|
36
|
+
* Lives in its own module, importing nothing but React and types, so a part can
|
|
37
|
+
* read it without importing `./switch`. That import would close a cycle, and
|
|
38
|
+
* Metro serves a partially initialised module for a cycle — leaving the context
|
|
39
|
+
* `undefined` at import time and red-boxing the app on a cold start.
|
|
40
|
+
*/
|
|
41
|
+
export function SwitchProvider({ value, children }: { value: SwitchContextValue; children: ReactNode }): ReactElement {
|
|
42
|
+
return <SwitchContext value={value}>{children}</SwitchContext>;
|
|
43
|
+
}
|
|
44
|
+
SwitchProvider.displayName = "DelacourUI.Switch.Provider";
|
|
45
|
+
|
|
46
|
+
/** The enclosing switch's state, or null outside a `<Switch>`. */
|
|
47
|
+
export function useSwitchContext(): SwitchContextValue | null {
|
|
48
|
+
return use(SwitchContext);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Reads the enclosing switch's settled state.
|
|
53
|
+
*
|
|
54
|
+
* Lets a custom child style itself to match without the switch having to pass
|
|
55
|
+
* props down through every slot. Throws outside a `<Switch>` — use
|
|
56
|
+
* {@link useSwitchContext} where the enclosing switch is optional.
|
|
57
|
+
*/
|
|
58
|
+
export function useSwitch(): SwitchContextValue {
|
|
59
|
+
const context = useSwitchContext();
|
|
60
|
+
if (!context) {
|
|
61
|
+
throw new Error("useSwitch must be called inside a <Switch>.");
|
|
62
|
+
}
|
|
63
|
+
return context;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The enclosing switch's state, for a compound part that cannot work without one.
|
|
68
|
+
*
|
|
69
|
+
* Internal: deliberately not re-exported from `index.ts`. A caller outside the
|
|
70
|
+
* library wants {@link useSwitch}, whose error message names the hook rather
|
|
71
|
+
* than a part.
|
|
72
|
+
*/
|
|
73
|
+
export function useSwitchPart(component: string): SwitchContextValue {
|
|
74
|
+
const context = useSwitchContext();
|
|
75
|
+
if (!context) {
|
|
76
|
+
throw new Error(`${component} must be rendered inside a <Switch>.`);
|
|
77
|
+
}
|
|
78
|
+
return context;
|
|
79
|
+
}
|