@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.
Files changed (113) hide show
  1. package/dist/Popover/Popover.d.ts.map +1 -1
  2. package/dist/Popover/Popover.styles.d.ts +64 -1
  3. package/dist/Popover/Popover.styles.d.ts.map +1 -1
  4. package/dist/Select/Select.d.ts.map +1 -1
  5. package/dist/_COMPONENT_TEMPLATE/ComponentName.d.ts +70 -0
  6. package/dist/_COMPONENT_TEMPLATE/ComponentName.d.ts.map +1 -0
  7. package/dist/_COMPONENT_TEMPLATE/ComponentName.styles.d.ts +22 -0
  8. package/dist/_COMPONENT_TEMPLATE/ComponentName.styles.d.ts.map +1 -0
  9. package/dist/_COMPONENT_TEMPLATE/ComponentName.types.d.ts +45 -0
  10. package/dist/_COMPONENT_TEMPLATE/ComponentName.types.d.ts.map +1 -0
  11. package/dist/_COMPONENT_TEMPLATE/index.d.ts +3 -0
  12. package/dist/_COMPONENT_TEMPLATE/index.d.ts.map +1 -0
  13. package/dist/fonts/GoogleFontLoader.d.ts.map +1 -1
  14. package/dist/fonts/IconFontLoader.d.ts.map +1 -1
  15. package/dist/index.cjs +371 -74
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.ts +11 -5
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +370 -76
  20. package/dist/index.js.map +1 -1
  21. package/dist/{Frame → primitives/Frame}/Frame.d.ts +1 -1
  22. package/dist/primitives/Frame/Frame.d.ts.map +1 -0
  23. package/dist/{Frame → primitives/Frame}/Frame.styles.d.ts +11 -2
  24. package/dist/primitives/Frame/Frame.styles.d.ts.map +1 -0
  25. package/dist/primitives/Frame/Frame.types.d.ts +240 -0
  26. package/dist/primitives/Frame/Frame.types.d.ts.map +1 -0
  27. package/dist/{Frame → primitives/Frame}/Frame.utils.d.ts +12 -12
  28. package/dist/primitives/Frame/Frame.utils.d.ts.map +1 -0
  29. package/dist/primitives/Frame/index.d.ts.map +1 -0
  30. package/dist/primitives/Icon/Icon.d.ts +17 -0
  31. package/dist/primitives/Icon/Icon.d.ts.map +1 -0
  32. package/dist/primitives/Icon/Icon.types.d.ts +55 -0
  33. package/dist/primitives/Icon/Icon.types.d.ts.map +1 -0
  34. package/dist/primitives/Icon/index.d.ts +3 -0
  35. package/dist/primitives/Icon/index.d.ts.map +1 -0
  36. package/dist/primitives/Text/Text.d.ts +17 -0
  37. package/dist/primitives/Text/Text.d.ts.map +1 -0
  38. package/dist/primitives/Text/Text.types.d.ts +85 -0
  39. package/dist/primitives/Text/Text.types.d.ts.map +1 -0
  40. package/dist/primitives/Text/index.d.ts +3 -0
  41. package/dist/primitives/Text/index.d.ts.map +1 -0
  42. package/dist/primitives/Wrapper/Wrapper.d.ts +29 -0
  43. package/dist/primitives/Wrapper/Wrapper.d.ts.map +1 -0
  44. package/dist/primitives/Wrapper/Wrapper.styles.d.ts +28 -0
  45. package/dist/primitives/Wrapper/Wrapper.styles.d.ts.map +1 -0
  46. package/dist/primitives/Wrapper/Wrapper.types.d.ts +113 -0
  47. package/dist/primitives/Wrapper/Wrapper.types.d.ts.map +1 -0
  48. package/dist/primitives/Wrapper/index.d.ts +3 -0
  49. package/dist/primitives/Wrapper/index.d.ts.map +1 -0
  50. package/dist/primitives/index.d.ts +12 -0
  51. package/dist/primitives/index.d.ts.map +1 -0
  52. package/dist/primitives/useFocusVisible.d.ts +29 -0
  53. package/dist/primitives/useFocusVisible.d.ts.map +1 -0
  54. package/dist/theme/defaults.d.ts.map +1 -1
  55. package/dist/theme/types.d.ts +13 -6
  56. package/dist/theme/types.d.ts.map +1 -1
  57. package/dist/tokens/computeTokens.d.ts +13 -6
  58. package/dist/tokens/computeTokens.d.ts.map +1 -1
  59. package/dist/tokens/types.d.ts +16 -7
  60. package/dist/tokens/types.d.ts.map +1 -1
  61. package/package.json +1 -1
  62. package/src/Button/Button.styles.ts +9 -9
  63. package/src/Button/Button.tsx +1 -1
  64. package/src/Card/Card.styles.ts +1 -1
  65. package/src/ColorScaleSlider/ColorScaleSlider.styles.ts +1 -1
  66. package/src/HueSlider/HueSlider.styles.ts +1 -1
  67. package/src/Popover/Popover.styles.ts +5 -1
  68. package/src/Popover/Popover.tsx +3 -1
  69. package/src/Select/Select.styles.ts +9 -9
  70. package/src/Select/Select.tsx +2 -3
  71. package/src/Select/SelectOption.tsx +6 -6
  72. package/src/Slider/Slider.styles.ts +1 -1
  73. package/src/TextInput/TextInput.styles.ts +3 -3
  74. package/src/Toggle/Toggle.styles.ts +1 -1
  75. package/src/_COMPONENT_TEMPLATE/ComponentName.styles.ts +29 -0
  76. package/src/_COMPONENT_TEMPLATE/ComponentName.tsx +106 -0
  77. package/src/_COMPONENT_TEMPLATE/ComponentName.types.ts +86 -0
  78. package/src/_COMPONENT_TEMPLATE/index.ts +2 -0
  79. package/src/fonts/GoogleFontLoader.tsx +2 -0
  80. package/src/fonts/IconFontLoader.tsx +2 -0
  81. package/src/index.ts +22 -5
  82. package/src/{Frame → primitives/Frame}/Frame.styles.ts +46 -9
  83. package/src/{Frame → primitives/Frame}/Frame.tsx +90 -16
  84. package/src/primitives/Frame/Frame.types.ts +315 -0
  85. package/src/{Frame → primitives/Frame}/Frame.utils.ts +56 -20
  86. package/src/primitives/Icon/Icon.tsx +89 -0
  87. package/src/primitives/Icon/Icon.types.ts +70 -0
  88. package/src/primitives/Icon/index.ts +2 -0
  89. package/src/primitives/Text/Text.tsx +90 -0
  90. package/src/primitives/Text/Text.types.ts +108 -0
  91. package/src/primitives/Text/index.ts +10 -0
  92. package/src/primitives/Wrapper/Wrapper.styles.ts +113 -0
  93. package/src/primitives/Wrapper/Wrapper.tsx +104 -0
  94. package/src/primitives/Wrapper/Wrapper.types.ts +149 -0
  95. package/src/primitives/Wrapper/index.ts +2 -0
  96. package/src/primitives/index.ts +46 -0
  97. package/src/primitives/useFocusVisible.ts +102 -0
  98. package/src/theme/defaults.ts +13 -6
  99. package/src/theme/types.ts +13 -6
  100. package/src/tokens/computeTokens.ts +1 -1
  101. package/src/tokens/types.ts +16 -7
  102. package/dist/Frame/Frame.d.ts.map +0 -1
  103. package/dist/Frame/Frame.styles.d.ts.map +0 -1
  104. package/dist/Frame/Frame.types.d.ts +0 -115
  105. package/dist/Frame/Frame.types.d.ts.map +0 -1
  106. package/dist/Frame/Frame.utils.d.ts.map +0 -1
  107. package/dist/Frame/index.d.ts.map +0 -1
  108. package/dist/Icon/Icon.d.ts +0 -36
  109. package/dist/Icon/Icon.d.ts.map +0 -1
  110. package/src/Frame/Frame.types.ts +0 -181
  111. package/src/Icon/Icon.tsx +0 -76
  112. /package/dist/{Frame → primitives/Frame}/index.d.ts +0 -0
  113. /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
