@newtonedev/components 0.1.0 → 0.1.2

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.
Files changed (181) hide show
  1. package/dist/AppShell/AppShell.d.ts +4 -0
  2. package/dist/AppShell/AppShell.d.ts.map +1 -0
  3. package/dist/AppShell/AppShell.styles.d.ts +16 -0
  4. package/dist/AppShell/AppShell.styles.d.ts.map +1 -0
  5. package/dist/AppShell/AppShell.types.d.ts +8 -0
  6. package/dist/AppShell/AppShell.types.d.ts.map +1 -0
  7. package/dist/AppShell/index.d.ts +3 -0
  8. package/dist/AppShell/index.d.ts.map +1 -0
  9. package/dist/Button/Button.d.ts +9 -4
  10. package/dist/Button/Button.d.ts.map +1 -1
  11. package/dist/Button/Button.styles.d.ts +33 -26
  12. package/dist/Button/Button.styles.d.ts.map +1 -1
  13. package/dist/Button/Button.types.d.ts +17 -2
  14. package/dist/Button/Button.types.d.ts.map +1 -1
  15. package/dist/ColorScaleSlider/ColorScaleSlider.d.ts +13 -0
  16. package/dist/ColorScaleSlider/ColorScaleSlider.d.ts.map +1 -0
  17. package/dist/ColorScaleSlider/ColorScaleSlider.styles.d.ts +54 -0
  18. package/dist/ColorScaleSlider/ColorScaleSlider.styles.d.ts.map +1 -0
  19. package/dist/ColorScaleSlider/ColorScaleSlider.types.d.ts +25 -0
  20. package/dist/ColorScaleSlider/ColorScaleSlider.types.d.ts.map +1 -0
  21. package/dist/ColorScaleSlider/index.d.ts +3 -0
  22. package/dist/ColorScaleSlider/index.d.ts.map +1 -0
  23. package/dist/Frame/Frame.d.ts +48 -0
  24. package/dist/Frame/Frame.d.ts.map +1 -0
  25. package/dist/Frame/Frame.styles.d.ts +39 -0
  26. package/dist/Frame/Frame.styles.d.ts.map +1 -0
  27. package/dist/Frame/Frame.types.d.ts +115 -0
  28. package/dist/Frame/Frame.types.d.ts.map +1 -0
  29. package/dist/Frame/Frame.utils.d.ts +39 -0
  30. package/dist/Frame/Frame.utils.d.ts.map +1 -0
  31. package/dist/Frame/index.d.ts +4 -0
  32. package/dist/Frame/index.d.ts.map +1 -0
  33. package/dist/HueSlider/HueSlider.d.ts +1 -1
  34. package/dist/HueSlider/HueSlider.d.ts.map +1 -1
  35. package/dist/HueSlider/HueSlider.styles.d.ts +47 -5
  36. package/dist/HueSlider/HueSlider.styles.d.ts.map +1 -1
  37. package/dist/HueSlider/HueSlider.types.d.ts +1 -0
  38. package/dist/HueSlider/HueSlider.types.d.ts.map +1 -1
  39. package/dist/Icon/Icon.d.ts +36 -0
  40. package/dist/Icon/Icon.d.ts.map +1 -0
  41. package/dist/Navbar/Navbar.d.ts +4 -0
  42. package/dist/Navbar/Navbar.d.ts.map +1 -0
  43. package/dist/Navbar/Navbar.styles.d.ts +31 -0
  44. package/dist/Navbar/Navbar.styles.d.ts.map +1 -0
  45. package/dist/Navbar/Navbar.types.d.ts +14 -0
  46. package/dist/Navbar/Navbar.types.d.ts.map +1 -0
  47. package/dist/Navbar/index.d.ts +3 -0
  48. package/dist/Navbar/index.d.ts.map +1 -0
  49. package/dist/Popover/Popover.d.ts +4 -0
  50. package/dist/Popover/Popover.d.ts.map +1 -0
  51. package/dist/Popover/Popover.styles.d.ts +9 -0
  52. package/dist/Popover/Popover.styles.d.ts.map +1 -0
  53. package/dist/Popover/Popover.types.d.ts +37 -0
  54. package/dist/Popover/Popover.types.d.ts.map +1 -0
  55. package/dist/Popover/index.d.ts +4 -0
  56. package/dist/Popover/index.d.ts.map +1 -0
  57. package/dist/Popover/usePopover.d.ts +3 -0
  58. package/dist/Popover/usePopover.d.ts.map +1 -0
  59. package/dist/Select/Select.d.ts +1 -8
  60. package/dist/Select/Select.d.ts.map +1 -1
  61. package/dist/Select/Select.styles.d.ts +32 -5
  62. package/dist/Select/Select.styles.d.ts.map +1 -1
  63. package/dist/Select/Select.types.d.ts +25 -1
  64. package/dist/Select/Select.types.d.ts.map +1 -1
  65. package/dist/Select/SelectOption.d.ts +13 -0
  66. package/dist/Select/SelectOption.d.ts.map +1 -0
  67. package/dist/Select/useSelect.d.ts +15 -0
  68. package/dist/Select/useSelect.d.ts.map +1 -0
  69. package/dist/Sidebar/Sidebar.d.ts +4 -0
  70. package/dist/Sidebar/Sidebar.d.ts.map +1 -0
  71. package/dist/Sidebar/Sidebar.styles.d.ts +31 -0
  72. package/dist/Sidebar/Sidebar.styles.d.ts.map +1 -0
  73. package/dist/Sidebar/Sidebar.types.d.ts +14 -0
  74. package/dist/Sidebar/Sidebar.types.d.ts.map +1 -0
  75. package/dist/Sidebar/index.d.ts +3 -0
  76. package/dist/Sidebar/index.d.ts.map +1 -0
  77. package/dist/Slider/Slider.d.ts +1 -1
  78. package/dist/Slider/Slider.d.ts.map +1 -1
  79. package/dist/Slider/Slider.styles.d.ts +48 -8
  80. package/dist/Slider/Slider.styles.d.ts.map +1 -1
  81. package/dist/Slider/Slider.types.d.ts +1 -0
  82. package/dist/Slider/Slider.types.d.ts.map +1 -1
  83. package/dist/TextInput/TextInput.styles.d.ts +3 -1
  84. package/dist/TextInput/TextInput.styles.d.ts.map +1 -1
  85. package/dist/Toggle/Toggle.styles.d.ts +2 -1
  86. package/dist/Toggle/Toggle.styles.d.ts.map +1 -1
  87. package/dist/fonts/GoogleFontLoader.d.ts +19 -0
  88. package/dist/fonts/GoogleFontLoader.d.ts.map +1 -0
  89. package/dist/fonts/IconFontLoader.d.ts +13 -0
  90. package/dist/fonts/IconFontLoader.d.ts.map +1 -0
  91. package/dist/fonts/buildGoogleFontsUrl.d.ts +17 -0
  92. package/dist/fonts/buildGoogleFontsUrl.d.ts.map +1 -0
  93. package/dist/fonts/googleFonts.d.ts +20 -0
  94. package/dist/fonts/googleFonts.d.ts.map +1 -0
  95. package/dist/index.cjs +2303 -205
  96. package/dist/index.cjs.map +1 -1
  97. package/dist/index.d.ts +27 -3
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +2279 -200
  100. package/dist/index.js.map +1 -1
  101. package/dist/registry/codegen.d.ts +11 -0
  102. package/dist/registry/codegen.d.ts.map +1 -0
  103. package/dist/registry/index.d.ts +4 -0
  104. package/dist/registry/index.d.ts.map +1 -0
  105. package/dist/registry/registry.d.ts +7 -0
  106. package/dist/registry/registry.d.ts.map +1 -0
  107. package/dist/registry/types.d.ts +32 -0
  108. package/dist/registry/types.d.ts.map +1 -0
  109. package/dist/theme/FrameContext.d.ts +24 -0
  110. package/dist/theme/FrameContext.d.ts.map +1 -0
  111. package/dist/theme/NewtoneProvider.d.ts.map +1 -1
  112. package/dist/theme/defaults.d.ts.map +1 -1
  113. package/dist/theme/types.d.ts +64 -1
  114. package/dist/theme/types.d.ts.map +1 -1
  115. package/dist/tokens/computeTokens.d.ts +55 -3
  116. package/dist/tokens/computeTokens.d.ts.map +1 -1
  117. package/dist/tokens/types.d.ts +52 -0
  118. package/dist/tokens/types.d.ts.map +1 -1
  119. package/dist/tokens/useTokens.d.ts +12 -9
  120. package/dist/tokens/useTokens.d.ts.map +1 -1
  121. package/package.json +1 -1
  122. package/src/AppShell/AppShell.styles.ts +20 -0
  123. package/src/AppShell/AppShell.tsx +17 -0
  124. package/src/AppShell/AppShell.types.ts +8 -0
  125. package/src/AppShell/index.ts +2 -0
  126. package/src/Button/Button.styles.ts +74 -41
  127. package/src/Button/Button.tsx +36 -17
  128. package/src/Button/Button.types.ts +20 -2
  129. package/src/Card/Card.styles.ts +2 -2
  130. package/src/ColorScaleSlider/ColorScaleSlider.styles.ts +60 -0
  131. package/src/ColorScaleSlider/ColorScaleSlider.tsx +156 -0
  132. package/src/ColorScaleSlider/ColorScaleSlider.types.ts +25 -0
  133. package/src/ColorScaleSlider/index.ts +2 -0
  134. package/src/Frame/Frame.styles.ts +213 -0
  135. package/src/Frame/Frame.tsx +242 -0
  136. package/src/Frame/Frame.types.ts +181 -0
  137. package/src/Frame/Frame.utils.ts +189 -0
  138. package/src/Frame/index.ts +21 -0
  139. package/src/HueSlider/HueSlider.styles.ts +58 -39
  140. package/src/HueSlider/HueSlider.tsx +97 -25
  141. package/src/HueSlider/HueSlider.types.ts +1 -0
  142. package/src/Icon/Icon.tsx +76 -0
  143. package/src/Navbar/Navbar.styles.ts +37 -0
  144. package/src/Navbar/Navbar.tsx +32 -0
  145. package/src/Navbar/Navbar.types.ts +14 -0
  146. package/src/Navbar/index.ts +2 -0
  147. package/src/Popover/Popover.styles.ts +39 -0
  148. package/src/Popover/Popover.tsx +103 -0
  149. package/src/Popover/Popover.types.ts +40 -0
  150. package/src/Popover/index.ts +3 -0
  151. package/src/Popover/usePopover.ts +26 -0
  152. package/src/Select/Select.styles.ts +49 -10
  153. package/src/Select/Select.tsx +127 -36
  154. package/src/Select/Select.types.ts +30 -1
  155. package/src/Select/SelectOption.tsx +104 -0
  156. package/src/Select/useSelect.ts +129 -0
  157. package/src/Sidebar/Sidebar.styles.ts +37 -0
  158. package/src/Sidebar/Sidebar.tsx +27 -0
  159. package/src/Sidebar/Sidebar.types.ts +14 -0
  160. package/src/Sidebar/index.ts +2 -0
  161. package/src/Slider/Slider.styles.ts +53 -25
  162. package/src/Slider/Slider.tsx +89 -24
  163. package/src/Slider/Slider.types.ts +1 -0
  164. package/src/TextInput/TextInput.styles.ts +9 -7
  165. package/src/Toggle/Toggle.styles.ts +4 -3
  166. package/src/fonts/GoogleFontLoader.tsx +63 -0
  167. package/src/fonts/IconFontLoader.tsx +49 -0
  168. package/src/fonts/buildGoogleFontsUrl.ts +31 -0
  169. package/src/fonts/googleFonts.ts +87 -0
  170. package/src/index.ts +70 -2
  171. package/src/registry/codegen.ts +132 -0
  172. package/src/registry/index.ts +17 -0
  173. package/src/registry/registry.ts +402 -0
  174. package/src/registry/types.ts +35 -0
  175. package/src/theme/FrameContext.tsx +29 -0
  176. package/src/theme/NewtoneProvider.tsx +9 -1
  177. package/src/theme/defaults.ts +51 -0
  178. package/src/theme/types.ts +66 -1
  179. package/src/tokens/computeTokens.ts +103 -46
  180. package/src/tokens/types.ts +52 -0
  181. package/src/tokens/useTokens.ts +30 -15
