@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,315 @@
1
+ import type { View, ViewStyle, GestureResponderEvent } from 'react-native';
2
+ import type { ThemeName, FrameElevation } from '../../theme/types';
3
+
4
+ // ── Spacing Types ──────────────────────────────────────────────
5
+
6
+ /** Design system spacing tokens (represent px values at Medium preset) */
7
+ export type SpacingToken = '00' | '02' | '04' | '06' | '08' | '10' | '12' | '16' | '20' | '24' | '32' | '40' | '48';
8
+
9
+ /** A spacing value: either a design token name or a pixel number */
10
+ export type SpacingValue = SpacingToken | number;
11
+
12
+ /** Per-side spacing */
13
+ export interface SpacingSides {
14
+ readonly top?: SpacingValue;
15
+ readonly right?: SpacingValue;
16
+ readonly bottom?: SpacingValue;
17
+ readonly left?: SpacingValue;
18
+ }
19
+
20
+ /** Axis-based spacing shorthand */
21
+ export interface SpacingAxes {
22
+ readonly x?: SpacingValue;
23
+ readonly y?: SpacingValue;
24
+ }
25
+
26
+ /**
27
+ * Padding value — supports three forms:
28
+ *
29
+ * - **Uniform**: `'12'` or `16` — same padding on all sides
30
+ * - **Axis shorthand**: `{ x: '16', y: '08' }` — horizontal/vertical
31
+ * - **Per-side**: `{ top: 8, bottom: 16, left: '12', right: '12' }` — individual sides
32
+ *
33
+ * Token names (`'00'` through `'48'`) resolve to pixel values from the theme.
34
+ */
35
+ export type PaddingProp = SpacingValue | SpacingAxes | SpacingSides;
36
+
37
+ /** Gap axis shorthand */
38
+ export interface GapAxes {
39
+ readonly row?: SpacingValue;
40
+ readonly column?: SpacingValue;
41
+ }
42
+
43
+ /**
44
+ * Gap value — supports two forms:
45
+ *
46
+ * - **Uniform**: `'12'` or `12` — same gap between all children
47
+ * - **Row/column**: `{ row: '08', column: '16' }` — different gaps per axis
48
+ *
49
+ * Token names (`'00'` through `'48'`) resolve to pixel values from the theme.
50
+ */
51
+ export type GapProp = SpacingValue | GapAxes;
52
+
53
+ // ── Radius Types ───────────────────────────────────────────────
54
+
55
+ /** Design system radius tokens */
56
+ export type RadiusToken = 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'pill';
57
+
58
+ /** A radius value: either a design token name or a pixel number */
59
+ export type RadiusValue = RadiusToken | number;
60
+
61
+ /** Per-corner radius */
62
+ export interface RadiusCorners {
63
+ readonly topLeft?: RadiusValue;
64
+ readonly topRight?: RadiusValue;
65
+ readonly bottomLeft?: RadiusValue;
66
+ readonly bottomRight?: RadiusValue;
67
+ }
68
+
69
+ /**
70
+ * Border radius — supports two forms:
71
+ *
72
+ * - **Uniform**: `'md'` or `8` — same radius on all corners
73
+ * - **Per-corner**: `{ topLeft: 'lg', topRight: 'lg', bottomLeft: 0, bottomRight: 0 }`
74
+ *
75
+ * Token names (`'none'` through `'pill'`) resolve to pixel values from the theme.
76
+ */
77
+ export type RadiusProp = RadiusValue | RadiusCorners;
78
+
79
+ // ── Sizing Types ───────────────────────────────────────────────
80
+
81
+ /**
82
+ * Figma-like sizing mode:
83
+ *
84
+ * - `'hug'` — shrink to fit content (default, no explicit dimension set)
85
+ * - `'fill'` — expand to fill available space (`flexGrow: 1` + `width/height: '100%'`)
86
+ * - `number` — fixed pixel dimension (e.g., `200`)
87
+ */
88
+ export type SizingMode = 'hug' | 'fill' | number;
89
+
90
+ // ── Layout Types ───────────────────────────────────────────────
91
+
92
+ /** Layout mode */
93
+ export type LayoutMode = 'flex' | 'grid';
94
+
95
+ /** Flex direction (Figma-like naming) */
96
+ export type Direction = 'horizontal' | 'vertical';
97
+
98
+ /** Cross-axis alignment */
99
+ export type Alignment = 'start' | 'center' | 'end' | 'stretch' | 'baseline';
100
+
101
+ /** Main-axis justification */
102
+ export type Justification = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
103
+
104
+ // ── Props ──────────────────────────────────────────────────────
105
+
106
+ /**
107
+ * Props for Frame — the foundational layout building block.
108
+ *
109
+ * Frame provides theme/elevation context, layout (flex/grid), spacing,
110
+ * sizing, appearance, and interactivity. Frames can be nested — inner
111
+ * frames override outer frames for their subtree.
112
+ *
113
+ * @example
114
+ * ```tsx
115
+ * // Horizontal toolbar with consistent spacing
116
+ * <Frame direction="horizontal" gap="md" padding="lg" align="center">
117
+ * <Button onPress={() => {}}>Save</Button>
118
+ * <Button variant="ghost" onPress={() => {}}>Cancel</Button>
119
+ * </Frame>
120
+ * ```
121
+ *
122
+ * @example
123
+ * ```tsx
124
+ * // Elevated card with primary theme
125
+ * <Frame theme="primary" elevation={2} radius="lg" padding="xl" bordered>
126
+ * <Text>Elevated card content</Text>
127
+ * </Frame>
128
+ * ```
129
+ *
130
+ * @example
131
+ * ```tsx
132
+ * // Clickable card with link
133
+ * <Frame radius="md" padding="md" bordered onPress={() => navigate('/details')}>
134
+ * <Text>Tap to navigate</Text>
135
+ * </Frame>
136
+ * ```
137
+ */
138
+ export interface FrameProps {
139
+ /**
140
+ * Child elements. Inherits this Frame's theme and elevation context.
141
+ * Raw strings/numbers are auto-wrapped in `<Text>` with theme-appropriate styling.
142
+ */
143
+ readonly children: React.ReactNode;
144
+
145
+ // ── Theme & Elevation ──
146
+
147
+ /**
148
+ * Theme override for this frame and all descendants.
149
+ * When omitted, inherits from parent Frame or NewtoneProvider.
150
+ *
151
+ * @example
152
+ * ```tsx
153
+ * <Frame theme="primary"> // Blue-ish surface
154
+ * <Frame theme="strong"> // High-contrast surface
155
+ * ```
156
+ */
157
+ readonly theme?: ThemeName;
158
+
159
+ /**
160
+ * Surface elevation: controls background lightness and shadow.
161
+ *
162
+ * | Value | Effect |
163
+ * |:-----:|:-------|
164
+ * | -2 | Deeply sunken (inset shadow) |
165
+ * | -1 | Sunken |
166
+ * | 0 | Default surface |
167
+ * | 1 | Elevated |
168
+ * | 2 | Prominently elevated (drop shadow) |
169
+ *
170
+ * When omitted, inherits from parent Frame or defaults to 0.
171
+ */
172
+ readonly elevation?: FrameElevation;
173
+
174
+ // ── Layout ──
175
+
176
+ /** Layout mode. @default 'flex' */
177
+ readonly layout?: LayoutMode;
178
+
179
+ /** Flex direction. @default 'vertical' */
180
+ readonly direction?: Direction;
181
+
182
+ /** Enable flex wrap. @default false */
183
+ readonly wrap?: boolean;
184
+
185
+ /** Reverse flex direction. @default false */
186
+ readonly reverse?: boolean;
187
+
188
+ /** CSS Grid column count (web-only, requires layout='grid'). */
189
+ readonly columns?: number;
190
+
191
+ /** CSS Grid row count (web-only, requires layout='grid'). */
192
+ readonly rows?: number;
193
+
194
+ // ── Alignment ──
195
+
196
+ /** Cross-axis alignment of children. */
197
+ readonly align?: Alignment;
198
+
199
+ /** Main-axis justification of children. */
200
+ readonly justify?: Justification;
201
+
202
+ // ── Spacing ──
203
+
204
+ /**
205
+ * Padding inside the frame.
206
+ *
207
+ * @example
208
+ * ```tsx
209
+ * <Frame padding="md" /> // uniform
210
+ * <Frame padding={16} /> // uniform px
211
+ * <Frame padding={{ x: 'lg', y: 'sm' }} /> // axis shorthand
212
+ * <Frame padding={{ top: 8, bottom: 16 }} /> // per-side
213
+ * ```
214
+ */
215
+ readonly padding?: PaddingProp;
216
+
217
+ /**
218
+ * Gap between children.
219
+ *
220
+ * @example
221
+ * ```tsx
222
+ * <Frame gap="md" /> // uniform
223
+ * <Frame gap={12} /> // uniform px
224
+ * <Frame gap={{ row: 'sm', column: 'lg' }} /> // per-axis
225
+ * ```
226
+ */
227
+ readonly gap?: GapProp;
228
+
229
+ // ── Sizing ──
230
+
231
+ /**
232
+ * Width sizing: `'hug'` (shrink to content), `'fill'` (expand), or fixed px.
233
+ * @example
234
+ * ```tsx
235
+ * <Frame width="fill" /> // expand to fill parent
236
+ * <Frame width={300} /> // fixed 300px
237
+ * ```
238
+ */
239
+ readonly width?: SizingMode;
240
+
241
+ /**
242
+ * Height sizing: `'hug'` (shrink to content), `'fill'` (expand), or fixed px.
243
+ * @example
244
+ * ```tsx
245
+ * <Frame height="fill" /> // expand to fill parent
246
+ * <Frame height={200} /> // fixed 200px
247
+ * ```
248
+ */
249
+ readonly height?: SizingMode;
250
+
251
+ /** Minimum width in pixels. */
252
+ readonly minWidth?: number;
253
+
254
+ /** Maximum width in pixels. */
255
+ readonly maxWidth?: number;
256
+
257
+ /** Minimum height in pixels. */
258
+ readonly minHeight?: number;
259
+
260
+ /** Maximum height in pixels. */
261
+ readonly maxHeight?: number;
262
+
263
+ // ── Appearance ──
264
+
265
+ /**
266
+ * Border radius.
267
+ *
268
+ * @example
269
+ * ```tsx
270
+ * <Frame radius="md" /> // uniform token
271
+ * <Frame radius={8} /> // uniform px
272
+ * <Frame radius={{ topLeft: 'lg', topRight: 'lg' }} /> // per-corner
273
+ * <Frame radius="pill" /> // fully rounded
274
+ * ```
275
+ */
276
+ readonly radius?: RadiusProp;
277
+
278
+ /** Show 1px border using theme border color. @default false */
279
+ readonly bordered?: boolean;
280
+
281
+ // ── Interactivity ──
282
+
283
+ /** Press handler — switches rendering from View to Pressable. */
284
+ readonly onPress?: (event: GestureResponderEvent) => void;
285
+
286
+ /** Navigation URL — switches rendering to Pressable with link role. */
287
+ readonly href?: string;
288
+
289
+ /** Disable interaction. @default false */
290
+ readonly disabled?: boolean;
291
+
292
+ // ── Accessibility ──
293
+
294
+ /** Accessible label read by screen readers. Use for icon-only or non-textual frames. */
295
+ readonly accessibilityLabel?: string;
296
+
297
+ /** Additional hint read after the label (e.g. "Opens settings page"). */
298
+ readonly accessibilityHint?: string;
299
+
300
+ // ── Testing & Platform ──
301
+
302
+ /** Test identifier — maps to `data-testid` on web. Used by testing libraries. */
303
+ readonly testID?: string;
304
+
305
+ /** DOM id — maps to `id` attribute on web. Used for anchors and scroll targets. */
306
+ readonly nativeID?: string;
307
+
308
+ /** Ref to the underlying View or Pressable element. */
309
+ readonly ref?: React.Ref<View>;
310
+
311
+ // ── Style override ──
312
+
313
+ /** Custom style overrides (applied last). */
314
+ readonly style?: ViewStyle | ViewStyle[];
315
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ViewStyle } from 'react-native';
2
- import type { ResolvedTokens } from '../tokens/types';
2
+ import type { ResolvedTokens } from '../../tokens/types';
3
3
  import type {
4
4
  SpacingValue,
5
5
  SpacingAxes,
@@ -15,27 +15,33 @@ import type {
15
15
  } from './Frame.types';
16
16
 
17
17
  // ── Spacing ──────────────────────────────────────────────────────
18
+ // These functions convert spacing props (token names like 'md' or pixel numbers)
19
+ // into actual pixel values that React Native can use for padding and gap.
18
20
 
19
- /** Resolve a spacing token or number to a pixel value */
21
+ /** Convert a spacing token name (like 'md') or raw number into pixels. */
20
22
  export function resolveSpacing(
21
23
  value: SpacingValue,
22
24
  tokens: ResolvedTokens,
23
25
  ): number {
26
+ // If already a number, use it directly as pixels.
24
27
  if (typeof value === 'number') return value;
28
+ // Otherwise look up the token name in the theme's spacing scale.
25
29
  return tokens.spacing[value];
26
30
  }
27
31
 
28
- /** Resolve padding prop to per-side pixel values */
32
+ /** Convert a padding prop into pixel values for all four sides. */
29
33
  export function resolvePadding(
30
34
  prop: PaddingProp,
31
35
  tokens: ResolvedTokens,
32
36
  ): { readonly top: number; readonly right: number; readonly bottom: number; readonly left: number } {
37
+ // Uniform: same padding on all sides (e.g. padding="md" or padding={16}).
33
38
  if (typeof prop === 'string' || typeof prop === 'number') {
34
39
  const px = resolveSpacing(prop, tokens);
35
40
  return { top: px, right: px, bottom: px, left: px };
36
41
  }
37
42
 
38
- // Axis shorthand: { x?, y? }
43
+ // Axis shorthand: separate horizontal (x) and vertical (y) padding.
44
+ // Example: padding={{ x: 'lg', y: 'sm' }}
39
45
  if ('x' in prop || 'y' in prop) {
40
46
  const axes = prop as SpacingAxes;
41
47
  const x = axes.x !== undefined ? resolveSpacing(axes.x, tokens) : 0;
@@ -43,7 +49,8 @@ export function resolvePadding(
43
49
  return { top: y, right: x, bottom: y, left: x };
44
50
  }
45
51
 
46
- // Per-side: { top?, right?, bottom?, left? }
52
+ // Per-side: different padding on each side.
53
+ // Example: padding={{ top: 8, bottom: 16, left: 'md', right: 'md' }}
47
54
  const sides = prop as SpacingSides;
48
55
  return {
49
56
  top: sides.top !== undefined ? resolveSpacing(sides.top, tokens) : 0,
@@ -53,17 +60,19 @@ export function resolvePadding(
53
60
  };
54
61
  }
55
62
 
56
- /** Resolve gap prop to rowGap/columnGap pixel values */
63
+ /** Convert a gap prop into row and column gap pixel values. */
57
64
  export function resolveGap(
58
65
  prop: GapProp,
59
66
  tokens: ResolvedTokens,
60
67
  ): { readonly rowGap: number; readonly columnGap: number } {
68
+ // Uniform: same gap in both directions (e.g. gap="md" or gap={12}).
61
69
  if (typeof prop === 'string' || typeof prop === 'number') {
62
70
  const px = resolveSpacing(prop, tokens);
63
71
  return { rowGap: px, columnGap: px };
64
72
  }
65
73
 
66
- // { row?, column? }
74
+ // Per-axis: different gap for rows vs columns.
75
+ // Example: gap={{ row: 'sm', column: 'lg' }}
67
76
  return {
68
77
  rowGap: prop.row !== undefined ? resolveSpacing(prop.row, tokens) : 0,
69
78
  columnGap: prop.column !== undefined ? resolveSpacing(prop.column, tokens) : 0,
@@ -71,8 +80,10 @@ export function resolveGap(
71
80
  }
72
81
 
73
82
  // ── Radius ───────────────────────────────────────────────────────
83
+ // These functions convert radius props (token names like 'lg' or pixel numbers)
84
+ // into actual pixel values for rounding corners.
74
85
 
75
- /** Resolve a radius token or number to a pixel value */
86
+ /** Convert a radius token name (like 'lg') or raw number into pixels. */
76
87
  export function resolveRadius(
77
88
  value: RadiusValue,
78
89
  tokens: ResolvedTokens,
@@ -81,7 +92,7 @@ export function resolveRadius(
81
92
  return tokens.radius[value];
82
93
  }
83
94
 
84
- /** Resolved per-corner radius values */
95
+ /** Pixel values for each corner of a rounded rectangle. */
85
96
  export interface ResolvedCorners {
86
97
  readonly topLeft: number;
87
98
  readonly topRight: number;
@@ -89,17 +100,19 @@ export interface ResolvedCorners {
89
100
  readonly bottomRight: number;
90
101
  }
91
102
 
92
- /** Resolve radius prop to per-corner pixel values */
103
+ /** Convert a radius prop into pixel values for all four corners. */
93
104
  export function resolveRadiusCorners(
94
105
  prop: RadiusProp,
95
106
  tokens: ResolvedTokens,
96
107
  ): ResolvedCorners {
108
+ // Uniform: same radius on all corners (e.g. radius="lg" or radius={8}).
97
109
  if (typeof prop === 'string' || typeof prop === 'number') {
98
110
  const px = resolveRadius(prop, tokens);
99
111
  return { topLeft: px, topRight: px, bottomLeft: px, bottomRight: px };
100
112
  }
101
113
 
102
- // Per-corner object
114
+ // Per-corner: different radius on each corner.
115
+ // Example: radius={{ topLeft: 'lg', topRight: 'lg', bottomLeft: 0, bottomRight: 0 }}
103
116
  return {
104
117
  topLeft: prop.topLeft !== undefined ? resolveRadius(prop.topLeft, tokens) : 0,
105
118
  topRight: prop.topRight !== undefined ? resolveRadius(prop.topRight, tokens) : 0,
@@ -108,7 +121,7 @@ export function resolveRadiusCorners(
108
121
  };
109
122
  }
110
123
 
111
- /** Check if any corner has a positive radius (for auto-clip) */
124
+ /** Check if any corner is rounded (used to decide whether to clip overflowing content). */
112
125
  export function hasPositiveRadius(corners: ResolvedCorners): boolean {
113
126
  return corners.topLeft > 0
114
127
  || corners.topRight > 0
@@ -117,8 +130,9 @@ export function hasPositiveRadius(corners: ResolvedCorners): boolean {
117
130
  }
118
131
 
119
132
  // ── Sizing ───────────────────────────────────────────────────────
133
+ // Converts Figma-like sizing modes into React Native style properties.
120
134
 
121
- /** Resolve width/height sizing modes to ViewStyle properties */
135
+ /** Convert width/height sizing modes to style properties. */
122
136
  export function resolveSizing(
123
137
  width: SizingMode | undefined,
124
138
  height: SizingMode | undefined,
@@ -127,29 +141,43 @@ export function resolveSizing(
127
141
 
128
142
  if (width !== undefined) {
129
143
  if (width === 'fill') {
144
+ // 'fill' = expand to take all available space in the parent.
145
+ // flexGrow tells the layout engine to give this element extra space.
146
+ // width: 100% ensures it spans the full width.
130
147
  style.flexGrow = 1;
131
148
  style.width = '100%';
132
149
  } else if (typeof width === 'number') {
150
+ // Fixed pixel width (e.g. 300).
133
151
  style.width = width;
134
152
  }
135
- // 'hug' = default RN behavior, no explicit dimension
153
+ // 'hug' = shrink to fit the content (React Native's default behavior).
136
154
  }
137
155
 
138
156
  if (height !== undefined) {
139
157
  if (height === 'fill') {
140
- style.flexGrow = style.flexGrow === 1 ? 1 : 1;
158
+ // Same as width 'fill' expand to fill all available vertical space.
159
+ style.flexGrow = 1;
141
160
  style.height = '100%';
142
161
  } else if (typeof height === 'number') {
162
+ // Fixed pixel height (e.g. 200).
143
163
  style.height = height;
144
164
  }
145
- // 'hug' = default RN behavior
165
+ // 'hug' = shrink to fit the content (React Native's default behavior).
146
166
  }
147
167
 
148
168
  return style as ViewStyle;
149
169
  }
150
170
 
151
171
  // ── Layout ───────────────────────────────────────────────────────
152
-
172
+ // These functions convert our friendly prop names (like 'start', 'between')
173
+ // into the actual CSS flexbox values that React Native uses internally.
174
+
175
+ // Maps our alignment names to flexbox cross-axis values:
176
+ // 'start' → push children to the top/left edge
177
+ // 'center' → center children in the cross direction
178
+ // 'end' → push children to the bottom/right edge
179
+ // 'stretch' → stretch children to fill the cross dimension
180
+ // 'baseline' → align children by their text baseline
153
181
  const ALIGN_MAP: Record<Alignment, ViewStyle['alignItems']> = {
154
182
  start: 'flex-start',
155
183
  center: 'center',
@@ -158,6 +186,13 @@ const ALIGN_MAP: Record<Alignment, ViewStyle['alignItems']> = {
158
186
  baseline: 'baseline',
159
187
  };
160
188
 
189
+ // Maps our justification names to flexbox main-axis values:
190
+ // 'start' → pack children at the beginning
191
+ // 'center' → pack children in the middle
192
+ // 'end' → pack children at the end
193
+ // 'between' → spread children with equal space between them
194
+ // 'around' → spread children with equal space around each
195
+ // 'evenly' → spread children with equal space everywhere
161
196
  const JUSTIFY_MAP: Record<Justification, ViewStyle['justifyContent']> = {
162
197
  start: 'flex-start',
163
198
  center: 'center',
@@ -167,21 +202,22 @@ const JUSTIFY_MAP: Record<Justification, ViewStyle['justifyContent']> = {
167
202
  evenly: 'space-evenly',
168
203
  };
169
204
 
170
- /** Resolve alignment prop to ViewStyle alignItems */
205
+ /** Convert our alignment name to the flexbox value React Native expects. */
171
206
  export function resolveAlignment(align: Alignment): ViewStyle['alignItems'] {
172
207
  return ALIGN_MAP[align];
173
208
  }
174
209
 
175
- /** Resolve justification prop to ViewStyle justifyContent */
210
+ /** Convert our justification name to the flexbox value React Native expects. */
176
211
  export function resolveJustification(justify: Justification): ViewStyle['justifyContent'] {
177
212
  return JUSTIFY_MAP[justify];
178
213
  }
179
214
 
180
- /** Resolve direction + reverse to ViewStyle flexDirection */
215
+ /** Convert direction ('horizontal'/'vertical') + reverse flag to flexbox direction. */
181
216
  export function resolveFlexDirection(
182
217
  direction: Direction,
183
218
  reverse: boolean,
184
219
  ): ViewStyle['flexDirection'] {
220
+ // 'horizontal' → children in a row, 'vertical' → children in a column.
185
221
  if (direction === 'horizontal') {
186
222
  return reverse ? 'row-reverse' : 'row';
187
223
  }
@@ -0,0 +1,89 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Text, type TextStyle } from 'react-native';
3
+ import { srgbToHex } from 'newtone';
4
+ import { useTokens } from '../../tokens/useTokens';
5
+ import type { IconProps } from './Icon.types';
6
+
7
+ /**
8
+ * Material Symbols icon component with variable font support.
9
+ *
10
+ * Uses global icon configuration (variant, weight, auto-grade) from theme config,
11
+ * with per-instance control over size, fill, and color.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <Icon name="home" />
16
+ * <Icon name="settings" size={24} fill={1} />
17
+ * <Icon name="check" color="#00ff00" />
18
+ * ```
19
+ */
20
+ export function Icon({
21
+ name,
22
+ size,
23
+ opticalSize,
24
+ fill = 0,
25
+ color,
26
+ elevation = 1,
27
+ style,
28
+ onPress,
29
+ // Accessibility
30
+ accessibilityLabel,
31
+ // Testing & platform
32
+ testID,
33
+ nativeID,
34
+ ref,
35
+ }: IconProps) {
36
+ // Get the current theme's design tokens for colors, typography, and icon settings.
37
+ const tokens = useTokens(elevation);
38
+
39
+ // Build the icon's style from the theme tokens and props.
40
+ // Wrapped in useMemo so it only recalculates when the inputs change,
41
+ // instead of rebuilding the style object on every render.
42
+ const iconStyle = useMemo<TextStyle>(() => {
43
+ // Use the provided size, or fall back to the theme's default text size.
44
+ const fontSize = size ?? tokens.typography.size.base;
45
+ // Optical size adjusts stroke weight for readability at small sizes.
46
+ const opsz = opticalSize ?? fontSize;
47
+ // Use the provided color, or fall back to the theme's primary text color.
48
+ const iconColor = color ?? srgbToHex(tokens.textPrimary.srgb);
49
+
50
+ // Build the font family name from the theme's icon variant setting.
51
+ // Example: variant 'rounded' → 'Material Symbols Rounded'
52
+ // The charAt(0).toUpperCase() + slice(1) capitalizes the first letter.
53
+ const fontFamily = `Material Symbols ${tokens.icons.variant.charAt(0).toUpperCase() + tokens.icons.variant.slice(1)}`;
54
+
55
+ // Material Symbols is a variable font with 4 adjustable axes:
56
+ // FILL: 0 = outlined (hollow), 1 = filled (solid)
57
+ // wght: font weight (thinner or bolder strokes)
58
+ // GRAD: grade (fine-tune weight without changing overall size)
59
+ // opsz: optical size (adjusts detail level for the display size)
60
+ const fontVariationSettings = `'FILL' ${fill}, 'wght' ${tokens.icons.weight}, 'GRAD' ${tokens.icons.grade}, 'opsz' ${opsz}`;
61
+
62
+ return {
63
+ fontFamily,
64
+ fontSize,
65
+ color: iconColor,
66
+ userSelect: 'none', // web-only: prevents users from selecting the icon as text
67
+ fontVariationSettings, // web-only: controls the variable font axes listed above
68
+ ...style,
69
+ } as TextStyle; // Cast needed because web-only properties aren't in RN's type definitions
70
+ }, [tokens, size, opticalSize, fill, color, style]);
71
+
72
+ // Material Symbols renders icons as text ligatures — the icon name (like "home")
73
+ // is passed as text content, and the font renders it as the icon glyph.
74
+ return (
75
+ <Text
76
+ ref={ref}
77
+ testID={testID}
78
+ nativeID={nativeID}
79
+ accessibilityLabel={accessibilityLabel}
80
+ // When no label is provided, the icon is decorative — hide it from screen readers
81
+ // so assistive technology doesn't try to read the ligature text (e.g. "home").
82
+ importantForAccessibility={accessibilityLabel ? 'yes' : 'no-hide-descendants'}
83
+ style={iconStyle}
84
+ onPress={onPress} // When provided, makes the icon tappable
85
+ >
86
+ {name}
87
+ </Text>
88
+ );
89
+ }