@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.
- package/dist/AppShell/AppShell.d.ts +4 -0
- package/dist/AppShell/AppShell.d.ts.map +1 -0
- package/dist/AppShell/AppShell.styles.d.ts +16 -0
- package/dist/AppShell/AppShell.styles.d.ts.map +1 -0
- package/dist/AppShell/AppShell.types.d.ts +8 -0
- package/dist/AppShell/AppShell.types.d.ts.map +1 -0
- package/dist/AppShell/index.d.ts +3 -0
- package/dist/AppShell/index.d.ts.map +1 -0
- package/dist/Button/Button.d.ts +9 -4
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.styles.d.ts +33 -26
- package/dist/Button/Button.styles.d.ts.map +1 -1
- package/dist/Button/Button.types.d.ts +17 -2
- package/dist/Button/Button.types.d.ts.map +1 -1
- package/dist/ColorScaleSlider/ColorScaleSlider.d.ts +13 -0
- package/dist/ColorScaleSlider/ColorScaleSlider.d.ts.map +1 -0
- package/dist/ColorScaleSlider/ColorScaleSlider.styles.d.ts +54 -0
- package/dist/ColorScaleSlider/ColorScaleSlider.styles.d.ts.map +1 -0
- package/dist/ColorScaleSlider/ColorScaleSlider.types.d.ts +25 -0
- package/dist/ColorScaleSlider/ColorScaleSlider.types.d.ts.map +1 -0
- package/dist/ColorScaleSlider/index.d.ts +3 -0
- package/dist/ColorScaleSlider/index.d.ts.map +1 -0
- package/dist/Frame/Frame.d.ts +48 -0
- package/dist/Frame/Frame.d.ts.map +1 -0
- package/dist/Frame/Frame.styles.d.ts +39 -0
- package/dist/Frame/Frame.styles.d.ts.map +1 -0
- package/dist/Frame/Frame.types.d.ts +115 -0
- package/dist/Frame/Frame.types.d.ts.map +1 -0
- package/dist/Frame/Frame.utils.d.ts +39 -0
- package/dist/Frame/Frame.utils.d.ts.map +1 -0
- package/dist/Frame/index.d.ts +4 -0
- package/dist/Frame/index.d.ts.map +1 -0
- package/dist/HueSlider/HueSlider.d.ts +1 -1
- package/dist/HueSlider/HueSlider.d.ts.map +1 -1
- package/dist/HueSlider/HueSlider.styles.d.ts +47 -5
- package/dist/HueSlider/HueSlider.styles.d.ts.map +1 -1
- package/dist/HueSlider/HueSlider.types.d.ts +1 -0
- package/dist/HueSlider/HueSlider.types.d.ts.map +1 -1
- package/dist/Icon/Icon.d.ts +36 -0
- package/dist/Icon/Icon.d.ts.map +1 -0
- package/dist/Navbar/Navbar.d.ts +4 -0
- package/dist/Navbar/Navbar.d.ts.map +1 -0
- package/dist/Navbar/Navbar.styles.d.ts +31 -0
- package/dist/Navbar/Navbar.styles.d.ts.map +1 -0
- package/dist/Navbar/Navbar.types.d.ts +14 -0
- package/dist/Navbar/Navbar.types.d.ts.map +1 -0
- package/dist/Navbar/index.d.ts +3 -0
- package/dist/Navbar/index.d.ts.map +1 -0
- package/dist/Popover/Popover.d.ts +4 -0
- package/dist/Popover/Popover.d.ts.map +1 -0
- package/dist/Popover/Popover.styles.d.ts +9 -0
- package/dist/Popover/Popover.styles.d.ts.map +1 -0
- package/dist/Popover/Popover.types.d.ts +37 -0
- package/dist/Popover/Popover.types.d.ts.map +1 -0
- package/dist/Popover/index.d.ts +4 -0
- package/dist/Popover/index.d.ts.map +1 -0
- package/dist/Popover/usePopover.d.ts +3 -0
- package/dist/Popover/usePopover.d.ts.map +1 -0
- package/dist/Select/Select.d.ts +1 -8
- package/dist/Select/Select.d.ts.map +1 -1
- package/dist/Select/Select.styles.d.ts +32 -5
- package/dist/Select/Select.styles.d.ts.map +1 -1
- package/dist/Select/Select.types.d.ts +25 -1
- package/dist/Select/Select.types.d.ts.map +1 -1
- package/dist/Select/SelectOption.d.ts +13 -0
- package/dist/Select/SelectOption.d.ts.map +1 -0
- package/dist/Select/useSelect.d.ts +15 -0
- package/dist/Select/useSelect.d.ts.map +1 -0
- package/dist/Sidebar/Sidebar.d.ts +4 -0
- package/dist/Sidebar/Sidebar.d.ts.map +1 -0
- package/dist/Sidebar/Sidebar.styles.d.ts +31 -0
- package/dist/Sidebar/Sidebar.styles.d.ts.map +1 -0
- package/dist/Sidebar/Sidebar.types.d.ts +14 -0
- package/dist/Sidebar/Sidebar.types.d.ts.map +1 -0
- package/dist/Sidebar/index.d.ts +3 -0
- package/dist/Sidebar/index.d.ts.map +1 -0
- package/dist/Slider/Slider.d.ts +1 -1
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.styles.d.ts +48 -8
- package/dist/Slider/Slider.styles.d.ts.map +1 -1
- package/dist/Slider/Slider.types.d.ts +1 -0
- package/dist/Slider/Slider.types.d.ts.map +1 -1
- package/dist/TextInput/TextInput.styles.d.ts +3 -1
- package/dist/TextInput/TextInput.styles.d.ts.map +1 -1
- package/dist/Toggle/Toggle.styles.d.ts +2 -1
- package/dist/Toggle/Toggle.styles.d.ts.map +1 -1
- package/dist/fonts/GoogleFontLoader.d.ts +19 -0
- package/dist/fonts/GoogleFontLoader.d.ts.map +1 -0
- package/dist/fonts/IconFontLoader.d.ts +13 -0
- package/dist/fonts/IconFontLoader.d.ts.map +1 -0
- package/dist/fonts/buildGoogleFontsUrl.d.ts +17 -0
- package/dist/fonts/buildGoogleFontsUrl.d.ts.map +1 -0
- package/dist/fonts/googleFonts.d.ts +20 -0
- package/dist/fonts/googleFonts.d.ts.map +1 -0
- package/dist/index.cjs +2303 -205
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +27 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2279 -200
- package/dist/index.js.map +1 -1
- package/dist/registry/codegen.d.ts +11 -0
- package/dist/registry/codegen.d.ts.map +1 -0
- package/dist/registry/index.d.ts +4 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/registry.d.ts +7 -0
- package/dist/registry/registry.d.ts.map +1 -0
- package/dist/registry/types.d.ts +32 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/theme/FrameContext.d.ts +24 -0
- package/dist/theme/FrameContext.d.ts.map +1 -0
- package/dist/theme/NewtoneProvider.d.ts.map +1 -1
- package/dist/theme/defaults.d.ts.map +1 -1
- package/dist/theme/types.d.ts +64 -1
- package/dist/theme/types.d.ts.map +1 -1
- package/dist/tokens/computeTokens.d.ts +55 -3
- package/dist/tokens/computeTokens.d.ts.map +1 -1
- package/dist/tokens/types.d.ts +52 -0
- package/dist/tokens/types.d.ts.map +1 -1
- package/dist/tokens/useTokens.d.ts +12 -9
- package/dist/tokens/useTokens.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/AppShell/AppShell.styles.ts +20 -0
- package/src/AppShell/AppShell.tsx +17 -0
- package/src/AppShell/AppShell.types.ts +8 -0
- package/src/AppShell/index.ts +2 -0
- package/src/Button/Button.styles.ts +74 -41
- package/src/Button/Button.tsx +36 -17
- package/src/Button/Button.types.ts +20 -2
- package/src/Card/Card.styles.ts +2 -2
- package/src/ColorScaleSlider/ColorScaleSlider.styles.ts +60 -0
- package/src/ColorScaleSlider/ColorScaleSlider.tsx +156 -0
- package/src/ColorScaleSlider/ColorScaleSlider.types.ts +25 -0
- package/src/ColorScaleSlider/index.ts +2 -0
- package/src/Frame/Frame.styles.ts +213 -0
- package/src/Frame/Frame.tsx +242 -0
- package/src/Frame/Frame.types.ts +181 -0
- package/src/Frame/Frame.utils.ts +189 -0
- package/src/Frame/index.ts +21 -0
- package/src/HueSlider/HueSlider.styles.ts +58 -39
- package/src/HueSlider/HueSlider.tsx +97 -25
- package/src/HueSlider/HueSlider.types.ts +1 -0
- package/src/Icon/Icon.tsx +76 -0
- package/src/Navbar/Navbar.styles.ts +37 -0
- package/src/Navbar/Navbar.tsx +32 -0
- package/src/Navbar/Navbar.types.ts +14 -0
- package/src/Navbar/index.ts +2 -0
- package/src/Popover/Popover.styles.ts +39 -0
- package/src/Popover/Popover.tsx +103 -0
- package/src/Popover/Popover.types.ts +40 -0
- package/src/Popover/index.ts +3 -0
- package/src/Popover/usePopover.ts +26 -0
- package/src/Select/Select.styles.ts +49 -10
- package/src/Select/Select.tsx +127 -36
- package/src/Select/Select.types.ts +30 -1
- package/src/Select/SelectOption.tsx +104 -0
- package/src/Select/useSelect.ts +129 -0
- package/src/Sidebar/Sidebar.styles.ts +37 -0
- package/src/Sidebar/Sidebar.tsx +27 -0
- package/src/Sidebar/Sidebar.types.ts +14 -0
- package/src/Sidebar/index.ts +2 -0
- package/src/Slider/Slider.styles.ts +53 -25
- package/src/Slider/Slider.tsx +89 -24
- package/src/Slider/Slider.types.ts +1 -0
- package/src/TextInput/TextInput.styles.ts +9 -7
- package/src/Toggle/Toggle.styles.ts +4 -3
- package/src/fonts/GoogleFontLoader.tsx +63 -0
- package/src/fonts/IconFontLoader.tsx +49 -0
- package/src/fonts/buildGoogleFontsUrl.ts +31 -0
- package/src/fonts/googleFonts.ts +87 -0
- package/src/index.ts +70 -2
- package/src/registry/codegen.ts +132 -0
- package/src/registry/index.ts +17 -0
- package/src/registry/registry.ts +402 -0
- package/src/registry/types.ts +35 -0
- package/src/theme/FrameContext.tsx +29 -0
- package/src/theme/NewtoneProvider.tsx +9 -1
- package/src/theme/defaults.ts +51 -0
- package/src/theme/types.ts +66 -1
- package/src/tokens/computeTokens.ts +103 -46
- package/src/tokens/types.ts +52 -0
- package/src/tokens/useTokens.ts +30 -15
package/src/Button/Button.tsx
CHANGED
|
@@ -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
|
|
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
|
|
22
|
-
*
|
|
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);
|
|
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
|
|
38
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
38
|
+
readonly iconPosition?: ButtonIconPosition;
|
|
21
39
|
|
|
22
40
|
/**
|
|
23
41
|
* Visual variant
|
package/src/Card/Card.styles.ts
CHANGED
|
@@ -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:
|
|
12
|
-
padding:
|
|
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,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
|
+
}
|