@@ -3,9 +3,10 @@ import { Pressable, Text } from 'react-native';
3
3
  import type { ButtonProps } from './Button.types';
4
4
  import { useTokens } from '../tokens/useTokens';
5
5
  import { getButtonStyles } from './Button.styles';
6
+ import { Icon } from '../Icon/Icon';
6
7
 
7
8
  /**
8
- * Button component with support for multiple variants and sizes.
9
+ * Button component with support for multiple variants, sizes, and optional icons.
9
10
  *
10
11
  * Automatically adapts to the current theme and mode from NewtoneProvider.
11
12
  *
@@ -18,13 +19,20 @@ import { getButtonStyles } from './Button.styles';
18
19
  *
19
20
  * @example
20
21
  * ```tsx
21
- * <Button variant="outline" size="lg" disabled>
22
- * Disabled button
22
+ * <Button icon="add" variant="primary" onPress={handleAdd}>
23
+ * New Item
23
24
  * </Button>
24
25
  * ```
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * <Button icon="delete" variant="ghost" size="sm" onPress={handleDelete} />
30
+ * ```
25
31
  */
26
32
  export function Button({
27
33
  children,
34
+ icon,
35
+ iconPosition = 'left',
28
36
  variant = 'primary',
29
37
  size = 'md',
30
38
  disabled = false,
@@ -32,11 +40,16 @@ export function Button({
32
40
  textStyle,
33
41
  ...pressableProps
34
42
  }: ButtonProps) {
35
- const tokens = useTokens(1); // Default elevation
43
+ const tokens = useTokens(1);
44
+ const isIconOnly = !!icon && !children;
45
+
46
+ const { styles, iconColor, iconSize } = React.useMemo(
47
+ () => getButtonStyles(tokens, variant, size, disabled, isIconOnly),
48
+ [tokens, variant, size, disabled, isIconOnly]
49
+ );
36
50
 
37
- const styles = React.useMemo(
38
- () => getButtonStyles(tokens, variant, size, disabled),
39
- [tokens, variant, size, disabled]
51
+ const renderIcon = () => (
52
+ <Icon name={icon!} size={iconSize} color={iconColor} />
40
53
  );
41
54
 
42
55
  return (
@@ -51,16 +64,22 @@ export function Button({
51
64
  {...pressableProps}
52
65
  >
53
66
  {({ pressed }) => (
54
- <Text
55
- style={[
56
- styles.text,
57
- pressed && !disabled && styles.textPressed,
58
- disabled && styles.textDisabled,
59
- ...(Array.isArray(textStyle) ? textStyle : [textStyle]),
60
- ]}
61
- >
62
- {children}
63
- </Text>
67
+ <>
68
+ {icon && iconPosition === 'left' && renderIcon()}
69
+ {children != null && (
70
+ <Text
71
+ style={[
72
+ styles.text,
73
+ pressed && !disabled && styles.textPressed,
74
+ disabled && styles.textDisabled,
75
+ ...(Array.isArray(textStyle) ? textStyle : [textStyle]),
76
+ ]}
77
+ >
78
+ {children}
79
+ </Text>
80
+ )}
81
+ {icon && iconPosition === 'right' && renderIcon()}
82
+ </>
64
83
  )}
65
84
  </Pressable>
66
85
  );
@@ -10,14 +10,32 @@ export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'outline';
10
10
  */
11
11
  export type ButtonSize = 'sm' | 'md' | 'lg';
12
12
 
13
+ /**
14
+ * Icon position relative to button text
15
+ */
16
+ export type ButtonIconPosition = 'left' | 'right';
17
+
13
18
  /**
14
19
  * Props for the Button component
15
20
  */
16
21
  export interface ButtonProps extends Omit<PressableProps, 'children' | 'style'> {
17
22
  /**
18
- * Button text or custom content
23
+ * Button text or custom content.
24
+ * Optional when `icon` is provided (renders icon-only button).
25
+ */
26
+ readonly children?: React.ReactNode;
27
+
28
+ /**
29
+ * Material Symbol icon name (e.g. 'add', 'delete', 'check').
30
+ * When provided without children, renders an icon-only button.
31
+ */
32
+ readonly icon?: string;
33
+
34
+ /**
35
+ * Position of the icon relative to text
36
+ * @default 'left'
19
37
  */
20
- readonly children: React.ReactNode;
38
+ readonly iconPosition?: ButtonIconPosition;
21
39
 
22
40
  /**
23
41
  * Visual variant
@@ -8,8 +8,8 @@ export function getCardStyles(tokens: ResolvedTokens, disabled: boolean) {
8
8
  backgroundColor: srgbToHex(tokens.background.srgb),
9
9
  borderWidth: 1,
10
10
  borderColor: srgbToHex(tokens.border.srgb),
11
- borderRadius: 8,
12
- padding: 16,
11
+ borderRadius: tokens.radius.lg,
12
+ padding: tokens.spacing.lg,
13
13
  opacity: disabled ? 0.5 : 1,
14
14
  },
15
15
  });
@@ -0,0 +1,60 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { srgbToHex } from 'newtone';
3
+ import type { ResolvedTokens } from '../tokens/types';
4
+
5
+ const TRACK_HEIGHT = 22;
6
+ export const THUMB_SIZE = 18;
7
+
8
+ export function getColorScaleSliderStyles(tokens: ResolvedTokens, disabled: boolean) {
9
+ return StyleSheet.create({
10
+ container: {
11
+ gap: tokens.spacing.xs,
12
+ opacity: disabled ? 0.5 : 1,
13
+ },
14
+ labelRow: {
15
+ flexDirection: 'row',
16
+ justifyContent: 'space-between',
17
+ alignItems: 'center',
18
+ },
19
+ label: {
20
+ fontFamily: tokens.typography.fonts.default,
21
+ fontSize: tokens.typography.size.sm,
22
+ fontWeight: tokens.typography.weight.semibold as any,
23
+ color: srgbToHex(tokens.textSecondary.srgb),
24
+ },
25
+ trackContainer: {
26
+ height: TRACK_HEIGHT + THUMB_SIZE,
27
+ justifyContent: 'center',
28
+ position: 'relative',
29
+ },
30
+ gradientTrack: {
31
+ position: 'absolute',
32
+ left: 0,
33
+ right: 0,
34
+ height: TRACK_HEIGHT,
35
+ borderRadius: TRACK_HEIGHT / 2,
36
+ flexDirection: 'row',
37
+ overflow: 'hidden',
38
+ },
39
+ segment: {
40
+ flex: 1,
41
+ },
42
+ thumb: {
43
+ position: 'absolute',
44
+ width: THUMB_SIZE,
45
+ height: THUMB_SIZE,
46
+ borderRadius: THUMB_SIZE / 2,
47
+ backgroundColor: '#ffffff',
48
+ borderWidth: 2,
49
+ borderColor: srgbToHex(tokens.border.srgb),
50
+ },
51
+ warning: {
52
+ fontFamily: tokens.typography.fonts.default,
53
+ fontSize: tokens.typography.size.xs,
54
+ fontWeight: tokens.typography.weight.medium as any,
55
+ color: srgbToHex(tokens.error.srgb),
56
+ },
57
+ });
58
+ }
59
+
60
+ export { TRACK_HEIGHT };
@@ -0,0 +1,156 @@
1
+ import React from 'react';
2
+ import { View, Text, PanResponder, Animated } from 'react-native';
3
+ import { srgbToHex } from 'newtone';
4
+ import type { ColorScaleSliderProps } from './ColorScaleSlider.types';
5
+ import { useTokens } from '../tokens/useTokens';
6
+ import { getColorScaleSliderStyles, THUMB_SIZE } from './ColorScaleSlider.styles';
7
+
8
+ /**
9
+ * Interactive color scale slider.
10
+ *
11
+ * Renders an array of colors as the track (lightest on left, darkest on right)
12
+ * with a draggable thumb. The value is a normalizedValue [0, 1] where
13
+ * 0 = darkest (right) and 1 = lightest (left).
14
+ *
15
+ * Used by PalettePanel for key color selection on non-neutral palettes.
16
+ */
17
+ export function ColorScaleSlider({
18
+ colors,
19
+ value,
20
+ onValueChange,
21
+ label,
22
+ warning,
23
+ trimEnds = false,
24
+ snap = false,
25
+ disabled = false,
26
+ animateValue = false,
27
+ style,
28
+ }: ColorScaleSliderProps) {
29
+ const tokens = useTokens(1);
30
+
31
+ const styles = React.useMemo(
32
+ () => getColorScaleSliderStyles(tokens, disabled),
33
+ [tokens, disabled]
34
+ );
35
+
36
+ const trackRef = React.useRef<View>(null);
37
+ const trackWidth = React.useRef(0);
38
+ const trackPageX = React.useRef(0);
39
+ const isDragging = React.useRef(false);
40
+ const thumbAnim = React.useRef(new Animated.Value(0)).current;
41
+
42
+ // Mutable refs to avoid stale closures in PanResponder
43
+ const onValueChangeRef = React.useRef(onValueChange);
44
+ const disabledRef = React.useRef(disabled);
45
+ const colorsLengthRef = React.useRef(colors.length);
46
+ const trimEndsRef = React.useRef(trimEnds);
47
+ const snapRef = React.useRef(snap);
48
+
49
+ React.useEffect(() => { onValueChangeRef.current = onValueChange; }, [onValueChange]);
50
+ React.useEffect(() => { disabledRef.current = disabled; }, [disabled]);
51
+ React.useEffect(() => { colorsLengthRef.current = colors.length; }, [colors.length]);
52
+ React.useEffect(() => { trimEndsRef.current = trimEnds; }, [trimEnds]);
53
+ React.useEffect(() => { snapRef.current = snap; }, [snap]);
54
+
55
+ const computeNv = React.useCallback((pageX: number) => {
56
+ const localX = pageX - trackPageX.current;
57
+ const ratio = Math.min(1, Math.max(0, localX / trackWidth.current));
58
+ const totalSteps = colorsLengthRef.current - 1;
59
+ const minNV = trimEndsRef.current ? 1 / totalSteps : 0;
60
+ const maxNV = trimEndsRef.current ? 1 - 1 / totalSteps : 1;
61
+ const range = maxNV - minNV;
62
+
63
+ // Left = lightest (maxNV), right = darkest (minNV)
64
+ let nv = maxNV - ratio * range;
65
+
66
+ if (snapRef.current && totalSteps > 0) {
67
+ const stepNv = 1 / totalSteps;
68
+ nv = Math.round(nv / stepNv) * stepNv;
69
+ nv = Math.min(maxNV, Math.max(minNV, nv));
70
+ }
71
+
72
+ return nv;
73
+ }, []);
74
+
75
+ const panResponder = React.useRef(
76
+ PanResponder.create({
77
+ onStartShouldSetPanResponder: () => !disabledRef.current,
78
+ onMoveShouldSetPanResponder: () => !disabledRef.current,
79
+ onPanResponderGrant: (evt) => {
80
+ isDragging.current = true;
81
+ onValueChangeRef.current(computeNv(evt.nativeEvent.pageX));
82
+ },
83
+ onPanResponderMove: (_evt, gestureState) => {
84
+ onValueChangeRef.current(computeNv(gestureState.moveX));
85
+ },
86
+ onPanResponderRelease: () => {
87
+ isDragging.current = false;
88
+ },
89
+ onPanResponderTerminate: () => {
90
+ isDragging.current = false;
91
+ },
92
+ })
93
+ ).current;
94
+
95
+ // Visible colors: trim interpolation endpoints if requested
96
+ const visibleColors = trimEnds ? colors.slice(1, -1) : colors;
97
+
98
+ // Thumb position mapped to the visible range
99
+ const totalSteps = colors.length - 1;
100
+ const minNV = trimEnds ? 1 / totalSteps : 0;
101
+ const maxNV = trimEnds ? 1 - 1 / totalSteps : 1;
102
+ const range = maxNV - minNV;
103
+ const clampedValue = value !== undefined
104
+ ? Math.min(maxNV, Math.max(minNV, value))
105
+ : (maxNV + minNV) / 2;
106
+ const ratio = range > 0 ? (maxNV - clampedValue) / range : 0.5;
107
+ const usableWidth = Math.max(0, trackWidth.current - THUMB_SIZE);
108
+ const thumbLeft = ratio * usableWidth;
109
+
110
+ // Sync animated thumb position: animate on prop-driven changes, instant during drag
111
+ React.useEffect(() => {
112
+ if (isDragging.current || !animateValue) {
113
+ thumbAnim.setValue(thumbLeft);
114
+ } else {
115
+ Animated.timing(thumbAnim, {
116
+ toValue: thumbLeft,
117
+ duration: 300,
118
+ useNativeDriver: false,
119
+ }).start();
120
+ }
121
+ }, [thumbLeft, animateValue, thumbAnim]);
122
+
123
+ return (
124
+ <View style={[styles.container, ...(Array.isArray(style) ? style : [style])]}>
125
+ {label && (
126
+ <View style={styles.labelRow}>
127
+ <Text style={styles.label}>{label}</Text>
128
+ </View>
129
+ )}
130
+ <View
131
+ ref={trackRef}
132
+ style={styles.trackContainer}
133
+ onLayout={(e) => {
134
+ trackWidth.current = e.nativeEvent.layout.width;
135
+ // Set thumb position immediately on layout (no animation)
136
+ const newUsableWidth = Math.max(0, e.nativeEvent.layout.width - THUMB_SIZE);
137
+ thumbAnim.setValue(ratio * newUsableWidth);
138
+ trackRef.current?.measure((_x, _y, _w, _h, pageX) => {
139
+ if (pageX != null) trackPageX.current = pageX;
140
+ });
141
+ }}
142
+ {...panResponder.panHandlers}
143
+ >
144
+ <View style={styles.gradientTrack}>
145
+ {visibleColors.map((color, i) => (
146
+ <View key={i} style={[styles.segment, { backgroundColor: srgbToHex(color.srgb) }]} />
147
+ ))}
148
+ </View>
149
+ <Animated.View style={[styles.thumb, { left: thumbAnim }]} />
150
+ </View>
151
+ {warning && (
152
+ <Text style={styles.warning}>{warning}</Text>
153
+ )}
154
+ </View>
155
+ );
156
+ }
@@ -0,0 +1,25 @@
1
+ import type { ViewStyle } from 'react-native';
2
+ import type { ColorResult } from 'newtone';
3
+
4
+ export interface ColorScaleSliderProps {
5
+ /** Preview colors for the track (index 0 = lightest, last = darkest) */
6
+ readonly colors: readonly ColorResult[];
7
+ /** Current normalizedValue [0, 1] (0 = darkest, 1 = lightest) */
8
+ readonly value?: number;
9
+ /** Called when the user drags the thumb */
10
+ readonly onValueChange: (normalizedValue: number) => void;
11
+ /** Optional label text above the slider */
12
+ readonly label?: string;
13
+ /** WCAG warning text shown below the slider */
14
+ readonly warning?: string;
15
+ /** Hide the first and last colors (interpolation endpoints, not selectable) */
16
+ readonly trimEnds?: boolean;
17
+ /** Snap thumb to the nearest color step instead of continuous dragging */
18
+ readonly snap?: boolean;
19
+ /** Whether the slider is disabled */
20
+ readonly disabled?: boolean;
21
+ /** Animate thumb position on prop-driven value changes (e.g., mode switch). Default: false */
22
+ readonly animateValue?: boolean;
23
+ /** Optional style override */
24
+ readonly style?: ViewStyle | ViewStyle[];
25
+ }
@@ -0,0 +1,2 @@
1
+ export { ColorScaleSlider } from './ColorScaleSlider';
2
+ export type { ColorScaleSliderProps } from './ColorScaleSlider.types';
@@ -0,0 +1,213 @@
1
+ import type { ViewStyle } from 'react-native';
2
+ import { StyleSheet } from 'react-native';
3
+ import { srgbToHex } from 'newtone';
4
+ import type { ResolvedTokens } from '../tokens/types';
5
+ import type { FrameElevation } from '../theme/types';
6
+ import type {
7
+ PaddingProp,
8
+ GapProp,
9
+ SizingMode,
10
+ Direction,
11
+ Alignment,
12
+ Justification,
13
+ RadiusProp,
14
+ LayoutMode,
15
+ } from './Frame.types';
16
+ import {
17
+ resolvePadding,
18
+ resolveGap,
19
+ resolveSizing,
20
+ resolveFlexDirection,
21
+ resolveAlignment,
22
+ resolveJustification,
23
+ resolveRadiusCorners,
24
+ hasPositiveRadius,
25
+ } from './Frame.utils';
26
+
27
+ // ── Input ────────────────────────────────────────────────────────
28
+
29
+ export interface FrameStyleInput {
30
+ readonly tokens: ResolvedTokens;
31
+ readonly frameElevation: FrameElevation;
32
+
33
+ // Layout
34
+ readonly layout?: LayoutMode;
35
+ readonly direction?: Direction;
36
+ readonly wrap?: boolean;
37
+ readonly reverse?: boolean;
38
+ readonly columns?: number;
39
+ readonly rows?: number;
40
+
41
+ // Alignment
42
+ readonly align?: Alignment;
43
+ readonly justify?: Justification;
44
+
45
+ // Spacing
46
+ readonly padding?: PaddingProp;
47
+ readonly gap?: GapProp;
48
+
49
+ // Sizing
50
+ readonly width?: SizingMode;
51
+ readonly height?: SizingMode;
52
+ readonly minWidth?: number;
53
+ readonly maxWidth?: number;
54
+ readonly minHeight?: number;
55
+ readonly maxHeight?: number;
56
+
57
+ // Appearance
58
+ readonly radius?: RadiusProp;
59
+ readonly bordered?: boolean;
60
+ readonly disabled?: boolean;
61
+ }
62
+
63
+ // ── Output ───────────────────────────────────────────────────────
64
+
65
+ export interface FrameStyles {
66
+ /** Main container style (ViewStyle) */
67
+ readonly container: ViewStyle;
68
+ /** Style applied when pressed (background shift) */
69
+ readonly pressed: ViewStyle;
70
+ /** Web-only CSS Grid properties (cast to ViewStyle at render) */
71
+ readonly gridWebStyle: React.CSSProperties | null;
72
+ /** Web-only inset box-shadow string for elevation -2 */
73
+ readonly insetBoxShadow: string | null;
74
+ }
75
+
76
+ // ── Builder ──────────────────────────────────────────────────────
77
+
78
+ export function getFrameStyles(input: FrameStyleInput): FrameStyles {
79
+ const {
80
+ tokens,
81
+ frameElevation,
82
+ layout = 'flex',
83
+ direction = 'vertical',
84
+ wrap = false,
85
+ reverse = false,
86
+ columns,
87
+ rows,
88
+ align,
89
+ justify,
90
+ padding,
91
+ gap,
92
+ width,
93
+ height,
94
+ minWidth,
95
+ maxWidth,
96
+ minHeight,
97
+ maxHeight,
98
+ radius,
99
+ bordered = false,
100
+ disabled = false,
101
+ } = input;
102
+
103
+ const container: Record<string, unknown> = {};
104
+
105
+ // ── Background & foreground ──
106
+ container.backgroundColor = srgbToHex(tokens.background.srgb);
107
+ container.color = srgbToHex(tokens.textPrimary.srgb);
108
+
109
+ // ── Layout mode ──
110
+ if (layout === 'flex') {
111
+ container.display = 'flex';
112
+ container.flexDirection = resolveFlexDirection(direction, reverse);
113
+ if (wrap) container.flexWrap = 'wrap';
114
+ }
115
+ // Grid: set flex as RN fallback; actual grid applied via gridWebStyle
116
+ if (layout === 'grid') {
117
+ container.display = 'flex';
118
+ container.flexDirection = 'row';
119
+ container.flexWrap = 'wrap';
120
+ }
121
+
122
+ // ── Alignment ──
123
+ if (align) container.alignItems = resolveAlignment(align);
124
+ if (justify) container.justifyContent = resolveJustification(justify);
125
+
126
+ // ── Padding ──
127
+ if (padding !== undefined) {
128
+ const p = resolvePadding(padding, tokens);
129
+ container.paddingTop = p.top;
130
+ container.paddingRight = p.right;
131
+ container.paddingBottom = p.bottom;
132
+ container.paddingLeft = p.left;
133
+ }
134
+
135
+ // ── Gap ──
136
+ if (gap !== undefined) {
137
+ const g = resolveGap(gap, tokens);
138
+ container.rowGap = g.rowGap;
139
+ container.columnGap = g.columnGap;
140
+ }
141
+
142
+ // ── Sizing ──
143
+ const sizing = resolveSizing(width, height);
144
+ Object.assign(container, sizing);
145
+
146
+ // ── Constraints ──
147
+ if (minWidth !== undefined) container.minWidth = minWidth;
148
+ if (maxWidth !== undefined) container.maxWidth = maxWidth;
149
+ if (minHeight !== undefined) container.minHeight = minHeight;
150
+ if (maxHeight !== undefined) container.maxHeight = maxHeight;
151
+
152
+ // ── Radius ──
153
+ if (radius !== undefined) {
154
+ const corners = resolveRadiusCorners(radius, tokens);
155
+ container.borderTopLeftRadius = corners.topLeft;
156
+ container.borderTopRightRadius = corners.topRight;
157
+ container.borderBottomLeftRadius = corners.bottomLeft;
158
+ container.borderBottomRightRadius = corners.bottomRight;
159
+
160
+ // Auto-clip when any corner has radius
161
+ if (hasPositiveRadius(corners)) {
162
+ container.overflow = 'hidden';
163
+ }
164
+ }
165
+
166
+ // ── Border ──
167
+ if (bordered) {
168
+ container.borderWidth = 1;
169
+ container.borderColor = srgbToHex(tokens.border.srgb);
170
+ }
171
+
172
+ // ── Outer shadow (elevation 2) ──
173
+ if (frameElevation === 2) {
174
+ container.shadowColor = '#000';
175
+ container.shadowOffset = { width: 0, height: 2 };
176
+ container.shadowOpacity = 0.12;
177
+ container.shadowRadius = 6;
178
+ // Android elevation prop (react-native-web maps to box-shadow)
179
+ container.elevation = 4;
180
+ }
181
+
182
+ // ── Disabled ──
183
+ if (disabled) {
184
+ container.opacity = 0.5;
185
+ }
186
+
187
+ // ── Pressed state (background shift to sunken) ──
188
+ const pressed = StyleSheet.create({
189
+ s: { backgroundColor: srgbToHex(tokens.backgroundSunken.srgb) },
190
+ }).s;
191
+
192
+ // ── Grid web style ──
193
+ let gridWebStyle: React.CSSProperties | null = null;
194
+ if (layout === 'grid') {
195
+ gridWebStyle = {
196
+ display: 'grid' as const,
197
+ gridTemplateColumns: columns ? `repeat(${columns}, 1fr)` : undefined,
198
+ gridTemplateRows: rows ? `repeat(${rows}, 1fr)` : undefined,
199
+ };
200
+ }
201
+
202
+ // ── Inset shadow (elevation -2) ──
203
+ const insetBoxShadow = frameElevation === -2
204
+ ? 'inset 0 2px 4px rgba(0,0,0,0.12)'
205
+ : null;
206
+
207
+ return {
208
+ container: StyleSheet.create({ c: container as ViewStyle }).c,
209
+ pressed,
210
+ gridWebStyle,
211
+ insetBoxShadow,
212
+ };
213
+ }