@newtonedev/components 0.1.2 → 0.1.3
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/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.styles.d.ts +64 -1
- package/dist/Popover/Popover.styles.d.ts.map +1 -1
- package/dist/Select/Select.d.ts.map +1 -1
- package/dist/_COMPONENT_TEMPLATE/ComponentName.d.ts +70 -0
- package/dist/_COMPONENT_TEMPLATE/ComponentName.d.ts.map +1 -0
- package/dist/_COMPONENT_TEMPLATE/ComponentName.styles.d.ts +22 -0
- package/dist/_COMPONENT_TEMPLATE/ComponentName.styles.d.ts.map +1 -0
- package/dist/_COMPONENT_TEMPLATE/ComponentName.types.d.ts +45 -0
- package/dist/_COMPONENT_TEMPLATE/ComponentName.types.d.ts.map +1 -0
- package/dist/_COMPONENT_TEMPLATE/index.d.ts +3 -0
- package/dist/_COMPONENT_TEMPLATE/index.d.ts.map +1 -0
- package/dist/fonts/GoogleFontLoader.d.ts.map +1 -1
- package/dist/fonts/IconFontLoader.d.ts.map +1 -1
- package/dist/index.cjs +371 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +370 -76
- package/dist/index.js.map +1 -1
- package/dist/{Frame → primitives/Frame}/Frame.d.ts +1 -1
- package/dist/primitives/Frame/Frame.d.ts.map +1 -0
- package/dist/{Frame → primitives/Frame}/Frame.styles.d.ts +11 -2
- package/dist/primitives/Frame/Frame.styles.d.ts.map +1 -0
- package/dist/primitives/Frame/Frame.types.d.ts +240 -0
- package/dist/primitives/Frame/Frame.types.d.ts.map +1 -0
- package/dist/{Frame → primitives/Frame}/Frame.utils.d.ts +12 -12
- package/dist/primitives/Frame/Frame.utils.d.ts.map +1 -0
- package/dist/primitives/Frame/index.d.ts.map +1 -0
- package/dist/primitives/Icon/Icon.d.ts +17 -0
- package/dist/primitives/Icon/Icon.d.ts.map +1 -0
- package/dist/primitives/Icon/Icon.types.d.ts +55 -0
- package/dist/primitives/Icon/Icon.types.d.ts.map +1 -0
- package/dist/primitives/Icon/index.d.ts +3 -0
- package/dist/primitives/Icon/index.d.ts.map +1 -0
- package/dist/primitives/Text/Text.d.ts +17 -0
- package/dist/primitives/Text/Text.d.ts.map +1 -0
- package/dist/primitives/Text/Text.types.d.ts +85 -0
- package/dist/primitives/Text/Text.types.d.ts.map +1 -0
- package/dist/primitives/Text/index.d.ts +3 -0
- package/dist/primitives/Text/index.d.ts.map +1 -0
- package/dist/primitives/Wrapper/Wrapper.d.ts +29 -0
- package/dist/primitives/Wrapper/Wrapper.d.ts.map +1 -0
- package/dist/primitives/Wrapper/Wrapper.styles.d.ts +28 -0
- package/dist/primitives/Wrapper/Wrapper.styles.d.ts.map +1 -0
- package/dist/primitives/Wrapper/Wrapper.types.d.ts +113 -0
- package/dist/primitives/Wrapper/Wrapper.types.d.ts.map +1 -0
- package/dist/primitives/Wrapper/index.d.ts +3 -0
- package/dist/primitives/Wrapper/index.d.ts.map +1 -0
- package/dist/primitives/index.d.ts +12 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/useFocusVisible.d.ts +29 -0
- package/dist/primitives/useFocusVisible.d.ts.map +1 -0
- package/dist/theme/defaults.d.ts.map +1 -1
- package/dist/theme/types.d.ts +13 -6
- package/dist/theme/types.d.ts.map +1 -1
- package/dist/tokens/computeTokens.d.ts +13 -6
- package/dist/tokens/computeTokens.d.ts.map +1 -1
- package/dist/tokens/types.d.ts +16 -7
- package/dist/tokens/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Button/Button.styles.ts +9 -9
- package/src/Button/Button.tsx +1 -1
- package/src/Card/Card.styles.ts +1 -1
- package/src/ColorScaleSlider/ColorScaleSlider.styles.ts +1 -1
- package/src/HueSlider/HueSlider.styles.ts +1 -1
- package/src/Popover/Popover.styles.ts +5 -1
- package/src/Popover/Popover.tsx +3 -1
- package/src/Select/Select.styles.ts +9 -9
- package/src/Select/Select.tsx +2 -3
- package/src/Select/SelectOption.tsx +6 -6
- package/src/Slider/Slider.styles.ts +1 -1
- package/src/TextInput/TextInput.styles.ts +3 -3
- package/src/Toggle/Toggle.styles.ts +1 -1
- package/src/_COMPONENT_TEMPLATE/ComponentName.styles.ts +29 -0
- package/src/_COMPONENT_TEMPLATE/ComponentName.tsx +106 -0
- package/src/_COMPONENT_TEMPLATE/ComponentName.types.ts +86 -0
- package/src/_COMPONENT_TEMPLATE/index.ts +2 -0
- package/src/fonts/GoogleFontLoader.tsx +2 -0
- package/src/fonts/IconFontLoader.tsx +2 -0
- package/src/index.ts +22 -5
- package/src/{Frame → primitives/Frame}/Frame.styles.ts +46 -9
- package/src/{Frame → primitives/Frame}/Frame.tsx +90 -16
- package/src/primitives/Frame/Frame.types.ts +315 -0
- package/src/{Frame → primitives/Frame}/Frame.utils.ts +56 -20
- package/src/primitives/Icon/Icon.tsx +89 -0
- package/src/primitives/Icon/Icon.types.ts +70 -0
- package/src/primitives/Icon/index.ts +2 -0
- package/src/primitives/Text/Text.tsx +90 -0
- package/src/primitives/Text/Text.types.ts +108 -0
- package/src/primitives/Text/index.ts +10 -0
- package/src/primitives/Wrapper/Wrapper.styles.ts +113 -0
- package/src/primitives/Wrapper/Wrapper.tsx +104 -0
- package/src/primitives/Wrapper/Wrapper.types.ts +149 -0
- package/src/primitives/Wrapper/index.ts +2 -0
- package/src/primitives/index.ts +46 -0
- package/src/primitives/useFocusVisible.ts +102 -0
- package/src/theme/defaults.ts +13 -6
- package/src/theme/types.ts +13 -6
- package/src/tokens/computeTokens.ts +1 -1
- package/src/tokens/types.ts +16 -7
- package/dist/Frame/Frame.d.ts.map +0 -1
- package/dist/Frame/Frame.styles.d.ts.map +0 -1
- package/dist/Frame/Frame.types.d.ts +0 -115
- package/dist/Frame/Frame.types.d.ts.map +0 -1
- package/dist/Frame/Frame.utils.d.ts.map +0 -1
- package/dist/Frame/index.d.ts.map +0 -1
- package/dist/Icon/Icon.d.ts +0 -36
- package/dist/Icon/Icon.d.ts.map +0 -1
- package/src/Frame/Frame.types.ts +0 -181
- package/src/Icon/Icon.tsx +0 -76
- /package/dist/{Frame → primitives/Frame}/index.d.ts +0 -0
- /package/src/{Frame → primitives/Frame}/index.ts +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Template — Copy this directory when creating a new component.
|
|
3
|
+
*
|
|
4
|
+
* CROSS-PLATFORM RULES (enforced for all @newtonedev/components):
|
|
5
|
+
*
|
|
6
|
+
* 1. PRIMITIVES — Import ONLY from 'react-native':
|
|
7
|
+
* View, Text, Pressable, ScrollView, TextInput, PanResponder, Animated, StyleSheet
|
|
8
|
+
* NEVER use HTML elements (div, span, input, select, button, etc.)
|
|
9
|
+
*
|
|
10
|
+
* 2. STYLES — Always use StyleSheet.create() via a dedicated .styles.ts file.
|
|
11
|
+
* Never use plain JS objects or React.CSSProperties.
|
|
12
|
+
*
|
|
13
|
+
* 3. COLORS — Use useTokens() for all colors. Never hardcode hex values.
|
|
14
|
+
* Token palette: background, backgroundElevated, backgroundSunken,
|
|
15
|
+
* textPrimary, textSecondary, interactive, interactiveHover, interactiveActive, border
|
|
16
|
+
*
|
|
17
|
+
* 4. ACCESSIBILITY — Use accessibilityRole and accessibilityState (not aria-* attributes).
|
|
18
|
+
* react-native-web maps these to ARIA on the DOM automatically.
|
|
19
|
+
* Reference: Toggle.tsx (accessibilityRole="switch", accessibilityState={{ checked }})
|
|
20
|
+
*
|
|
21
|
+
* 5. SHADOWS — Use RN shadow properties:
|
|
22
|
+
* shadowColor, shadowOffset, shadowOpacity, shadowRadius, elevation
|
|
23
|
+
* NEVER use boxShadow as a CSS string. react-native-web maps RN shadows automatically.
|
|
24
|
+
* Reference: Frame.styles.ts, Popover.styles.ts
|
|
25
|
+
*
|
|
26
|
+
* 6. WEB-ONLY DOM — Guard with: if (typeof document === 'undefined') return;
|
|
27
|
+
* This covers both native (no document) and SSR (no document during render).
|
|
28
|
+
* Reference: GoogleFontLoader.tsx, Popover.tsx
|
|
29
|
+
*
|
|
30
|
+
* 7. WEB-ONLY EVENTS — Spread via typed-any pattern with comment:
|
|
31
|
+
* const webProps = { onKeyDown: handler } as any; // web-only
|
|
32
|
+
* <Pressable {...webProps} />
|
|
33
|
+
* These are silently ignored on native. Reference: Select.tsx, Popover.tsx
|
|
34
|
+
*
|
|
35
|
+
* 8. WEB-ONLY CSS — Properties like userSelect, fontVariationSettings, cursor
|
|
36
|
+
* are silently ignored on native. Mark with inline // web-only comment.
|
|
37
|
+
* Cast via `as TextStyle` or `as any` to satisfy TypeScript.
|
|
38
|
+
*
|
|
39
|
+
* 9. TYPES — All props must be readonly. Define in ComponentName.types.ts.
|
|
40
|
+
*
|
|
41
|
+
* 10. MEMOIZATION — Wrap style computation in useMemo keyed on tokens + props.
|
|
42
|
+
*
|
|
43
|
+
* 11. JSDOC — Every interface and prop must have JSDoc with @example blocks.
|
|
44
|
+
* Complex props (unions, objects) must explain all accepted forms inline.
|
|
45
|
+
* Developers must be able to use components via autocomplete alone,
|
|
46
|
+
* without consulting external documentation. See ComponentName.types.ts.
|
|
47
|
+
*
|
|
48
|
+
* 12. INLINE COMMENTS — Every logical block in .tsx and .styles.ts files must
|
|
49
|
+
* have a plain-language comment explaining what it does and why. Write for
|
|
50
|
+
* someone who has never seen React or React Native before. No jargon,
|
|
51
|
+
* no assumed knowledge. A reader should be able to understand the entire
|
|
52
|
+
* file top-to-bottom without external documentation.
|
|
53
|
+
* Reference: Frame.tsx, Text.tsx, Frame.styles.ts, Frame.utils.ts
|
|
54
|
+
*
|
|
55
|
+
* 13. STANDARD PROPS — Every component must accept testID, nativeID, and ref.
|
|
56
|
+
* testID maps to data-testid on web (used by testing libraries).
|
|
57
|
+
* nativeID maps to id on web (used for DOM anchors).
|
|
58
|
+
* ref is a regular React 19 prop (no forwardRef needed).
|
|
59
|
+
* Reference: Frame.tsx, Text.tsx, Icon.tsx, Wrapper.tsx
|
|
60
|
+
*
|
|
61
|
+
* 14. ACCESSIBILITY — Interactive components must accept accessibilityLabel
|
|
62
|
+
* and accessibilityHint. Use accessibilityState={{ disabled }} on Pressable.
|
|
63
|
+
* Decorative elements use importantForAccessibility="no-hide-descendants".
|
|
64
|
+
* Layout-only containers use accessibilityRole="none".
|
|
65
|
+
* Reference: Frame.tsx (interactive), Icon.tsx (decorative), Wrapper.tsx (layout)
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
import React, { useMemo } from 'react';
|
|
69
|
+
import { View } from 'react-native';
|
|
70
|
+
import type { ComponentNameProps } from './ComponentName.types';
|
|
71
|
+
import { getComponentNameStyles } from './ComponentName.styles';
|
|
72
|
+
import { useTokens } from '../tokens/useTokens';
|
|
73
|
+
|
|
74
|
+
export function ComponentName({
|
|
75
|
+
style,
|
|
76
|
+
// Standard props — every component must accept these
|
|
77
|
+
testID,
|
|
78
|
+
nativeID,
|
|
79
|
+
ref,
|
|
80
|
+
}: ComponentNameProps) {
|
|
81
|
+
// Get the current theme's design tokens (colors, fonts, spacing).
|
|
82
|
+
// The "1" is the default elevation level for token lookups.
|
|
83
|
+
const tokens = useTokens(1);
|
|
84
|
+
|
|
85
|
+
// Build the component's styles from the theme tokens.
|
|
86
|
+
// Wrapped in useMemo so it only recalculates when the tokens change,
|
|
87
|
+
// instead of recalculating on every render (which would be wasteful).
|
|
88
|
+
const styles = useMemo(
|
|
89
|
+
() => getComponentNameStyles(tokens),
|
|
90
|
+
[tokens]
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Render the component. User's custom styles are merged last
|
|
94
|
+
// so they can override the theme defaults.
|
|
95
|
+
return (
|
|
96
|
+
<View
|
|
97
|
+
ref={ref}
|
|
98
|
+
testID={testID}
|
|
99
|
+
nativeID={nativeID}
|
|
100
|
+
style={[styles.container, style]}
|
|
101
|
+
accessibilityRole="none"
|
|
102
|
+
>
|
|
103
|
+
{/* Component content */}
|
|
104
|
+
</View>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { View, ViewStyle } from 'react-native';
|
|
2
|
+
import type { ElevationLevel } from '../theme/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Props for ComponentName — [one-line description of what this component does].
|
|
6
|
+
*
|
|
7
|
+
* [Brief explanation of when to use this component and what it provides.]
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* // Basic usage
|
|
12
|
+
* <ComponentName>Content</ComponentName>
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* // With common prop combinations
|
|
18
|
+
* <ComponentName disabled elevation={2}>
|
|
19
|
+
* Advanced usage
|
|
20
|
+
* </ComponentName>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export interface ComponentNameProps {
|
|
24
|
+
/**
|
|
25
|
+
* [Describe what children this component expects.]
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* <ComponentName>Text or elements</ComponentName>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
readonly children?: React.ReactNode;
|
|
33
|
+
|
|
34
|
+
/** Additional styles applied to the container. */
|
|
35
|
+
readonly style?: ViewStyle;
|
|
36
|
+
|
|
37
|
+
/** Elevation level for token computation. @default 1 */
|
|
38
|
+
readonly elevation?: ElevationLevel;
|
|
39
|
+
|
|
40
|
+
/** Whether the component is disabled. @default false */
|
|
41
|
+
readonly disabled?: boolean;
|
|
42
|
+
|
|
43
|
+
// ── Standard Props (required for all components) ──
|
|
44
|
+
|
|
45
|
+
/** Test identifier — maps to `data-testid` on web. Used by testing libraries. */
|
|
46
|
+
readonly testID?: string;
|
|
47
|
+
|
|
48
|
+
/** DOM id — maps to `id` attribute on web. Used for anchors and scroll targets. */
|
|
49
|
+
readonly nativeID?: string;
|
|
50
|
+
|
|
51
|
+
/** Ref to the underlying View element. */
|
|
52
|
+
readonly ref?: React.Ref<View>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/*
|
|
56
|
+
* ═══════════════════════════════════════════════════════════════
|
|
57
|
+
* JSDOC CONVENTION (enforced for all @newtonedev/components):
|
|
58
|
+
*
|
|
59
|
+
* Every .types.ts file MUST follow these rules to support IDE
|
|
60
|
+
* auto-fill so developers never need external documentation:
|
|
61
|
+
*
|
|
62
|
+
* 1. INTERFACE-LEVEL JSDOC — Every exported interface must have:
|
|
63
|
+
* - One-line description of what the component does
|
|
64
|
+
* - Brief explanation of when/why to use it
|
|
65
|
+
* - At least 2 @example blocks showing common usage patterns
|
|
66
|
+
*
|
|
67
|
+
* 2. PROP-LEVEL JSDOC — Every prop must have:
|
|
68
|
+
* - Description of what it controls
|
|
69
|
+
* - @default tag if the prop has a default value
|
|
70
|
+
* - @example block for complex props (unions, objects, callbacks)
|
|
71
|
+
*
|
|
72
|
+
* 3. COMPLEX TYPES — Union types and object props must explain
|
|
73
|
+
* all accepted forms inline:
|
|
74
|
+
* - PaddingProp: uniform ('md'), axis ({x, y}), per-side ({top, ...})
|
|
75
|
+
* - SizingMode: 'hug', 'fill', or number
|
|
76
|
+
* - Callbacks: show the expected signature and return value
|
|
77
|
+
*
|
|
78
|
+
* 4. TYPE ALIASES — Exported type aliases (e.g., TextSize, Direction)
|
|
79
|
+
* must have a JSDoc comment explaining what they map to.
|
|
80
|
+
*
|
|
81
|
+
* 5. EXTERNAL REFERENCES — When a prop references external concepts
|
|
82
|
+
* (e.g., icon names, font families), include a @see link.
|
|
83
|
+
*
|
|
84
|
+
* Reference: Frame.types.ts, Icon.types.ts, Text.types.ts, Wrapper.types.ts
|
|
85
|
+
* ═══════════════════════════════════════════════════════════════
|
|
86
|
+
*/
|
|
@@ -22,6 +22,8 @@ export function GoogleFontLoader({ fonts }: GoogleFontLoaderProps) {
|
|
|
22
22
|
const linkRef = useRef<HTMLLinkElement | null>(null);
|
|
23
23
|
|
|
24
24
|
useEffect(() => {
|
|
25
|
+
// Web-only: on native, fonts are linked at build time (no DOM to inject <link> tags).
|
|
26
|
+
// Also guards against SSR where document is undefined.
|
|
25
27
|
if (typeof document === 'undefined') return;
|
|
26
28
|
|
|
27
29
|
const url = buildGoogleFontsUrl(fonts);
|
|
@@ -15,6 +15,8 @@ export function IconFontLoader({ icons }: IconFontLoaderProps) {
|
|
|
15
15
|
const linkRef = useRef<HTMLLinkElement | null>(null);
|
|
16
16
|
|
|
17
17
|
useEffect(() => {
|
|
18
|
+
// Web-only: on native, fonts are linked at build time (no DOM to inject <link> tags).
|
|
19
|
+
// Also guards against SSR where document is undefined.
|
|
18
20
|
if (typeof document === 'undefined') return;
|
|
19
21
|
|
|
20
22
|
const variantName = icons.variant.charAt(0).toUpperCase() + icons.variant.slice(1);
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,10 @@ export type { ButtonProps, ButtonVariant, ButtonSize, ButtonIconPosition } from
|
|
|
27
27
|
export { Card } from './Card/Card';
|
|
28
28
|
export type { CardProps } from './Card/Card.types';
|
|
29
29
|
|
|
30
|
-
export {
|
|
30
|
+
export { useFocusVisible } from './primitives/useFocusVisible';
|
|
31
|
+
export type { FocusVisibleResult } from './primitives/useFocusVisible';
|
|
32
|
+
|
|
33
|
+
export { Frame } from './primitives/Frame/Frame';
|
|
31
34
|
export type {
|
|
32
35
|
FrameProps,
|
|
33
36
|
SpacingToken,
|
|
@@ -46,8 +49,8 @@ export type {
|
|
|
46
49
|
Direction,
|
|
47
50
|
Alignment,
|
|
48
51
|
Justification,
|
|
49
|
-
} from './Frame/Frame.types';
|
|
50
|
-
export type { ResolvedCorners } from './Frame/Frame.utils';
|
|
52
|
+
} from './primitives/Frame/Frame.types';
|
|
53
|
+
export type { ResolvedCorners } from './primitives/Frame/Frame.utils';
|
|
51
54
|
|
|
52
55
|
export { TextInput } from './TextInput/TextInput';
|
|
53
56
|
export type { TextInputProps } from './TextInput/TextInput.types';
|
|
@@ -72,8 +75,22 @@ export type { HueSliderProps } from './HueSlider/HueSlider.types';
|
|
|
72
75
|
export { ColorScaleSlider } from './ColorScaleSlider/ColorScaleSlider';
|
|
73
76
|
export type { ColorScaleSliderProps } from './ColorScaleSlider/ColorScaleSlider.types';
|
|
74
77
|
|
|
75
|
-
export { Icon } from './Icon/Icon';
|
|
76
|
-
export type { IconProps } from './Icon/Icon';
|
|
78
|
+
export { Icon } from './primitives/Icon/Icon';
|
|
79
|
+
export type { IconProps } from './primitives/Icon/Icon.types';
|
|
80
|
+
|
|
81
|
+
export { Wrapper } from './primitives/Wrapper/Wrapper';
|
|
82
|
+
export type { WrapperProps } from './primitives/Wrapper/Wrapper.types';
|
|
83
|
+
|
|
84
|
+
export { Text } from './primitives/Text/Text';
|
|
85
|
+
export type {
|
|
86
|
+
TextProps,
|
|
87
|
+
TextSize,
|
|
88
|
+
TextWeight,
|
|
89
|
+
TextColor,
|
|
90
|
+
TextFont,
|
|
91
|
+
TextLineHeight,
|
|
92
|
+
TextAlign,
|
|
93
|
+
} from './primitives/Text/Text.types';
|
|
77
94
|
|
|
78
95
|
export { AppShell } from './AppShell/AppShell';
|
|
79
96
|
export type { AppShellProps } from './AppShell/AppShell.types';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ViewStyle } from 'react-native';
|
|
2
2
|
import { StyleSheet } from 'react-native';
|
|
3
3
|
import { srgbToHex } from 'newtone';
|
|
4
|
-
import type { ResolvedTokens } from '
|
|
5
|
-
import type { FrameElevation } from '
|
|
4
|
+
import type { ResolvedTokens } from '../../tokens/types';
|
|
5
|
+
import type { FrameElevation } from '../../theme/types';
|
|
6
6
|
import type {
|
|
7
7
|
PaddingProp,
|
|
8
8
|
GapProp,
|
|
@@ -75,6 +75,15 @@ export interface FrameStyles {
|
|
|
75
75
|
|
|
76
76
|
// ── Builder ──────────────────────────────────────────────────────
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Build all visual styles for a Frame.
|
|
80
|
+
*
|
|
81
|
+
* Takes the Frame's props + design tokens and produces:
|
|
82
|
+
* - container: the main style (background, layout, spacing, border, shadow, etc.)
|
|
83
|
+
* - pressed: style override applied when the user is pressing/clicking
|
|
84
|
+
* - gridWebStyle: CSS Grid properties (only works on web, null otherwise)
|
|
85
|
+
* - insetBoxShadow: inner shadow CSS string for sunken frames (web-only)
|
|
86
|
+
*/
|
|
78
87
|
export function getFrameStyles(input: FrameStyleInput): FrameStyles {
|
|
79
88
|
const {
|
|
80
89
|
tokens,
|
|
@@ -100,30 +109,40 @@ export function getFrameStyles(input: FrameStyleInput): FrameStyles {
|
|
|
100
109
|
disabled = false,
|
|
101
110
|
} = input;
|
|
102
111
|
|
|
112
|
+
// We build styles as a plain object first, then validate it through
|
|
113
|
+
// StyleSheet.create() at the end. Using Record<string, unknown> lets us
|
|
114
|
+
// set properties conditionally without TypeScript complaining.
|
|
103
115
|
const container: Record<string, unknown> = {};
|
|
104
116
|
|
|
105
117
|
// ── Background & foreground ──
|
|
118
|
+
// Set the surface color and default text color from the current theme.
|
|
106
119
|
container.backgroundColor = srgbToHex(tokens.background.srgb);
|
|
107
120
|
container.color = srgbToHex(tokens.textPrimary.srgb);
|
|
108
121
|
|
|
109
122
|
// ── Layout mode ──
|
|
110
123
|
if (layout === 'flex') {
|
|
124
|
+
// Standard flex layout: children arranged in a row or column.
|
|
111
125
|
container.display = 'flex';
|
|
112
126
|
container.flexDirection = resolveFlexDirection(direction, reverse);
|
|
113
127
|
if (wrap) container.flexWrap = 'wrap';
|
|
114
128
|
}
|
|
115
|
-
// Grid: set flex as RN fallback; actual grid applied via gridWebStyle
|
|
116
129
|
if (layout === 'grid') {
|
|
130
|
+
// Grid layout: uses CSS Grid on web, but React Native doesn't support grid.
|
|
131
|
+
// So we set flex as a fallback (wrapping row) for native platforms.
|
|
132
|
+
// The actual CSS Grid styles are applied separately via gridWebStyle below.
|
|
117
133
|
container.display = 'flex';
|
|
118
134
|
container.flexDirection = 'row';
|
|
119
135
|
container.flexWrap = 'wrap';
|
|
120
136
|
}
|
|
121
137
|
|
|
122
138
|
// ── Alignment ──
|
|
139
|
+
// Cross-axis: how children are positioned perpendicular to the main direction.
|
|
123
140
|
if (align) container.alignItems = resolveAlignment(align);
|
|
141
|
+
// Main-axis: how children are distributed along the main direction.
|
|
124
142
|
if (justify) container.justifyContent = resolveJustification(justify);
|
|
125
143
|
|
|
126
144
|
// ── Padding ──
|
|
145
|
+
// Convert spacing tokens (like 'md') or pixel values into actual padding.
|
|
127
146
|
if (padding !== undefined) {
|
|
128
147
|
const p = resolvePadding(padding, tokens);
|
|
129
148
|
container.paddingTop = p.top;
|
|
@@ -133,6 +152,7 @@ export function getFrameStyles(input: FrameStyleInput): FrameStyles {
|
|
|
133
152
|
}
|
|
134
153
|
|
|
135
154
|
// ── Gap ──
|
|
155
|
+
// Space between children (row gap for vertical stacks, column gap for horizontal).
|
|
136
156
|
if (gap !== undefined) {
|
|
137
157
|
const g = resolveGap(gap, tokens);
|
|
138
158
|
container.rowGap = g.rowGap;
|
|
@@ -140,16 +160,19 @@ export function getFrameStyles(input: FrameStyleInput): FrameStyles {
|
|
|
140
160
|
}
|
|
141
161
|
|
|
142
162
|
// ── Sizing ──
|
|
163
|
+
// Apply width/height settings: 'hug' (shrink), 'fill' (expand), or fixed pixels.
|
|
143
164
|
const sizing = resolveSizing(width, height);
|
|
144
165
|
Object.assign(container, sizing);
|
|
145
166
|
|
|
146
167
|
// ── Constraints ──
|
|
168
|
+
// Set min/max boundaries for width and height.
|
|
147
169
|
if (minWidth !== undefined) container.minWidth = minWidth;
|
|
148
170
|
if (maxWidth !== undefined) container.maxWidth = maxWidth;
|
|
149
171
|
if (minHeight !== undefined) container.minHeight = minHeight;
|
|
150
172
|
if (maxHeight !== undefined) container.maxHeight = maxHeight;
|
|
151
173
|
|
|
152
174
|
// ── Radius ──
|
|
175
|
+
// Round the corners of the Frame (e.g. for cards, buttons, pills).
|
|
153
176
|
if (radius !== undefined) {
|
|
154
177
|
const corners = resolveRadiusCorners(radius, tokens);
|
|
155
178
|
container.borderTopLeftRadius = corners.topLeft;
|
|
@@ -157,54 +180,68 @@ export function getFrameStyles(input: FrameStyleInput): FrameStyles {
|
|
|
157
180
|
container.borderBottomLeftRadius = corners.bottomLeft;
|
|
158
181
|
container.borderBottomRightRadius = corners.bottomRight;
|
|
159
182
|
|
|
160
|
-
//
|
|
183
|
+
// Clip overflowing content when corners are rounded,
|
|
184
|
+
// otherwise children would visually leak outside the rounded edges.
|
|
161
185
|
if (hasPositiveRadius(corners)) {
|
|
162
186
|
container.overflow = 'hidden';
|
|
163
187
|
}
|
|
164
188
|
}
|
|
165
189
|
|
|
166
190
|
// ── Border ──
|
|
191
|
+
// Add a thin border using the theme's border color.
|
|
167
192
|
if (bordered) {
|
|
168
193
|
container.borderWidth = 1;
|
|
169
194
|
container.borderColor = srgbToHex(tokens.border.srgb);
|
|
170
195
|
}
|
|
171
196
|
|
|
172
197
|
// ── Outer shadow (elevation 2) ──
|
|
198
|
+
// Add a subtle drop shadow to make the Frame look raised above the surface.
|
|
199
|
+
// These are React Native shadow properties — react-native-web converts them
|
|
200
|
+
// to CSS box-shadow automatically.
|
|
173
201
|
if (frameElevation === 2) {
|
|
174
202
|
container.shadowColor = '#000';
|
|
175
203
|
container.shadowOffset = { width: 0, height: 2 };
|
|
176
204
|
container.shadowOpacity = 0.12;
|
|
177
205
|
container.shadowRadius = 6;
|
|
178
|
-
|
|
179
|
-
container.elevation = 4;
|
|
206
|
+
container.elevation = 4; // Android-specific shadow depth
|
|
180
207
|
}
|
|
181
208
|
|
|
182
209
|
// ── Disabled ──
|
|
210
|
+
// Make the Frame look faded when disabled.
|
|
183
211
|
if (disabled) {
|
|
184
212
|
container.opacity = 0.5;
|
|
185
213
|
}
|
|
186
214
|
|
|
187
|
-
// ── Pressed state
|
|
215
|
+
// ── Pressed state ──
|
|
216
|
+
// When the user is pressing an interactive Frame, shift the background
|
|
217
|
+
// to a darker "sunken" shade to give visual feedback.
|
|
188
218
|
const pressed = StyleSheet.create({
|
|
189
219
|
s: { backgroundColor: srgbToHex(tokens.backgroundSunken.srgb) },
|
|
190
220
|
}).s;
|
|
191
221
|
|
|
192
|
-
// ── Grid web style ──
|
|
222
|
+
// ── Grid web style (web-only) ──
|
|
223
|
+
// CSS Grid only works in web browsers. On native platforms this stays null,
|
|
224
|
+
// and the flex fallback (set above) handles the layout instead.
|
|
193
225
|
let gridWebStyle: React.CSSProperties | null = null;
|
|
194
226
|
if (layout === 'grid') {
|
|
195
227
|
gridWebStyle = {
|
|
196
228
|
display: 'grid' as const,
|
|
229
|
+
// Divide into equal-width columns (e.g. 3 columns → "repeat(3, 1fr)").
|
|
197
230
|
gridTemplateColumns: columns ? `repeat(${columns}, 1fr)` : undefined,
|
|
198
231
|
gridTemplateRows: rows ? `repeat(${rows}, 1fr)` : undefined,
|
|
199
232
|
};
|
|
200
233
|
}
|
|
201
234
|
|
|
202
|
-
// ── Inset shadow (elevation -2) ──
|
|
235
|
+
// ── Inset shadow (elevation -2, web-only) ──
|
|
236
|
+
// Creates an inner shadow effect to make the Frame look sunken/recessed.
|
|
237
|
+
// Only used for the deepest sunken level (-2).
|
|
203
238
|
const insetBoxShadow = frameElevation === -2
|
|
204
239
|
? 'inset 0 2px 4px rgba(0,0,0,0.12)'
|
|
205
240
|
: null;
|
|
206
241
|
|
|
207
242
|
return {
|
|
243
|
+
// Validate and optimize the container styles through StyleSheet.create(),
|
|
244
|
+
// then extract the single style object with `.c`.
|
|
208
245
|
container: StyleSheet.create({ c: container as ViewStyle }).c,
|
|
209
246
|
pressed,
|
|
210
247
|
gridWebStyle,
|
|
@@ -2,16 +2,20 @@ import React, { useMemo } from 'react';
|
|
|
2
2
|
import { View, Pressable, Text } from 'react-native';
|
|
3
3
|
import type { ViewStyle, TextStyle } from 'react-native';
|
|
4
4
|
import type { FrameProps } from './Frame.types';
|
|
5
|
-
import type { ElevationLevel, FrameElevation } from '
|
|
5
|
+
import type { ElevationLevel, FrameElevation } from '../../theme/types';
|
|
6
6
|
import { srgbToHex } from 'newtone';
|
|
7
|
-
import { FrameContext, useFrameContext } from '
|
|
8
|
-
import { useNewtoneTheme } from '
|
|
9
|
-
import { computeTokens } from '
|
|
7
|
+
import { FrameContext, useFrameContext } from '../../theme/FrameContext';
|
|
8
|
+
import { useNewtoneTheme } from '../../theme/NewtoneProvider';
|
|
9
|
+
import { computeTokens } from '../../tokens/computeTokens';
|
|
10
10
|
import { getFrameStyles } from './Frame.styles';
|
|
11
|
+
import { useFocusVisible } from '../useFocusVisible';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* Wrap raw string/number children in <Text>
|
|
14
|
-
*
|
|
14
|
+
* Wrap raw string/number children in <Text> so they display correctly.
|
|
15
|
+
*
|
|
16
|
+
* In React Native, raw text like <View>"hello"</View> will crash on native
|
|
17
|
+
* and show console warnings on web. All text must be inside a <Text> element.
|
|
18
|
+
* This helper scans children and auto-wraps any bare strings or numbers.
|
|
15
19
|
*/
|
|
16
20
|
function wrapTextChildren(
|
|
17
21
|
children: React.ReactNode,
|
|
@@ -118,24 +122,39 @@ export function Frame({
|
|
|
118
122
|
onPress,
|
|
119
123
|
href,
|
|
120
124
|
disabled = false,
|
|
125
|
+
// Accessibility
|
|
126
|
+
accessibilityLabel,
|
|
127
|
+
accessibilityHint,
|
|
128
|
+
// Testing & platform
|
|
129
|
+
testID,
|
|
130
|
+
nativeID,
|
|
131
|
+
ref,
|
|
121
132
|
// Style override
|
|
122
133
|
style,
|
|
123
134
|
}: FrameProps) {
|
|
135
|
+
// Read the global theme configuration, current color mode (light/dark),
|
|
136
|
+
// and the default theme name from the nearest NewtoneProvider.
|
|
124
137
|
const { config, mode, theme: providerTheme } = useNewtoneTheme();
|
|
138
|
+
// Read the theme/elevation from the nearest parent Frame (if nested).
|
|
125
139
|
const parentFrameCtx = useFrameContext();
|
|
126
140
|
|
|
127
|
-
//
|
|
141
|
+
// Decide which theme to use. Priority: this Frame's prop > parent Frame > NewtoneProvider.
|
|
128
142
|
const resolvedTheme = theme ?? parentFrameCtx?.theme ?? providerTheme;
|
|
129
143
|
|
|
130
|
-
//
|
|
144
|
+
// The user-facing elevation (-2 to 2) controls visual depth (shadows, background).
|
|
131
145
|
const resolvedFrameElevation: FrameElevation = elevation ?? 0;
|
|
132
146
|
|
|
133
|
-
//
|
|
147
|
+
// Convert user-facing elevation to internal level (0-2) for token computation.
|
|
148
|
+
// When no elevation is set, inherit from parent Frame or default to 1.
|
|
134
149
|
const resolvedElevation: ElevationLevel = elevation !== undefined
|
|
135
150
|
? toElevationLevel(elevation)
|
|
136
151
|
: parentFrameCtx?.elevation ?? 1;
|
|
137
152
|
|
|
138
|
-
//
|
|
153
|
+
// Generate the design tokens (colors, spacing, fonts) for this Frame.
|
|
154
|
+
// Frame computes its own tokens instead of using useTokens() because
|
|
155
|
+
// useTokens() reads from FrameContext — but this Frame IS the provider,
|
|
156
|
+
// so it needs to compute fresh tokens from the resolved theme/elevation.
|
|
157
|
+
// Wrapped in useMemo so it only recalculates when the theme/mode/elevation changes.
|
|
139
158
|
const tokens = useMemo(() => {
|
|
140
159
|
const themeMapping = config.themes[resolvedTheme];
|
|
141
160
|
return computeTokens(
|
|
@@ -151,6 +170,8 @@ export function Frame({
|
|
|
151
170
|
);
|
|
152
171
|
}, [config, mode, resolvedTheme, resolvedElevation]);
|
|
153
172
|
|
|
173
|
+
// Calculate all visual styles (background, layout, border, shadow, etc.).
|
|
174
|
+
// Only recalculates when one of the style-related props changes.
|
|
154
175
|
const styles = useMemo(
|
|
155
176
|
() => getFrameStyles({
|
|
156
177
|
tokens,
|
|
@@ -184,13 +205,16 @@ export function Frame({
|
|
|
184
205
|
],
|
|
185
206
|
);
|
|
186
207
|
|
|
187
|
-
//
|
|
208
|
+
// This is the value that child components will inherit via FrameContext.
|
|
209
|
+
// Any nested Frame, Text, Icon, etc. will read this theme and elevation.
|
|
188
210
|
const contextValue = useMemo(
|
|
189
211
|
() => ({ theme: resolvedTheme, elevation: resolvedElevation }),
|
|
190
212
|
[resolvedTheme, resolvedElevation],
|
|
191
213
|
);
|
|
192
214
|
|
|
193
|
-
//
|
|
215
|
+
// Some styles only work on web browsers (CSS grid, inset shadows).
|
|
216
|
+
// We collect them separately and cast them to ViewStyle so React Native
|
|
217
|
+
// doesn't complain about unknown properties (they're silently ignored on native).
|
|
194
218
|
const webOverrides: ViewStyle[] = [];
|
|
195
219
|
if (styles.gridWebStyle) {
|
|
196
220
|
webOverrides.push(styles.gridWebStyle as unknown as ViewStyle);
|
|
@@ -199,11 +223,34 @@ export function Frame({
|
|
|
199
223
|
webOverrides.push({ boxShadow: styles.insetBoxShadow } as unknown as ViewStyle);
|
|
200
224
|
}
|
|
201
225
|
|
|
226
|
+
// Normalize user's custom styles into an array for merging.
|
|
202
227
|
const userStyles = Array.isArray(style) ? style : style ? [style] : [];
|
|
203
228
|
|
|
229
|
+
// If the Frame has onPress or href, it needs to respond to taps/clicks.
|
|
230
|
+
// In that case we render a Pressable (tappable); otherwise a plain View.
|
|
204
231
|
const isInteractive = onPress !== undefined || href !== undefined;
|
|
205
232
|
|
|
206
|
-
//
|
|
233
|
+
// Detect keyboard-only focus (Tab, arrows) vs mouse/touch focus.
|
|
234
|
+
// Only shows a visible focus ring when the user navigated via keyboard,
|
|
235
|
+
// matching the browser's native :focus-visible behavior.
|
|
236
|
+
const { isFocusVisible, focusProps } = useFocusVisible();
|
|
237
|
+
|
|
238
|
+
// Focus ring style: 2px solid outline in the theme's interactive color,
|
|
239
|
+
// offset by 2px so it doesn't overlap the Frame's border.
|
|
240
|
+
// Uses CSS outline properties — silently ignored on native platforms.
|
|
241
|
+
const focusRingStyle = isFocusVisible && !disabled ? {
|
|
242
|
+
outlineWidth: 2,
|
|
243
|
+
outlineStyle: 'solid',
|
|
244
|
+
outlineColor: srgbToHex(tokens.interactive.srgb),
|
|
245
|
+
outlineOffset: 2,
|
|
246
|
+
} as unknown as ViewStyle : undefined; // web-only
|
|
247
|
+
|
|
248
|
+
// Spread focus event handlers onto the Pressable. These are web-only
|
|
249
|
+
// handlers supported by react-native-web — silently ignored on native.
|
|
250
|
+
const webFocusProps = isInteractive ? focusProps as any : {}; // web-only
|
|
251
|
+
|
|
252
|
+
// Default text style for any raw strings/numbers passed as children.
|
|
253
|
+
// Uses the theme's primary text color, default font, and base size.
|
|
207
254
|
const textStyle = useMemo<TextStyle>(
|
|
208
255
|
() => ({
|
|
209
256
|
color: srgbToHex(tokens.textPrimary.srgb),
|
|
@@ -213,19 +260,38 @@ export function Frame({
|
|
|
213
260
|
}),
|
|
214
261
|
[tokens],
|
|
215
262
|
);
|
|
216
|
-
|
|
263
|
+
// Auto-wrap bare text ("hello") in <Text> elements (required by React Native).
|
|
264
|
+
// Wrapped in useMemo so it only re-scans children when they or the text style changes.
|
|
265
|
+
const wrappedChildren = useMemo(
|
|
266
|
+
() => wrapTextChildren(children, textStyle),
|
|
267
|
+
[children, textStyle],
|
|
268
|
+
);
|
|
217
269
|
|
|
270
|
+
// FrameContext.Provider shares this Frame's theme and elevation with all
|
|
271
|
+
// descendants, so nested components automatically pick up the right colors.
|
|
218
272
|
return (
|
|
219
273
|
<FrameContext.Provider value={contextValue}>
|
|
220
274
|
{isInteractive ? (
|
|
275
|
+
// Pressable handles taps. When href is set, react-native-web renders
|
|
276
|
+
// it as an <a> tag so it works like a regular link on the web.
|
|
221
277
|
<Pressable
|
|
278
|
+
ref={ref}
|
|
279
|
+
testID={testID}
|
|
280
|
+
nativeID={nativeID}
|
|
281
|
+
accessibilityLabel={accessibilityLabel}
|
|
282
|
+
accessibilityHint={accessibilityHint}
|
|
283
|
+
// Tell screen readers this is disabled so assistive technology can announce it.
|
|
284
|
+
accessibilityState={disabled ? { disabled: true } : undefined}
|
|
222
285
|
onPress={onPress}
|
|
223
286
|
disabled={disabled}
|
|
224
|
-
// react-native-web renders Pressable with href as <a>
|
|
225
287
|
{...(href ? { href, accessibilityRole: 'link' as const } : { accessibilityRole: 'button' as const })}
|
|
288
|
+
{...webFocusProps}
|
|
289
|
+
// The style callback receives { pressed: true/false } so we can
|
|
290
|
+
// change the background when the user is actively pressing.
|
|
226
291
|
style={({ pressed }) => [
|
|
227
292
|
styles.container,
|
|
228
293
|
pressed && !disabled && styles.pressed,
|
|
294
|
+
focusRingStyle,
|
|
229
295
|
...webOverrides,
|
|
230
296
|
...userStyles,
|
|
231
297
|
]}
|
|
@@ -233,7 +299,15 @@ export function Frame({
|
|
|
233
299
|
{wrappedChildren}
|
|
234
300
|
</Pressable>
|
|
235
301
|
) : (
|
|
236
|
-
|
|
302
|
+
// Non-interactive Frame: just a plain View with no tap handling.
|
|
303
|
+
<View
|
|
304
|
+
ref={ref}
|
|
305
|
+
testID={testID}
|
|
306
|
+
nativeID={nativeID}
|
|
307
|
+
accessibilityLabel={accessibilityLabel}
|
|
308
|
+
accessibilityHint={accessibilityHint}
|
|
309
|
+
style={[styles.container, ...webOverrides, ...userStyles]}
|
|
310
|
+
>
|
|
237
311
|
{wrappedChildren}
|
|
238
312
|
</View>
|
|
239
313
|
)}
|