@castui/cast-ui 4.9.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
 
@@ -265,6 +275,18 @@ npm run build # compile to dist/
265
275
  | `npm run build-storybook` | Build static Storybook |
266
276
  | `npm run build` | TypeScript compilation to `dist/` |
267
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
+
268
290
  ## CI/CD
269
291
 
270
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'],
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
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, } 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
3
  export { useBreakpoint, useMinWidth, useResponsiveValue, } from './hooks';
4
4
  export { Button, type ButtonProps, type ButtonSize } from './components/Button';
5
5
  export { Icon, type IconProps } from './components/Icon';
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SelectGroup = exports.SelectOption = exports.Select = exports.DialogContent = exports.Dialog = exports.Icon = exports.Button = exports.useResponsiveValue = exports.useMinWidth = exports.useBreakpoint = exports.applyCastTheme = exports.themes = exports.useTheme = exports.ThemeProvider = 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.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 = exports.ListDivider = exports.ListSubheader = exports.ListItem = exports.List = exports.SelectDropdown = exports.SelectTag = exports.SelectSeparator = void 0;
5
- exports.Autocomplete = exports.TableCell = exports.TableRow = exports.TableBody = exports.TableHead = 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;
6
6
  // Cast UI — Cross-platform design system component library
7
7
  //
8
8
  // Tokens
@@ -43,10 +43,21 @@ Object.defineProperty(exports, "breakpoints", { enumerable: true, get: function
43
43
  Object.defineProperty(exports, "breakpointOrder", { enumerable: true, get: function () { return tokens_1.breakpointOrder; } });
44
44
  Object.defineProperty(exports, "resolveBreakpoint", { enumerable: true, get: function () { return tokens_1.resolveBreakpoint; } });
45
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; } });
46
56
  // Theme
47
57
  var theme_1 = require("./theme");
48
58
  Object.defineProperty(exports, "ThemeProvider", { enumerable: true, get: function () { return theme_1.ThemeProvider; } });
49
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; } });
50
61
  Object.defineProperty(exports, "themes", { enumerable: true, get: function () { return theme_1.themes; } });
51
62
  Object.defineProperty(exports, "applyCastTheme", { enumerable: true, get: function () { return theme_1.applyCastTheme; } });
52
63
  // Hooks
@@ -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.
@@ -44,6 +44,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
44
44
  const react_1 = require("react");
45
45
  const themes_1 = require("./themes");
46
46
  const colors_1 = require("../tokens/colors");
47
+ const motion_1 = require("../tokens/motion");
47
48
  // ---------------------------------------------------------------------------
48
49
  // Deep merge utility (for partial colour overrides)
49
50
  // ---------------------------------------------------------------------------
@@ -77,9 +78,10 @@ const defaultTheme = {
77
78
  scheme: colors_1.colorSchemes.light,
78
79
  colors: colors_1.colorSchemes.light.intents,
79
80
  disabledColors: colors_1.colorSchemes.light.disabled,
81
+ motion: motion_1.motionTokens,
80
82
  };
81
83
  const ThemeContext = (0, react_1.createContext)(defaultTheme);
82
- function ThemeProvider({ density = 'default', colorMode = 'light', colors, scheme: schemeOverride, children, }) {
84
+ function ThemeProvider({ density = 'default', colorMode = 'light', colors, scheme: schemeOverride, motion: motionOverrides, children, }) {
83
85
  const theme = (0, react_1.useMemo)(() => {
84
86
  const baseScheme = colors_1.colorSchemes[colorMode];
85
87
  const resolvedIntents = colors
@@ -98,8 +100,9 @@ function ThemeProvider({ density = 'default', colorMode = 'light', colors, schem
98
100
  scheme,
99
101
  colors: scheme.intents,
100
102
  disabledColors: scheme.disabled,
103
+ motion: (0, motion_1.resolveMotion)(motionOverrides),
101
104
  };
102
- }, [density, colorMode, colors, schemeOverride]);
105
+ }, [density, colorMode, colors, schemeOverride, motionOverrides]);
103
106
  return ((0, jsx_runtime_1.jsx)(ThemeContext.Provider, { value: theme, children: children }));
104
107
  }
105
108
  // ---------------------------------------------------------------------------
@@ -42,7 +42,7 @@ export type CastThemeFile = {
42
42
  name?: string;
43
43
  description?: string;
44
44
  generatedAt?: string;
45
- /** Theme-file format version emitted by the plugin (currently 3). */
45
+ /** Theme-file format version emitted by the plugin (version 4 adds motion). */
46
46
  version?: number;
47
47
  /** Optional explicit schema version for consumer validation. */
48
48
  schemaVersion?: number;
@@ -61,10 +61,40 @@ export type CastThemeFile = {
61
61
  }>>;
62
62
  typography?: Record<string, unknown>;
63
63
  shadows?: Record<string, unknown>;
64
+ /**
65
+ * Motion block exported from the kit's `motion` variable collection
66
+ * (cast-theme version 4+). `easing` carries cubic-bezier control points
67
+ * as [x1, y1, x2, y2]. Motion is mode-independent, so this block is not
68
+ * keyed by colour mode.
69
+ */
70
+ motion?: {
71
+ duration?: Record<string, number>;
72
+ cycle?: Record<string, number>;
73
+ easing?: Record<string, readonly number[]>;
74
+ spring?: Record<string, {
75
+ damping?: number;
76
+ stiffness?: number;
77
+ mass?: number;
78
+ }>;
79
+ feedback?: {
80
+ press?: {
81
+ scale?: number;
82
+ };
83
+ shake?: {
84
+ amplitude?: number;
85
+ };
86
+ };
87
+ loop?: {
88
+ pulse?: {
89
+ from?: number;
90
+ to?: number;
91
+ };
92
+ };
93
+ };
64
94
  [key: string]: unknown;
65
95
  };