+ */
@@ -0,0 +1,2 @@
1
+ export { ComponentName } from './ComponentName';
2
+ export type { ComponentNameProps } from './ComponentName.types';
@@ -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 { Frame } from './Frame/Frame';
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 '../tokens/types';
5
- import type { FrameElevation } from '../theme/types';
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
- // Auto-clip when any corner has radius
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
- // Android elevation prop (react-native-web maps to box-shadow)
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 (background shift to sunken) ──
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 '../theme/types';
5
+ import type { ElevationLevel, FrameElevation } from '../../theme/types';
6
6
  import { srgbToHex } from 'newtone';
7
- import { FrameContext, useFrameContext } from '../theme/FrameContext';
8
- import { useNewtoneTheme } from '../theme/NewtoneProvider';
9
- import { computeTokens } from '../tokens/computeTokens';
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> to prevent
14
- * react-native-web "Unexpected text node" console errors.
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
- // Resolve theme: prop > parent Frame > NewtoneProvider
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
- // Resolve frame elevation (user-facing, for shadow/style decisions)
144
+ // The user-facing elevation (-2 to 2) controls visual depth (shadows, background).
131
145
  const resolvedFrameElevation: FrameElevation = elevation ?? 0;
132
146
 
133
- // Map to internal elevation level for token computation
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
- // Compute tokens directly (not useTokens) to avoid reading our own FrameContext
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
- // Memoize context value
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
- // Build the style array with web-specific overrides
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
- // Wrap raw string/number children in <Text> for RN compatibility
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
- const wrappedChildren = wrapTextChildren(children, textStyle);
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
- <View style={[styles.container, ...webOverrides, ...userStyles]}>
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
  )}