@castui/cast-ui 4.8.0 → 4.10.0

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 CHANGED
@@ -18,8 +18,18 @@ so what designers see in Figma is what ships in the app. Every component
18
18
  supports light and dark mode, three spacing densities, and your own brand
19
19
  colours — all switchable while the app is running, with no rebuild.
20
20
 
21
+ Documentation site — live examples, patterns, templates, themes, motion,
22
+ and the system graph: **https://connagh.github.io/cast-ui/**
23
+
21
24
  Browse every component live in the
22
- [hosted Storybook](https://main--6990f00d7b8682c18d2ed5f3.chromatic.com).
25
+ [hosted Storybook](https://main--6990f00d7b8682c18d2ed5f3.chromatic.com),
26
+ or grab the open source
27
+ [Figma kit](https://www.figma.com/community/file/1648821010844688421/cast-ui-kit-for-react-native).
28
+
29
+ Motion is part of the token system too: durations, easing curves, and
30
+ springs live in the kit's `motion` variable collection, ship as
31
+ `theme.motion`, honour the OS reduce-motion setting, and can be retimed at
32
+ runtime like any colour.
23
33
 
24
34
  ## Installation
25
35
 
@@ -163,6 +173,61 @@ The full guide lives in the
163
173
  [hosted Storybook](https://main--6990f00d7b8682c18d2ed5f3.chromatic.com)
164
174
  under **Guides → Customisation**.
165
175
 
176
+ ## Responsive layout
177
+
178
+ Cast UI ships a set of breakpoints and hooks for building layouts that adapt
179
+ across phones, tablets, and desktops. The values follow the Material 3 window
180
+ size classes, so the same numbers hold up across watches, phones, foldables,
181
+ tablets, and large screens.
182
+
183
+ | Tier | Range (dp) | Typical devices |
184
+ |------|-----------|-----------------|
185
+ | `base` | `< 600` | Watches and every phone in portrait. Your default layout. |
186
+ | `sm` | `>= 600` | Large phones in landscape, foldables unfolded, small tablets |
187
+ | `md` | `>= 840` | Tablets |
188
+ | `lg` | `>= 1200` | Laptops and desktops |
189
+ | `xl` | `>= 1600` | Large desktops and TVs |
190
+
191
+ The hooks read the live window width, so they update on resize, rotation, and
192
+ foldables, and they behave the same on the web. The most common one picks a
193
+ value per tier:
194
+
195
+ ```tsx
196
+ import { useResponsiveValue, useBreakpoint, useMinWidth } from '@castui/cast-ui';
197
+
198
+ function Gallery() {
199
+ const columns = useResponsiveValue({ base: 1, md: 2, xl: 4 });
200
+ // 1 column on phones, 2 on tablets, 4 on large desktops
201
+ ...
202
+ }
203
+ ```
204
+
205
+ `useResponsiveValue` is mobile-first: a tier with no value falls back to the
206
+ nearest one below it, so `{ base: 1, md: 2 }` gives 1 up to `md` and 2 from
207
+ `md` on. The other two hooks cover the rest:
208
+
209
+ ```tsx
210
+ const tier = useBreakpoint(); // 'base' | 'sm' | 'md' | 'lg' | 'xl'
211
+ const isWide = useMinWidth('lg'); // true from 1200dp up
212
+ ```
213
+
214
+ The raw thresholds are also exported as `breakpoints` if you need a number
215
+ directly.
216
+
217
+ A few things worth knowing. React Native has no CSS media queries, so a
218
+ breakpoint here is a width threshold you compare against, not automatic
219
+ restyling. `base` is the mobile-first default: you write the phone layout with
220
+ no breakpoint, then add overrides for larger screens. The gap between a small
221
+ and a large phone is better handled with flexible layout (flex, percentages,
222
+ `maxWidth`) than with a breakpoint.
223
+
224
+ Breakpoints are a fixed foundation, which sets them apart from the rest of the
225
+ theme. They are not part of `ThemeProvider`: they do not change with density,
226
+ they are not touched by brand colour overrides, and they are not carried in
227
+ `cast-theme.json`. The scale stays identical in every app, so layouts stay
228
+ predictable. The same values live as the `breakpoint/*` primitive variables in
229
+ the [cast-ui-kit Figma file](https://www.figma.com/design/JGtlpxLPJMZcwvQ3UZ9ZUl/cast-ui-kit).
230
+
166
231
  ## Theming from Figma — the cast-sync plugin
167
232
 
168
233
  [`cast-sync/`](./cast-sync) is a Figma plugin that turns the Figma file's
@@ -210,6 +275,18 @@ npm run build # compile to dist/
210
275
  | `npm run build-storybook` | Build static Storybook |
211
276
  | `npm run build` | TypeScript compilation to `dist/` |
212
277
 
278
+ ### Documentation site
279
+
280
+ ```bash
281
+ cd site
282
+ npm install
283
+ npm run dev # local dev server
284
+ npm run smoke # render every route + evaluate every live example
285
+ ```
286
+
287
+ The site aliases `@castui/cast-ui` to `../src`, so it always documents the
288
+ code in your working tree.
289
+
213
290
  ## CI/CD
214
291
 
215
292
  | Workflow | Trigger | Purpose |
@@ -81,6 +81,7 @@ function toOpenArray(v, type) {
81
81
  function AccordionItem({ value, title, leadingIcon, disabled = false, children, style, accessibilityLabel, }) {
82
82
  const { openValues, toggle, size } = useAccordionContext('AccordionItem');
83
83
  const { components, colors, scheme } = (0, theme_1.useTheme)();
84
+ const motion = (0, theme_1.useMotion)();
84
85
  const [isHovered, setIsHovered] = (0, react_1.useState)(false);
85
86
  const sizeTokens = components.accordion[size];
86
87
  const isOpen = openValues.includes(value);
@@ -89,11 +90,11 @@ function AccordionItem({ value, title, leadingIcon, disabled = false, children,
89
90
  (0, react_1.useEffect)(() => {
90
91
  react_native_1.Animated.timing(spin, {
91
92
  toValue: isOpen ? 1 : 0,
92
- duration: 160,
93
- easing: react_native_1.Easing.inOut(react_native_1.Easing.ease),
93
+ duration: motion.scale(motion.transition.expand.duration),
94
+ easing: motion.transition.expand.easing,
94
95
  useNativeDriver: true,
95
96
  }).start();
96
- }, [isOpen, spin]);
97
+ }, [isOpen, spin, motion]);
97
98
  const rotate = spin.interpolate({
98
99
  inputRange: [0, 1],
99
100
  outputRange: ['0deg', '90deg'],
@@ -23,15 +23,12 @@ const theme_1 = require("../../theme");
23
23
  // ---------------------------------------------------------------------------
24
24
  // Constants
25
25
  // ---------------------------------------------------------------------------
26
- /** Fade timing. */
27
- const DURATION = 220;
28
- /** react-native-web does not support the native animation driver. */
29
- const USE_NATIVE_DRIVER = react_native_1.Platform.OS !== 'web';
30
26
  // ---------------------------------------------------------------------------
31
27
  // Component
32
28
  // ---------------------------------------------------------------------------
33
29
  function Backdrop({ open, onPress, invisible = false, children, style, accessibilityLabel, }) {
34
30
  const { scheme } = (0, theme_1.useTheme)();
31
+ const motion = (0, theme_1.useMotion)();
35
32
  const targetOpacity = invisible ? 0 : scheme.overlay.scrimOpacity;
36
33
  const fade = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
37
34
  const [mounted, setMounted] = (0, react_1.useState)(open);
@@ -40,15 +37,17 @@ function Backdrop({ open, onPress, invisible = false, children, style, accessibi
40
37
  setMounted(true);
41
38
  react_native_1.Animated.timing(fade, {
42
39
  toValue: 1,
43
- duration: DURATION,
44
- useNativeDriver: USE_NATIVE_DRIVER,
40
+ duration: motion.scale(motion.transition.standard.duration),
41
+ easing: motion.transition.standard.easing,
42
+ useNativeDriver: motion.useNativeDriver,
45
43
  }).start();
46
44
  }
47
45
  else if (mounted) {
48
46
  react_native_1.Animated.timing(fade, {
49
47
  toValue: 0,
50
- duration: DURATION,
51
- useNativeDriver: USE_NATIVE_DRIVER,
48
+ duration: motion.scale(motion.transition.standard.duration),
49
+ easing: motion.transition.standard.easing,
50
+ useNativeDriver: motion.useNativeDriver,
52
51
  }).start(({ finished }) => {
53
52
  if (finished)
54
53
  setMounted(false);
@@ -34,10 +34,6 @@ const tokens_1 = require("../../tokens");
34
34
  // ---------------------------------------------------------------------------
35
35
  /** The sheet never grows past this share of the screen height. */
36
36
  const MAX_HEIGHT_RATIO = 0.9;
37
- /** Animation timing. */
38
- const DURATION = 220;
39
- /** react-native-web does not support the native animation driver. */
40
- const USE_NATIVE_DRIVER = react_native_1.Platform.OS !== 'web';
41
37
  /** Upward shadow for web (matches Figma shadow/lg, cast above the sheet). */
42
38
  const SHADOW_WEB = {
43
39
  boxShadow: '0px -4px 6px rgba(0,0,0,0.04), 0px -10px 15px rgba(0,0,0,0.08)',
@@ -97,6 +93,7 @@ function BottomSheetContent({ title: titleText, showHandle = true, children, sty
97
93
  // ---------------------------------------------------------------------------
98
94
  function BottomSheet({ open, onClose, closeOnBackdropPress = true, ...contentProps }) {
99
95
  const { scheme } = (0, theme_1.useTheme)();
96
+ const motion = (0, theme_1.useMotion)();
100
97
  const scrimOpacity = scheme.overlay.scrimOpacity;
101
98
  const screenHeight = react_native_1.Dimensions.get('window').height;
102
99
  const translateY = (0, react_1.useRef)(new react_native_1.Animated.Value(screenHeight)).current;
@@ -108,15 +105,14 @@ function BottomSheet({ open, onClose, closeOnBackdropPress = true, ...contentPro
108
105
  react_native_1.Animated.parallel([
109
106
  react_native_1.Animated.timing(backdrop, {
110
107
  toValue: 1,
111
- duration: DURATION,
112
- useNativeDriver: USE_NATIVE_DRIVER,
108
+ duration: motion.scale(motion.transition.standard.duration),
109
+ easing: motion.transition.standard.easing,
110
+ useNativeDriver: motion.useNativeDriver,
113
111
  }),
114
112
  react_native_1.Animated.spring(translateY, {
115
113
  toValue: 0,
116
- damping: 22,
117
- stiffness: 220,
118
- mass: 0.9,
119
- useNativeDriver: USE_NATIVE_DRIVER,
114
+ ...motion.spring.overlay,
115
+ useNativeDriver: motion.useNativeDriver,
120
116
  }),
121
117
  ]).start();
122
118
  }
@@ -124,13 +120,15 @@ function BottomSheet({ open, onClose, closeOnBackdropPress = true, ...contentPro
124
120
  react_native_1.Animated.parallel([
125
121
  react_native_1.Animated.timing(backdrop, {
126
122
  toValue: 0,
127
- duration: DURATION,
128
- useNativeDriver: USE_NATIVE_DRIVER,
123
+ duration: motion.scale(motion.transition.standard.duration),
124
+ easing: motion.transition.standard.easing,
125
+ useNativeDriver: motion.useNativeDriver,
129
126
  }),
130
127
  react_native_1.Animated.timing(translateY, {
131
128
  toValue: screenHeight,
132
- duration: DURATION,
133
- useNativeDriver: USE_NATIVE_DRIVER,
129
+ duration: motion.scale(motion.transition.standard.duration),
130
+ easing: motion.transition.standard.easing,
131
+ useNativeDriver: motion.useNativeDriver,
134
132
  }),
135
133
  ]).start(({ finished }) => {
136
134
  if (finished)
@@ -36,10 +36,6 @@ const tokens_1 = require("../../tokens");
36
36
  const DEFAULT_WIDTH = 320;
37
37
  /** Top/bottom panels never grow past this share of the screen height. */
38
38
  const MAX_HEIGHT_RATIO = 0.9;
39
- /** Animation timing. */
40
- const DURATION = 240;
41
- /** react-native-web does not support the native animation driver. */
42
- const USE_NATIVE_DRIVER = react_native_1.Platform.OS !== 'web';
43
39
  const SHADOW_WEB = {
44
40
  boxShadow: '0px 0px 6px rgba(0,0,0,0.04), 0px 0px 15px rgba(0,0,0,0.08)',
45
41
  };
@@ -99,6 +95,7 @@ function DrawerContent({ anchor = 'left', title: titleText, children, style, acc
99
95
  // ---------------------------------------------------------------------------
100
96
  function Drawer({ open, onClose, closeOnBackdropPress = true, anchor = 'left', ...contentProps }) {
101
97
  const { scheme } = (0, theme_1.useTheme)();
98
+ const motion = (0, theme_1.useMotion)();
102
99
  const scrimOpacity = scheme.overlay.scrimOpacity;
103
100
  const screen = react_native_1.Dimensions.get('window');
104
101
  const horizontal = isHorizontal(anchor);
@@ -113,15 +110,14 @@ function Drawer({ open, onClose, closeOnBackdropPress = true, anchor = 'left', .
113
110
  react_native_1.Animated.parallel([
114
111
  react_native_1.Animated.timing(backdrop, {
115
112
  toValue: 1,
116
- duration: DURATION,
117
- useNativeDriver: USE_NATIVE_DRIVER,
113
+ duration: motion.scale(motion.transition.standard.duration),
114
+ easing: motion.transition.standard.easing,
115
+ useNativeDriver: motion.useNativeDriver,
118
116
  }),
119
117
  react_native_1.Animated.spring(offset, {
120
118
  toValue: 0,
121
- damping: 24,
122
- stiffness: 240,
123
- mass: 0.9,
124
- useNativeDriver: USE_NATIVE_DRIVER,
119
+ ...motion.spring.overlay,
120
+ useNativeDriver: motion.useNativeDriver,
125
121
  }),
126
122
  ]).start();
127
123
  }
@@ -129,13 +125,15 @@ function Drawer({ open, onClose, closeOnBackdropPress = true, anchor = 'left', .
129
125
  react_native_1.Animated.parallel([
130
126
  react_native_1.Animated.timing(backdrop, {
131
127
  toValue: 0,
132
- duration: DURATION,
133
- useNativeDriver: USE_NATIVE_DRIVER,
128
+ duration: motion.scale(motion.transition.standard.duration),
129
+ easing: motion.transition.standard.easing,
130
+ useNativeDriver: motion.useNativeDriver,
134
131
  }),
135
132
  react_native_1.Animated.timing(offset, {
136
133
  toValue: distance * sign,
137
- duration: DURATION,
138
- useNativeDriver: USE_NATIVE_DRIVER,
134
+ duration: motion.scale(motion.transition.standard.duration),
135
+ easing: motion.transition.standard.easing,
136
+ useNativeDriver: motion.useNativeDriver,
139
137
  }),
140
138
  ]).start(({ finished }) => {
141
139
  if (finished)
@@ -38,6 +38,7 @@ const clamp = (n) => Math.max(0, Math.min(100, n));
38
38
  // ---------------------------------------------------------------------------
39
39
  function Progress({ value, intent = 'brand', size = 'default', style, accessibilityLabel = 'Loading', }) {
40
40
  const { components, colors, scheme } = (0, theme_1.useTheme)();
41
+ const motion = (0, theme_1.useMotion)();
41
42
  const { trackHeight } = components.progress[size];
42
43
  const borderRadius = components.progress.borderRadius;
43
44
  const fill = colors[intent].bold.default.bg;
@@ -50,18 +51,18 @@ function Progress({ value, intent = 'brand', size = 'default', style, accessibil
50
51
  const onLayout = (e) => setTrackWidth(e.nativeEvent.layout.width);
51
52
  const slide = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
52
53
  (0, react_1.useEffect)(() => {
53
- if (!isIndeterminate || trackWidth === 0)
54
+ if (!isIndeterminate || trackWidth === 0 || motion.reduceMotion)
54
55
  return;
55
56
  slide.setValue(0);
56
57
  const loop = react_native_1.Animated.loop(react_native_1.Animated.timing(slide, {
57
58
  toValue: 1,
58
- duration: 1200,
59
- easing: react_native_1.Easing.inOut(react_native_1.Easing.ease),
59
+ duration: motion.loop.indeterminate.duration,
60
+ easing: motion.loop.indeterminate.easing,
60
61
  useNativeDriver: true,
61
62
  }));
62
63
  loop.start();
63
64
  return () => loop.stop();
64
- }, [isIndeterminate, trackWidth, slide]);
65
+ }, [isIndeterminate, trackWidth, slide, motion]);
65
66
  const barWidth = trackWidth * INDETERMINATE_BAR_FRACTION;
66
67
  const translateX = slide.interpolate({
67
68
  inputRange: [0, 1],
@@ -19,11 +19,6 @@ const jsx_runtime_1 = require("react/jsx-runtime");
19
19
  const react_1 = require("react");
20
20
  const react_native_1 = require("react-native");
21
21
  const theme_1 = require("../../theme");
22
- // ---------------------------------------------------------------------------
23
- // Constants
24
- // ---------------------------------------------------------------------------
25
- // react-native-web has no native driver and warns if asked for one.
26
- const USE_NATIVE_DRIVER = react_native_1.Platform.OS !== 'web';
27
22
  /** Default size + corner radius per shape (radius/2, surface/overlay/radius, radius/full). */
28
23
  const SHAPE_DEFAULTS = {
29
24
  text: { width: 120, height: 12, radius: 4 },
@@ -35,29 +30,32 @@ const SHAPE_DEFAULTS = {
35
30
  // ---------------------------------------------------------------------------
36
31
  function Skeleton({ shape = 'text', width, height, radius, animated = true, style, accessibilityLabel = 'Loading', }) {
37
32
  const { scheme } = (0, theme_1.useTheme)();
33
+ const motion = (0, theme_1.useMotion)();
38
34
  const skeletonColors = scheme.skeleton;
39
35
  const defaults = SHAPE_DEFAULTS[shape];
40
36
  const opacity = (0, react_1.useRef)(new react_native_1.Animated.Value(1)).current;
41
37
  (0, react_1.useEffect)(() => {
42
- if (!animated) {
38
+ if (!animated || motion.reduceMotion) {
43
39
  opacity.setValue(1);
44
40
  return;
45
41
  }
46
42
  const loop = react_native_1.Animated.loop(react_native_1.Animated.sequence([
47
43
  react_native_1.Animated.timing(opacity, {
48
- toValue: 0.5,
49
- duration: 700,
50
- useNativeDriver: USE_NATIVE_DRIVER,
44
+ toValue: motion.loop.pulse.to,
45
+ duration: motion.loop.pulse.duration,
46
+ easing: motion.loop.pulse.easing,
47
+ useNativeDriver: motion.useNativeDriver,
51
48
  }),
52
49
  react_native_1.Animated.timing(opacity, {
53
- toValue: 1,
54
- duration: 700,
55
- useNativeDriver: USE_NATIVE_DRIVER,
50
+ toValue: motion.loop.pulse.from,
51
+ duration: motion.loop.pulse.duration,
52
+ easing: motion.loop.pulse.easing,
53
+ useNativeDriver: motion.useNativeDriver,
56
54
  }),
57
55
  ]));
58
56
  loop.start();
59
57
  return () => loop.stop();
60
- }, [animated, opacity]);
58
+ }, [animated, opacity, motion]);
61
59
  return ((0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { accessibilityRole: "image", accessibilityLabel: accessibilityLabel, style: [
62
60
  {
63
61
  width: width ?? defaults.width,
@@ -58,9 +58,6 @@ const ACTION_ICON = {
58
58
  default: 'default',
59
59
  large: 'default',
60
60
  };
61
- const DURATION_IN = 160;
62
- const DURATION_OUT = 140;
63
- const USE_NATIVE_DRIVER = react_native_1.Platform.OS !== 'web';
64
61
  const SHADOW_WEB = {
65
62
  boxShadow: '0px 4px 6px -1px rgba(0,0,0,0.12), 0px 2px 4px -2px rgba(0,0,0,0.1)',
66
63
  };
@@ -111,6 +108,7 @@ function SpeedDialAction({ icon, label, onPress, disabled = false }) {
111
108
  // ---------------------------------------------------------------------------
112
109
  function SpeedDial({ children, icon = 'add', openIcon = 'close', open: controlledOpen, onOpenChange, defaultOpen = false, direction = 'up', intent = 'brand', size = 'default', backdrop = true, style, accessibilityLabel, }) {
113
110
  const { components, colors, scheme } = (0, theme_1.useTheme)();
111
+ const motion = (0, theme_1.useMotion)();
114
112
  const { fabSize, actionSize, gap } = components.speedDial[size];
115
113
  const fabColors = colors[intent].bold.default;
116
114
  const isControlled = controlledOpen !== undefined;
@@ -128,10 +126,10 @@ function SpeedDial({ children, icon = 'add', openIcon = 'close', open: controlle
128
126
  (0, react_1.useEffect)(() => {
129
127
  if (open) {
130
128
  setActionsMounted(true);
131
- react_native_1.Animated.timing(anim, { toValue: 1, duration: DURATION_IN, useNativeDriver: USE_NATIVE_DRIVER }).start();
129
+ react_native_1.Animated.timing(anim, { toValue: 1, duration: motion.scale(motion.transition.enter.duration), easing: motion.transition.enter.easing, useNativeDriver: motion.useNativeDriver }).start();
132
130
  }
133
131
  else if (actionsMounted) {
134
- react_native_1.Animated.timing(anim, { toValue: 0, duration: DURATION_OUT, useNativeDriver: USE_NATIVE_DRIVER }).start(({ finished }) => {
132
+ react_native_1.Animated.timing(anim, { toValue: 0, duration: motion.scale(motion.transition.exit.duration), easing: motion.transition.exit.easing, useNativeDriver: motion.useNativeDriver }).start(({ finished }) => {
135
133
  if (finished)
136
134
  setActionsMounted(false);
137
135
  });
@@ -36,28 +36,29 @@ const theme_1 = require("../../theme");
36
36
  // ---------------------------------------------------------------------------
37
37
  // Constants
38
38
  // ---------------------------------------------------------------------------
39
- /** One full rotation, in milliseconds. */
40
- const ROTATION_DURATION = 800;
41
39
  // ---------------------------------------------------------------------------
42
40
  // Component
43
41
  // ---------------------------------------------------------------------------
44
42
  function Spinner({ intent = 'brand', size = 'default', style, accessibilityLabel = 'Loading', }) {
45
43
  const { components, colors, scheme } = (0, theme_1.useTheme)();
44
+ const motion = (0, theme_1.useMotion)();
46
45
  const { diameter, stroke } = components.spinner[size];
47
46
  const arc = colors[intent].bold.default.bg;
48
47
  const trackColor = scheme.spinner.track;
49
48
  const spin = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
50
49
  (0, react_1.useEffect)(() => {
51
50
  spin.setValue(0);
51
+ if (motion.reduceMotion)
52
+ return;
52
53
  const loop = react_native_1.Animated.loop(react_native_1.Animated.timing(spin, {
53
54
  toValue: 1,
54
- duration: ROTATION_DURATION,
55
- easing: react_native_1.Easing.linear,
55
+ duration: motion.loop.spin.duration,
56
+ easing: motion.loop.spin.easing,
56
57
  useNativeDriver: true,
57
58
  }));
58
59
  loop.start();
59
60
  return () => loop.stop();
60
- }, [spin]);
61
+ }, [spin, motion]);
61
62
  const rotate = spin.interpolate({
62
63
  inputRange: [0, 1],
63
64
  outputRange: ['0deg', '360deg'],
@@ -0,0 +1 @@
1
+ export { useBreakpoint, useMinWidth, useResponsiveValue } from './useBreakpoint';
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useResponsiveValue = exports.useMinWidth = exports.useBreakpoint = void 0;
4
+ var useBreakpoint_1 = require("./useBreakpoint");
5
+ Object.defineProperty(exports, "useBreakpoint", { enumerable: true, get: function () { return useBreakpoint_1.useBreakpoint; } });
6
+ Object.defineProperty(exports, "useMinWidth", { enumerable: true, get: function () { return useBreakpoint_1.useMinWidth; } });
7
+ Object.defineProperty(exports, "useResponsiveValue", { enumerable: true, get: function () { return useBreakpoint_1.useResponsiveValue; } });
@@ -0,0 +1,22 @@
1
+ import { type Breakpoint, type BreakpointKey } from '../tokens/breakpoints';
2
+ /**
3
+ * The active breakpoint tier for the current window width. Mobile-first:
4
+ * 'base' below `sm`, then 'sm' | 'md' | 'lg' | 'xl' as the width grows.
5
+ *
6
+ * const bp = useBreakpoint(); // 'base' | 'sm' | 'md' | 'lg' | 'xl'
7
+ */
8
+ export declare function useBreakpoint(): BreakpointKey;
9
+ /**
10
+ * True when the window is at or above the given breakpoint.
11
+ *
12
+ * const isWide = useMinWidth('lg'); // true from 1024dp up
13
+ */
14
+ export declare function useMinWidth(breakpoint: Breakpoint): boolean;
15
+ /**
16
+ * Pick a value for the current breakpoint, mobile-first. Supply values for any
17
+ * subset of tiers; the value at the current tier wins, falling back to the
18
+ * nearest defined tier below it.
19
+ *
20
+ * const columns = useResponsiveValue({ base: 1, md: 2, xl: 4 });
21
+ */
22
+ export declare function useResponsiveValue<T>(values: Partial<Record<BreakpointKey, T>>): T | undefined;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useBreakpoint = useBreakpoint;
4
+ exports.useMinWidth = useMinWidth;
5
+ exports.useResponsiveValue = useResponsiveValue;
6
+ /**
7
+ * Responsive hooks built on the breakpoint scale.
8
+ *
9
+ * These read the live window width via react-native's useWindowDimensions, so
10
+ * they recompute on resize, rotation, and foldable changes, and they work the
11
+ * same on web (react-native-web). Breakpoints are a standalone foundation: they
12
+ * are not part of the theme and do not change with density.
13
+ */
14
+ const react_native_1 = require("react-native");
15
+ const breakpoints_1 = require("../tokens/breakpoints");
16
+ /**
17
+ * The active breakpoint tier for the current window width. Mobile-first:
18
+ * 'base' below `sm`, then 'sm' | 'md' | 'lg' | 'xl' as the width grows.
19
+ *
20
+ * const bp = useBreakpoint(); // 'base' | 'sm' | 'md' | 'lg' | 'xl'
21
+ */
22
+ function useBreakpoint() {
23
+ const { width } = (0, react_native_1.useWindowDimensions)();
24
+ return (0, breakpoints_1.resolveBreakpoint)(width);
25
+ }
26
+ /**
27
+ * True when the window is at or above the given breakpoint.
28
+ *
29
+ * const isWide = useMinWidth('lg'); // true from 1024dp up
30
+ */
31
+ function useMinWidth(breakpoint) {
32
+ const { width } = (0, react_native_1.useWindowDimensions)();
33
+ return width >= breakpoints_1.breakpoints[breakpoint];
34
+ }
35
+ /**
36
+ * Pick a value for the current breakpoint, mobile-first. Supply values for any
37
+ * subset of tiers; the value at the current tier wins, falling back to the
38
+ * nearest defined tier below it.
39
+ *
40
+ * const columns = useResponsiveValue({ base: 1, md: 2, xl: 4 });
41
+ */
42
+ function useResponsiveValue(values) {
43
+ const current = useBreakpoint();
44
+ return (0, breakpoints_1.resolveResponsiveValue)(values, current);
45
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- export { lightColors, darkColors, colorSchemes, intentColors, disabledColors, controlTokens, surfaceTokens, textTokens, overlayTokens, selectColors, menuColors, tagTokens, errorTokens, listColors, checkboxColors, toggleColors, progressColors, tabsColors, radioColors, avatarColors, skeletonColors, sliderColors, tableColors, fontFamily, fontWeight, label, title, body, heading, display, caption, type IntentName, type ProminenceName, type StateName, type ColorMode, type ColorScheme, type LabelSize, iconSize, type IconSize, } from './tokens';
2
- export { ThemeProvider, useTheme, themes, applyCastTheme, type Theme, type ThemeProviderProps, type CastThemeFile, type CastThemeProps, type DensityTheme, type ComponentTokens, type ButtonSizeTokens, type ButtonThemeTokens, type DialogSizeTokens, type DialogThemeTokens, type InputSizeTokens, type InputThemeTokens, type SelectContentTokens, type SelectOptionTokens, type SelectGroupTokens, type SelectSeparatorTokens, type SelectThemeTokens, type ListItemTokens, type ListSubheaderTokens, type ListThemeTokens, type CheckboxSizeTokens, type CheckboxThemeTokens, type AlertSizeTokens, type AlertThemeTokens, type ToggleSizeTokens, type ToggleThemeTokens, type CardSizeTokens, type CardThemeTokens, type BadgeSizeTokens, type BadgeThemeTokens, type RadioSizeTokens, type RadioThemeTokens, type ToastSizeTokens, type ToastThemeTokens, type ChipSizeTokens, type ChipThemeTokens, type AvatarSizeTokens, type AvatarThemeTokens, type PopoverSizeTokens, type PopoverThemeTokens, type TooltipSizeTokens, type TooltipThemeTokens, type ProgressSizeTokens, type ProgressThemeTokens, type TabsSizeTokens, type TabsThemeTokens, type SpinnerSizeTokens, type SpinnerThemeTokens, type BottomSheetThemeTokens, type LinkSizeTokens, type LinkThemeTokens, type BreadcrumbsSizeTokens, type BreadcrumbsThemeTokens, type CodeBlockSizeTokens, type CodeBlockThemeTokens, type DrawerThemeTokens, type MenuItemTokens, type MenuGroupTokens, type MenuThemeTokens, type ToggleButtonGroupSizeTokens, type ToggleButtonGroupThemeTokens, type AppBarSizeTokens, type AppBarThemeTokens, type SliderSizeTokens, type SliderThemeTokens, type SpeedDialSizeTokens, type SpeedDialThemeTokens, type TableSizeTokens, type TableThemeTokens, type DeepPartial, } from './theme';
1
+ export { lightColors, darkColors, colorSchemes, intentColors, disabledColors, controlTokens, surfaceTokens, textTokens, overlayTokens, selectColors, menuColors, tagTokens, errorTokens, listColors, checkboxColors, toggleColors, progressColors, tabsColors, radioColors, avatarColors, skeletonColors, sliderColors, tableColors, fontFamily, fontWeight, label, title, body, heading, display, caption, type IntentName, type ProminenceName, type StateName, type ColorMode, type ColorScheme, type LabelSize, iconSize, type IconSize, breakpoints, breakpointOrder, resolveBreakpoint, resolveResponsiveValue, type Breakpoint, type BreakpointKey, duration, cycle, easing, easingBezier, spring, transition, feedback, loop, motionTokens, resolveMotion, type MotionTokens, type MotionTransition, type MotionOverrides, type MotionDurations, type MotionCycles, type EasingName, type EasingBezierPoints, type SpringConfig, } from './tokens';
2
+ export { ThemeProvider, useTheme, useMotion, themes, applyCastTheme, type Theme, type Motion, type ThemeProviderProps, type CastThemeFile, type CastThemeProps, type DensityTheme, type ComponentTokens, type ButtonSizeTokens, type ButtonThemeTokens, type DialogSizeTokens, type DialogThemeTokens, type InputSizeTokens, type InputThemeTokens, type SelectContentTokens, type SelectOptionTokens, type SelectGroupTokens, type SelectSeparatorTokens, type SelectThemeTokens, type ListItemTokens, type ListSubheaderTokens, type ListThemeTokens, type CheckboxSizeTokens, type CheckboxThemeTokens, type AlertSizeTokens, type AlertThemeTokens, type ToggleSizeTokens, type ToggleThemeTokens, type CardSizeTokens, type CardThemeTokens, type BadgeSizeTokens, type BadgeThemeTokens, type RadioSizeTokens, type RadioThemeTokens, type ToastSizeTokens, type ToastThemeTokens, type ChipSizeTokens, type ChipThemeTokens, type AvatarSizeTokens, type AvatarThemeTokens, type PopoverSizeTokens, type PopoverThemeTokens, type TooltipSizeTokens, type TooltipThemeTokens, type ProgressSizeTokens, type ProgressThemeTokens, type TabsSizeTokens, type TabsThemeTokens, type SpinnerSizeTokens, type SpinnerThemeTokens, type BottomSheetThemeTokens, type LinkSizeTokens, type LinkThemeTokens, type BreadcrumbsSizeTokens, type BreadcrumbsThemeTokens, type CodeBlockSizeTokens, type CodeBlockThemeTokens, type DrawerThemeTokens, type MenuItemTokens, type MenuGroupTokens, type MenuThemeTokens, type ToggleButtonGroupSizeTokens, type ToggleButtonGroupThemeTokens, type AppBarSizeTokens, type AppBarThemeTokens, type SliderSizeTokens, type SliderThemeTokens, type SpeedDialSizeTokens, type SpeedDialThemeTokens, type TableSizeTokens, type TableThemeTokens, type DeepPartial, } from './theme';
3
+ export { useBreakpoint, useMinWidth, useResponsiveValue, } from './hooks';
3
4
  export { Button, type ButtonProps, type ButtonSize } from './components/Button';
4
5
  export { Icon, type IconProps } from './components/Icon';
5
6
  export { Dialog, DialogContent, type DialogProps, type DialogContentProps, type DialogAction, type DialogSize, } from './components/Dialog';
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ListDivider = exports.ListSubheader = exports.ListItem = exports.List = exports.SelectDropdown = exports.SelectTag = exports.SelectSeparator = exports.SelectGroup = exports.SelectOption = exports.Select = exports.DialogContent = exports.Dialog = exports.Icon = exports.Button = exports.applyCastTheme = exports.themes = exports.useTheme = exports.ThemeProvider = exports.iconSize = exports.caption = exports.display = exports.heading = exports.body = exports.title = exports.label = exports.fontWeight = exports.fontFamily = exports.tableColors = exports.sliderColors = exports.skeletonColors = exports.avatarColors = exports.radioColors = exports.tabsColors = exports.progressColors = exports.toggleColors = exports.checkboxColors = exports.listColors = exports.errorTokens = exports.tagTokens = exports.menuColors = exports.selectColors = exports.overlayTokens = exports.textTokens = exports.surfaceTokens = exports.controlTokens = exports.disabledColors = exports.intentColors = exports.colorSchemes = exports.darkColors = exports.lightColors = void 0;
4
- exports.Autocomplete = exports.TableCell = exports.TableRow = exports.TableBody = exports.TableHead = exports.Table = exports.SpeedDialAction = exports.SpeedDial = exports.Slider = exports.AppBar = exports.ToggleButton = exports.ToggleButtonGroup = exports.MenuContent = exports.MenuLabel = exports.MenuDivider = exports.MenuItem = exports.Menu = exports.DrawerContent = exports.Drawer = exports.CodeBlock = exports.Breadcrumb = exports.Breadcrumbs = exports.Backdrop = exports.Link = exports.AccordionItem = exports.Accordion = exports.Tab = exports.Tabs = exports.BottomSheetContent = exports.BottomSheet = exports.Spinner = exports.Progress = exports.Text = exports.Tooltip = exports.Skeleton = exports.Popover = exports.Avatar = exports.Divider = exports.Chip = exports.Toast = exports.RadioGroup = exports.Radio = exports.Input = exports.Badge = exports.Card = exports.Toggle = exports.Alert = exports.Checkbox = void 0;
3
+ exports.themes = exports.useMotion = exports.useTheme = exports.ThemeProvider = exports.resolveMotion = exports.motionTokens = exports.loop = exports.feedback = exports.transition = exports.spring = exports.easingBezier = exports.easing = exports.cycle = exports.duration = exports.resolveResponsiveValue = exports.resolveBreakpoint = exports.breakpointOrder = exports.breakpoints = exports.iconSize = exports.caption = exports.display = exports.heading = exports.body = exports.title = exports.label = exports.fontWeight = exports.fontFamily = exports.tableColors = exports.sliderColors = exports.skeletonColors = exports.avatarColors = exports.radioColors = exports.tabsColors = exports.progressColors = exports.toggleColors = exports.checkboxColors = exports.listColors = exports.errorTokens = exports.tagTokens = exports.menuColors = exports.selectColors = exports.overlayTokens = exports.textTokens = exports.surfaceTokens = exports.controlTokens = exports.disabledColors = exports.intentColors = exports.colorSchemes = exports.darkColors = exports.lightColors = void 0;
4
+ exports.Menu = exports.DrawerContent = exports.Drawer = exports.CodeBlock = exports.Breadcrumb = exports.Breadcrumbs = exports.Backdrop = exports.Link = exports.AccordionItem = exports.Accordion = exports.Tab = exports.Tabs = exports.BottomSheetContent = exports.BottomSheet = exports.Spinner = exports.Progress = exports.Text = exports.Tooltip = exports.Skeleton = exports.Popover = exports.Avatar = exports.Divider = exports.Chip = exports.Toast = exports.RadioGroup = exports.Radio = exports.Input = exports.Badge = exports.Card = exports.Toggle = exports.Alert = exports.Checkbox = exports.ListDivider = exports.ListSubheader = exports.ListItem = exports.List = exports.SelectDropdown = exports.SelectTag = exports.SelectSeparator = exports.SelectGroup = exports.SelectOption = exports.Select = exports.DialogContent = exports.Dialog = exports.Icon = exports.Button = exports.useResponsiveValue = exports.useMinWidth = exports.useBreakpoint = exports.applyCastTheme = void 0;
5
+ exports.Autocomplete = exports.TableCell = exports.TableRow = exports.TableBody = exports.TableHead = exports.Table = exports.SpeedDialAction = exports.SpeedDial = exports.Slider = exports.AppBar = exports.ToggleButton = exports.ToggleButtonGroup = exports.MenuContent = exports.MenuLabel = exports.MenuDivider = exports.MenuItem = void 0;
5
6
  // Cast UI — Cross-platform design system component library
6
7
  //
7
8
  // Tokens
@@ -38,12 +39,32 @@ Object.defineProperty(exports, "heading", { enumerable: true, get: function () {
38
39
  Object.defineProperty(exports, "display", { enumerable: true, get: function () { return tokens_1.display; } });
39
40
  Object.defineProperty(exports, "caption", { enumerable: true, get: function () { return tokens_1.caption; } });
40
41
  Object.defineProperty(exports, "iconSize", { enumerable: true, get: function () { return tokens_1.iconSize; } });
42
+ Object.defineProperty(exports, "breakpoints", { enumerable: true, get: function () { return tokens_1.breakpoints; } });
43
+ Object.defineProperty(exports, "breakpointOrder", { enumerable: true, get: function () { return tokens_1.breakpointOrder; } });
44
+ Object.defineProperty(exports, "resolveBreakpoint", { enumerable: true, get: function () { return tokens_1.resolveBreakpoint; } });
45
+ Object.defineProperty(exports, "resolveResponsiveValue", { enumerable: true, get: function () { return tokens_1.resolveResponsiveValue; } });
46
+ Object.defineProperty(exports, "duration", { enumerable: true, get: function () { return tokens_1.duration; } });
47
+ Object.defineProperty(exports, "cycle", { enumerable: true, get: function () { return tokens_1.cycle; } });
48
+ Object.defineProperty(exports, "easing", { enumerable: true, get: function () { return tokens_1.easing; } });
49
+ Object.defineProperty(exports, "easingBezier", { enumerable: true, get: function () { return tokens_1.easingBezier; } });
50
+ Object.defineProperty(exports, "spring", { enumerable: true, get: function () { return tokens_1.spring; } });
51
+ Object.defineProperty(exports, "transition", { enumerable: true, get: function () { return tokens_1.transition; } });
52
+ Object.defineProperty(exports, "feedback", { enumerable: true, get: function () { return tokens_1.feedback; } });
53
+ Object.defineProperty(exports, "loop", { enumerable: true, get: function () { return tokens_1.loop; } });
54
+ Object.defineProperty(exports, "motionTokens", { enumerable: true, get: function () { return tokens_1.motionTokens; } });
55
+ Object.defineProperty(exports, "resolveMotion", { enumerable: true, get: function () { return tokens_1.resolveMotion; } });
41
56
  // Theme
42
57
  var theme_1 = require("./theme");
43
58
  Object.defineProperty(exports, "ThemeProvider", { enumerable: true, get: function () { return theme_1.ThemeProvider; } });
44
59
  Object.defineProperty(exports, "useTheme", { enumerable: true, get: function () { return theme_1.useTheme; } });
60
+ Object.defineProperty(exports, "useMotion", { enumerable: true, get: function () { return theme_1.useMotion; } });
45
61
  Object.defineProperty(exports, "themes", { enumerable: true, get: function () { return theme_1.themes; } });
46
62
  Object.defineProperty(exports, "applyCastTheme", { enumerable: true, get: function () { return theme_1.applyCastTheme; } });
63
+ // Hooks
64
+ var hooks_1 = require("./hooks");
65
+ Object.defineProperty(exports, "useBreakpoint", { enumerable: true, get: function () { return hooks_1.useBreakpoint; } });
66
+ Object.defineProperty(exports, "useMinWidth", { enumerable: true, get: function () { return hooks_1.useMinWidth; } });
67
+ Object.defineProperty(exports, "useResponsiveValue", { enumerable: true, get: function () { return hooks_1.useResponsiveValue; } });
47
68
  // Components
48
69
  var Button_1 = require("./components/Button");
49
70
  Object.defineProperty(exports, "Button", { enumerable: true, get: function () { return Button_1.Button; } });
@@ -39,6 +39,7 @@
39
39
  import React from 'react';
40
40
  import { intentColors as defaultIntentColors } from '../tokens/colors';
41
41
  import type { ColorMode, ColorScheme, IntentName } from '../tokens/colors';
42
+ import type { MotionTokens, MotionOverrides } from '../tokens/motion';
42
43
  import type { DensityTheme, ComponentTokens, DeepPartial } from './types';
43
44
  type IntentColorMap = typeof defaultIntentColors;
44
45
  export type Theme = {
@@ -52,6 +53,8 @@ export type Theme = {
52
53
  colors: IntentColorMap;
53
54
  /** Disabled colours of the active scheme — kept for backwards compatibility. */
54
55
  disabledColors: ColorScheme['disabled'];
56
+ /** Motion tokens — animation durations, easings, springs. Constant across density and colour mode. */
57
+ motion: MotionTokens;
55
58
  };
56
59
  export type ThemeProviderProps = {
57
60
  /** Density theme — controls spacing and padding across all components. */
@@ -71,9 +74,17 @@ export type ThemeProviderProps = {
71
74
  * Usually you don't set this by hand — `applyCastTheme` builds it for you.
72
75
  */
73
76
  scheme?: DeepPartial<ColorScheme>;
77
+ /**
78
+ * Primitive-level motion overrides — durations, cycle lengths, easing
79
+ * beziers, springs. Semantic roles (transition/feedback/loop) are rebuilt
80
+ * from these, so one duration change flows into every role that uses it.
81
+ * Usually you don't set this by hand — `applyCastTheme` maps a
82
+ * cast-theme.json `motion` block onto it.
83
+ */
84
+ motion?: MotionOverrides;
74
85
  children: React.ReactNode;
75
86
  };
76
- export declare function ThemeProvider({ density, colorMode, colors, scheme: schemeOverride, children, }: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
87
+ export declare function ThemeProvider({ density, colorMode, colors, scheme: schemeOverride, motion: motionOverrides, children, }: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
77
88
  /**
78
89
  * Access the current theme — density tokens, intent colours, and component tokens.
79
90
  * Must be called within a ThemeProvider; falls back to the "default" density if not.