66
96
  /** The subset of ThemeProvider props this helper produces. */
67
- export type CastThemeProps = Pick<ThemeProviderProps, 'colorMode' | 'colors' | 'scheme'>;
97
+ export type CastThemeProps = Pick<ThemeProviderProps, 'colorMode' | 'colors' | 'scheme' | 'motion'>;
68
98
  /**
69
99
  * Build ThemeProvider props from a cast-theme file for a given colour mode.
70
100
  *
@@ -33,6 +33,77 @@
33
33
  */
34
34
  Object.defineProperty(exports, "__esModule", { value: true });
35
35
  exports.applyCastTheme = applyCastTheme;
36
+ const EASING_NAMES = ['standard', 'entrance', 'exit', 'emphasized', 'linear'];
37
+ const DURATION_KEYS = ['instant', 'fast', 'base', 'slow'];
38
+ const CYCLE_KEYS = ['pulse', 'spin', 'sweep'];
39
+ function pickNumbers(source, keys) {
40
+ if (!source)
41
+ return undefined;
42
+ const out = {};
43
+ for (const key of keys) {
44
+ const value = source[key];
45
+ if (typeof value === 'number' && Number.isFinite(value))
46
+ out[key] = value;
47
+ }
48
+ return Object.keys(out).length > 0 ? out : undefined;
49
+ }
50
+ /** Map the file's `motion` block onto ThemeProvider motion overrides. */
51
+ function mapMotion(fileMotion) {
52
+ if (!fileMotion || typeof fileMotion !== 'object')
53
+ return undefined;
54
+ const out = {};
55
+ const durations = pickNumbers(fileMotion.duration, DURATION_KEYS);
56
+ if (durations)
57
+ out.duration = durations;
58
+ const cycles = pickNumbers(fileMotion.cycle, CYCLE_KEYS);
59
+ if (cycles)
60
+ out.cycle = cycles;
61
+ if (fileMotion.easing) {
62
+ const beziers = {};
63
+ for (const name of EASING_NAMES) {
64
+ const pts = fileMotion.easing[name];
65
+ if (Array.isArray(pts) &&
66
+ pts.length === 4 &&
67
+ pts.every((n) => typeof n === 'number' && Number.isFinite(n))) {
68
+ beziers[name] = [pts[0], pts[1], pts[2], pts[3]];
69
+ }
70
+ }
71
+ if (Object.keys(beziers).length > 0)
72
+ out.easingBezier = beziers;
73
+ }
74
+ const overlay = fileMotion.spring?.overlay;
75
+ if (overlay && typeof overlay === 'object') {
76
+ const springOut = {};
77
+ if (typeof overlay.damping === 'number')
78
+ springOut.damping = overlay.damping;
79
+ if (typeof overlay.stiffness === 'number')
80
+ springOut.stiffness = overlay.stiffness;
81
+ if (typeof overlay.mass === 'number')
82
+ springOut.mass = overlay.mass;
83
+ if (Object.keys(springOut).length > 0)
84
+ out.spring = { overlay: springOut };
85
+ }
86
+ const press = fileMotion.feedback?.press;
87
+ const shake = fileMotion.feedback?.shake;
88
+ const fb = {};
89
+ if (press && typeof press.scale === 'number')
90
+ fb.press = { scale: press.scale };
91
+ if (shake && typeof shake.amplitude === 'number')
92
+ fb.shake = { amplitude: shake.amplitude };
93
+ if (Object.keys(fb).length > 0)
94
+ out.feedback = fb;
95
+ const pulse = fileMotion.loop?.pulse;
96
+ if (pulse && typeof pulse === 'object') {
97
+ const pulseOut = {};
98
+ if (typeof pulse.from === 'number')
99
+ pulseOut.from = pulse.from;
100
+ if (typeof pulse.to === 'number')
101
+ pulseOut.to = pulse.to;
102
+ if (Object.keys(pulseOut).length > 0)
103
+ out.loop = { pulse: pulseOut };
104
+ }
105
+ return Object.keys(out).length > 0 ? out : undefined;
106
+ }
36
107
  /** Map the file's `text` block onto the scheme's `text` slots (matching keys only). */
37
108
  function mapText(fileText) {
38
109
  if (!fileText)
@@ -91,5 +162,6 @@ function applyCastTheme(theme, mode = 'light') {
91
162
  colorMode: mode,
92
163
  colors: intents,
93
164
  scheme: Object.keys(schemeOverride).length > 0 ? schemeOverride : undefined,
165
+ motion: mapMotion(theme?.motion),
94
166
  };
95
167
  }