@praxiis/ui 0.0.1

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 (187) hide show
  1. package/dist/index.d.mts +52556 -0
  2. package/dist/index.d.ts +52556 -0
  3. package/dist/index.js +8753 -0
  4. package/dist/index.mjs +8777 -0
  5. package/package.json +70 -0
  6. package/src/__test-utils__/index.tsx +39 -0
  7. package/src/components/CalendarStrip/CalendarStrip.helpers.ts +106 -0
  8. package/src/components/CalendarStrip/CalendarStrip.tsx +83 -0
  9. package/src/components/CalendarStrip/CalendarStrip.types.ts +133 -0
  10. package/src/components/CalendarStrip/DayCard/DayCard.helpers.ts +44 -0
  11. package/src/components/CalendarStrip/DayCard/DayCard.tsx +71 -0
  12. package/src/components/CalendarStrip/DayCard/DayCard.types.ts +134 -0
  13. package/src/components/CalendarStrip/DayCard/index.ts +2 -0
  14. package/src/components/CalendarStrip/DayCard/useDayCardLogic.ts +45 -0
  15. package/src/components/CalendarStrip/index.ts +9 -0
  16. package/src/components/CalendarStrip/useCalendarStripLogic.ts +53 -0
  17. package/src/components/EmptyState/EmptyState.helpers.ts +104 -0
  18. package/src/components/EmptyState/EmptyState.tsx +205 -0
  19. package/src/components/EmptyState/EmptyState.types.ts +213 -0
  20. package/src/components/EmptyState/index.ts +44 -0
  21. package/src/components/EmptyState/useEmptyStateLogic.ts +131 -0
  22. package/src/components/Header/Header.helpers.ts +93 -0
  23. package/src/components/Header/Header.tsx +185 -0
  24. package/src/components/Header/Header.types.ts +153 -0
  25. package/src/components/Header/index.ts +44 -0
  26. package/src/components/Header/useHeaderLogic.ts +146 -0
  27. package/src/components/ScheduleItem/ScheduleItem/ScheduleItem.helpers.ts +50 -0
  28. package/src/components/ScheduleItem/ScheduleItem/ScheduleItem.tsx +78 -0
  29. package/src/components/ScheduleItem/ScheduleItem/ScheduleItem.types.ts +99 -0
  30. package/src/components/ScheduleItem/ScheduleItem/index.ts +16 -0
  31. package/src/components/ScheduleItem/ScheduleItem/useScheduleItemLogic.ts +31 -0
  32. package/src/components/ScheduleItem/index.ts +15 -0
  33. package/src/components/index.ts +40 -0
  34. package/src/core/index.ts +34 -0
  35. package/src/core/restyle/RestyleThemeProviderWrapper.tsx +31 -0
  36. package/src/core/restyle/index.ts +38 -0
  37. package/src/core/restyle/restylePresetRegistry.ts +195 -0
  38. package/src/core/restyle/restyleTheme.ts +1352 -0
  39. package/src/core/restyle/restyleTypes.ts +8 -0
  40. package/src/core/restyle/useRestyleTheme.ts +10 -0
  41. package/src/hooks/animations/index.ts +3 -0
  42. package/src/hooks/animations/useAnimatedValue.ts +10 -0
  43. package/src/hooks/animations/useEntranceAnimation.ts +106 -0
  44. package/src/hooks/animations/usePulseAnimation.ts +63 -0
  45. package/src/hooks/index.ts +30 -0
  46. package/src/hooks/useReducedMotion.ts +60 -0
  47. package/src/i18n/index.ts +2 -0
  48. package/src/i18n/labels/en.ts +120 -0
  49. package/src/i18n/labels/es.ts +120 -0
  50. package/src/i18n/labels/index.ts +6 -0
  51. package/src/i18n/labels/types.ts +165 -0
  52. package/src/index.tsx +215 -0
  53. package/src/primitives/actions/Button/Button.helpers.ts +243 -0
  54. package/src/primitives/actions/Button/Button.tsx +198 -0
  55. package/src/primitives/actions/Button/Button.types.ts +207 -0
  56. package/src/primitives/actions/Button/index.ts +41 -0
  57. package/src/primitives/actions/Button/useButtonLogic.ts +160 -0
  58. package/src/primitives/actions/IconButton/IconButton.helpers.ts +235 -0
  59. package/src/primitives/actions/IconButton/IconButton.tsx +177 -0
  60. package/src/primitives/actions/IconButton/IconButton.types.ts +273 -0
  61. package/src/primitives/actions/IconButton/index.ts +30 -0
  62. package/src/primitives/actions/IconButton/useIconButtonLogic.ts +172 -0
  63. package/src/primitives/actions/index.ts +20 -0
  64. package/src/primitives/content/Avatar/Avatar.helpers.ts +177 -0
  65. package/src/primitives/content/Avatar/Avatar.tsx +199 -0
  66. package/src/primitives/content/Avatar/Avatar.types.ts +222 -0
  67. package/src/primitives/content/Avatar/index.ts +46 -0
  68. package/src/primitives/content/Avatar/useAvatarLogic.ts +149 -0
  69. package/src/primitives/content/Badge/Badge.helpers.ts +175 -0
  70. package/src/primitives/content/Badge/Badge.tsx +174 -0
  71. package/src/primitives/content/Badge/Badge.types.ts +223 -0
  72. package/src/primitives/content/Badge/index.ts +40 -0
  73. package/src/primitives/content/Badge/useBadgeLogic.ts +128 -0
  74. package/src/primitives/content/Card/Card.helpers.ts +27 -0
  75. package/src/primitives/content/Card/Card.tsx +123 -0
  76. package/src/primitives/content/Card/Card.types.ts +95 -0
  77. package/src/primitives/content/Card/index.ts +20 -0
  78. package/src/primitives/content/Card/useCardLogic.ts +48 -0
  79. package/src/primitives/content/Chip/Chip.helpers.ts +304 -0
  80. package/src/primitives/content/Chip/Chip.tsx +205 -0
  81. package/src/primitives/content/Chip/Chip.types.ts +234 -0
  82. package/src/primitives/content/Chip/index.ts +47 -0
  83. package/src/primitives/content/Chip/useChipLogic.ts +167 -0
  84. package/src/primitives/content/Icon/Icon.helpers.ts +54 -0
  85. package/src/primitives/content/Icon/Icon.tsx +110 -0
  86. package/src/primitives/content/Icon/Icon.types.ts +95 -0
  87. package/src/primitives/content/Icon/index.ts +20 -0
  88. package/src/primitives/content/Icon/useIconLogic.ts +73 -0
  89. package/src/primitives/content/index.ts +45 -0
  90. package/src/primitives/feedback/ProgressBar/ProgressBar.helpers.ts +122 -0
  91. package/src/primitives/feedback/ProgressBar/ProgressBar.tsx +154 -0
  92. package/src/primitives/feedback/ProgressBar/ProgressBar.types.ts +178 -0
  93. package/src/primitives/feedback/ProgressBar/index.ts +17 -0
  94. package/src/primitives/feedback/ProgressBar/useProgressBarLogic.ts +120 -0
  95. package/src/primitives/feedback/Skeleton/Skeleton.helpers.ts +145 -0
  96. package/src/primitives/feedback/Skeleton/Skeleton.tsx +155 -0
  97. package/src/primitives/feedback/Skeleton/Skeleton.types.ts +223 -0
  98. package/src/primitives/feedback/Skeleton/index.ts +44 -0
  99. package/src/primitives/feedback/Skeleton/useSkeletonLogic.ts +125 -0
  100. package/src/primitives/feedback/Spinner/Spinner.helpers.ts +40 -0
  101. package/src/primitives/feedback/Spinner/Spinner.tsx +105 -0
  102. package/src/primitives/feedback/Spinner/Spinner.types.ts +114 -0
  103. package/src/primitives/feedback/Spinner/index.ts +18 -0
  104. package/src/primitives/feedback/Spinner/useSpinnerLogic.ts +84 -0
  105. package/src/primitives/feedback/Toast/Toast.helpers.ts +163 -0
  106. package/src/primitives/feedback/Toast/Toast.tsx +190 -0
  107. package/src/primitives/feedback/Toast/Toast.types.ts +270 -0
  108. package/src/primitives/feedback/Toast/ToastContext.tsx +96 -0
  109. package/src/primitives/feedback/Toast/ToastProvider.tsx +241 -0
  110. package/src/primitives/feedback/Toast/index.ts +59 -0
  111. package/src/primitives/feedback/Toast/useToastLogic.ts +112 -0
  112. package/src/primitives/feedback/index.ts +45 -0
  113. package/src/primitives/index.ts +158 -0
  114. package/src/primitives/inputs/Checkbox/Checkbox.helpers.ts +132 -0
  115. package/src/primitives/inputs/Checkbox/Checkbox.tsx +150 -0
  116. package/src/primitives/inputs/Checkbox/Checkbox.types.ts +106 -0
  117. package/src/primitives/inputs/Checkbox/index.ts +30 -0
  118. package/src/primitives/inputs/Checkbox/useCheckboxLogic.ts +121 -0
  119. package/src/primitives/inputs/RadioButton/RadioButton.helpers.ts +123 -0
  120. package/src/primitives/inputs/RadioButton/RadioButton.tsx +159 -0
  121. package/src/primitives/inputs/RadioButton/RadioButton.types.ts +106 -0
  122. package/src/primitives/inputs/RadioButton/index.ts +25 -0
  123. package/src/primitives/inputs/RadioButton/useRadioButtonLogic.ts +117 -0
  124. package/src/primitives/inputs/SegmentedControl/SegmentedControl.helpers.ts +174 -0
  125. package/src/primitives/inputs/SegmentedControl/SegmentedControl.tsx +224 -0
  126. package/src/primitives/inputs/SegmentedControl/SegmentedControl.types.ts +187 -0
  127. package/src/primitives/inputs/SegmentedControl/index.ts +39 -0
  128. package/src/primitives/inputs/SegmentedControl/useSegmentedControlLogic.ts +151 -0
  129. package/src/primitives/inputs/SelectSheet/SelectSheet.helpers.ts +147 -0
  130. package/src/primitives/inputs/SelectSheet/SelectSheet.tsx +247 -0
  131. package/src/primitives/inputs/SelectSheet/SelectSheet.types.ts +196 -0
  132. package/src/primitives/inputs/SelectSheet/SelectSheetOption.tsx +177 -0
  133. package/src/primitives/inputs/SelectSheet/index.ts +48 -0
  134. package/src/primitives/inputs/SelectSheet/useSelectSheetLogic.ts +309 -0
  135. package/src/primitives/inputs/Switch/Switch.helpers.ts +109 -0
  136. package/src/primitives/inputs/Switch/Switch.tsx +191 -0
  137. package/src/primitives/inputs/Switch/Switch.types.ts +154 -0
  138. package/src/primitives/inputs/Switch/index.ts +40 -0
  139. package/src/primitives/inputs/Switch/useSwitchLogic.ts +192 -0
  140. package/src/primitives/inputs/TextInput/TextInput.helpers.ts +206 -0
  141. package/src/primitives/inputs/TextInput/TextInput.tsx +392 -0
  142. package/src/primitives/inputs/TextInput/TextInput.types.ts +216 -0
  143. package/src/primitives/inputs/TextInput/index.ts +37 -0
  144. package/src/primitives/inputs/TextInput/useTextInputLogic.ts +195 -0
  145. package/src/primitives/inputs/index.ts +52 -0
  146. package/src/primitives/layout/AnimatedBox.tsx +44 -0
  147. package/src/primitives/layout/Box.tsx +71 -0
  148. package/src/primitives/layout/Divider/Divider.helpers.ts +115 -0
  149. package/src/primitives/layout/Divider/Divider.tsx +139 -0
  150. package/src/primitives/layout/Divider/Divider.types.ts +178 -0
  151. package/src/primitives/layout/Divider/index.ts +24 -0
  152. package/src/primitives/layout/Divider/useDividerLogic.ts +109 -0
  153. package/src/primitives/layout/FlatList.tsx +66 -0
  154. package/src/primitives/layout/Pressable.tsx +74 -0
  155. package/src/primitives/layout/ScrollView.tsx +63 -0
  156. package/src/primitives/layout/Stack.tsx +69 -0
  157. package/src/primitives/layout/index.ts +40 -0
  158. package/src/primitives/navigation/index.ts +6 -0
  159. package/src/primitives/overlays/Modal/Modal.helpers.ts +31 -0
  160. package/src/primitives/overlays/Modal/Modal.tsx +264 -0
  161. package/src/primitives/overlays/Modal/Modal.types.ts +193 -0
  162. package/src/primitives/overlays/Modal/index.ts +43 -0
  163. package/src/primitives/overlays/Modal/useModalLogic.ts +103 -0
  164. package/src/primitives/overlays/index.ts +12 -0
  165. package/src/primitives/typography/Text.tsx +51 -0
  166. package/src/primitives/typography/index.ts +1 -0
  167. package/src/provider/DesignSystemContext.ts +22 -0
  168. package/src/provider/DesignSystemProvider.tsx +121 -0
  169. package/src/provider/index.ts +7 -0
  170. package/src/providers/ThemeProvider/createTheme.ts +304 -0
  171. package/src/providers/ThemeProvider/defaultTheme.ts +70 -0
  172. package/src/providers/ThemeProvider/index.ts +34 -0
  173. package/src/providers/ThemeProvider/types.ts +249 -0
  174. package/src/providers/index.ts +29 -0
  175. package/src/tokens/colors.ts +371 -0
  176. package/src/tokens/index.ts +145 -0
  177. package/src/tokens/motion.ts +176 -0
  178. package/src/tokens/radii.ts +82 -0
  179. package/src/tokens/scales.ts +588 -0
  180. package/src/tokens/shadows.ts +190 -0
  181. package/src/tokens/spacing.ts +140 -0
  182. package/src/tokens/tokens.json +207 -0
  183. package/src/tokens/typography.ts +251 -0
  184. package/src/types.ts +50 -0
  185. package/src/utils/accessibility.ts +169 -0
  186. package/src/utils/index.ts +25 -0
  187. package/src/utils/platform.ts +72 -0
@@ -0,0 +1,82 @@
1
+ /**
2
+ * StayEasy Design System - Border Radius Tokens
3
+ *
4
+ * Philosophy: Rounded but not bubbly.
5
+ * Friendly corners that feel premium, not childish.
6
+ *
7
+ * @example
8
+ * borderRadius: theme.radii.md // 12
9
+ */
10
+
11
+ // =============================================================================
12
+ // BORDER RADIUS SCALE
13
+ // =============================================================================
14
+
15
+ export const radii = {
16
+ /** 0 - Sharp corners */
17
+ none: 0,
18
+
19
+ /** 4 - Subtle rounding */
20
+ xs: 4,
21
+
22
+ /** 8 - Light rounding */
23
+ sm: 8,
24
+
25
+ /** 12 - Standard rounding (most common) */
26
+ md: 12,
27
+
28
+ /** 16 - Generous rounding */
29
+ lg: 16,
30
+
31
+ /** 20 - Large rounding */
32
+ xl: 20,
33
+
34
+ /** 24 - Extra large rounding */
35
+ '2xl': 24,
36
+
37
+ /** 32 - Very large rounding */
38
+ '3xl': 32,
39
+
40
+ /** 9999 - Full/pill rounding */
41
+ full: 9999,
42
+ } as const;
43
+
44
+ // =============================================================================
45
+ // SEMANTIC RADII
46
+ // =============================================================================
47
+
48
+ export const semanticRadii = {
49
+ /** Buttons - friendly but not too soft */
50
+ button: radii.md, // 12
51
+
52
+ /** Input fields - matches buttons */
53
+ input: radii.md, // 12
54
+
55
+ /** Cards - generous for premium feel */
56
+ card: radii.lg, // 16
57
+
58
+ /** Modals - prominent rounding */
59
+ modal: radii.xl, // 20
60
+
61
+ /** Bottom sheets - very rounded top */
62
+ sheet: radii['2xl'], // 24
63
+
64
+ /** Chips/badges - pill-like */
65
+ chip: radii.full, // 9999
66
+
67
+ /** Avatars - circular */
68
+ avatar: radii.full, // 9999
69
+
70
+ /** Image thumbnails */
71
+ thumbnail: radii.md, // 12
72
+
73
+ /** Toast notifications */
74
+ toast: radii.md, // 12
75
+ } as const;
76
+
77
+ // =============================================================================
78
+ // TYPE EXPORTS
79
+ // =============================================================================
80
+
81
+ export type Radii = typeof radii;
82
+ export type RadiiKey = keyof typeof radii;
@@ -0,0 +1,588 @@
1
+ /**
2
+ * El Sendero Design System - Scale Tokens
3
+ *
4
+ * Defines mathematical relationships between component sizes.
5
+ * All ratios are explicitly documented to prevent magic numbers.
6
+ *
7
+ * @example
8
+ * import { ICON_BUTTON_ICON_SIZE, calculateHitSlop } from '@/design-system/tokens/scales';
9
+ */
10
+
11
+ // =============================================================================
12
+ // ICON RATIO CONSTANTS
13
+ // =============================================================================
14
+
15
+ /**
16
+ * Icon-to-container ratios for different component types.
17
+ * These ensure visual consistency across the design system.
18
+ *
19
+ * Design Rationale:
20
+ * - Circular buttons need breathing room for touch feedback ripples
21
+ * - Toggle controls need maximum icon visibility within compact containers
22
+ * - Text-adjacent icons should match the text visual weight
23
+ */
24
+ export const ICON_RATIOS = {
25
+ /**
26
+ * Circular button ratio (~45%)
27
+ * Used by: IconButton
28
+ * Provides breathing room for touch feedback ripples
29
+ */
30
+ circularButton: 0.45,
31
+
32
+ /**
33
+ * Toggle control ratio (~75%)
34
+ * Used by: Checkbox, Radio
35
+ * Maximizes icon visibility within compact container
36
+ */
37
+ toggleControl: 0.75,
38
+
39
+ /**
40
+ * Text-adjacent ratio (100%)
41
+ * Used by: Button with icon, TextInput icons
42
+ * Icon matches the text visual weight
43
+ */
44
+ textAdjacent: 1.0,
45
+ } as const;
46
+
47
+ // =============================================================================
48
+ // COMPONENT SIZE SCALES
49
+ // =============================================================================
50
+
51
+ /**
52
+ * Unified container size scale (in pixels)
53
+ * All interactive button components use this scale.
54
+ *
55
+ * | Size | Pixels | Use Case |
56
+ * |------|--------|----------|
57
+ * | sm | 36 | Dense UIs, secondary actions |
58
+ * | md | 44 | Default (meets Apple HIG 44px touch target) |
59
+ * | lg | 56 | Tablets, high-emphasis actions |
60
+ */
61
+ export const CONTAINER_SIZES = {
62
+ sm: 36,
63
+ md: 44,
64
+ lg: 56,
65
+ } as const;
66
+
67
+ /**
68
+ * Button heights (slightly different scale for text buttons)
69
+ *
70
+ * | Size | Pixels | Use Case |
71
+ * |------|--------|----------|
72
+ * | sm | 36 | Dense forms, secondary actions |
73
+ * | md | 44 | Default mobile buttons |
74
+ * | lg | 52 | Primary CTAs, prominent actions |
75
+ */
76
+ export const BUTTON_HEIGHTS = {
77
+ sm: 36,
78
+ md: 44,
79
+ lg: 52,
80
+ } as const;
81
+
82
+ /**
83
+ * Toggle control container sizes (compact scale for form controls)
84
+ *
85
+ * | Size | Pixels | Use Case |
86
+ * |------|--------|----------|
87
+ * | sm | 20 | Dense forms, compact UI |
88
+ * | md | 24 | Default mobile checkboxes |
89
+ * | lg | 32 | Tablets, accessibility focus |
90
+ */
91
+ export const TOGGLE_CONTAINER_SIZES = {
92
+ sm: 20,
93
+ md: 24,
94
+ lg: 32,
95
+ } as const;
96
+
97
+ // =============================================================================
98
+ // ICON SIZE VALUES (in pixels)
99
+ // =============================================================================
100
+
101
+ /**
102
+ * Icon size scale in pixels.
103
+ * Mirrors the iconSizes theme values for calculations.
104
+ */
105
+ export const ICON_PIXEL_SIZES = {
106
+ xs: 12,
107
+ sm: 16,
108
+ md: 20,
109
+ lg: 24,
110
+ xl: 32,
111
+ "2xl": 40,
112
+ "3xl": 48,
113
+ } as const;
114
+
115
+ // =============================================================================
116
+ // DERIVED ICON SIZE MAPPINGS
117
+ // =============================================================================
118
+
119
+ type ComponentSize = "sm" | "md" | "lg";
120
+ type IconSizeKey = "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl";
121
+
122
+ /**
123
+ * IconButton: Container -> Icon mapping
124
+ *
125
+ * Uses ~45% ratio (ICON_RATIOS.circularButton)
126
+ *
127
+ * | Size | Container | Icon | Actual Ratio |
128
+ * |------|-----------|--------|--------------|
129
+ * | sm | 36px | 16px | 44% |
130
+ * | md | 44px | 20px | 45% |
131
+ * | lg | 56px | 24px | 43% |
132
+ */
133
+ export const ICON_BUTTON_ICON_SIZE: Record<ComponentSize, IconSizeKey> = {
134
+ sm: "sm", // 16px / 36px = 44%
135
+ md: "md", // 20px / 44px = 45%
136
+ lg: "lg", // 24px / 56px = 43%
137
+ } as const;
138
+
139
+ /**
140
+ * Checkbox: Container -> Icon mapping
141
+ *
142
+ * Uses ~75% ratio (ICON_RATIOS.toggleControl)
143
+ *
144
+ * | Size | Container | Icon | Actual Ratio |
145
+ * |------|-----------|--------|--------------|
146
+ * | sm | 20px | 16px | 80% |
147
+ * | md | 24px | 20px | 83% |
148
+ * | lg | 32px | 24px | 75% |
149
+ */
150
+ export const CHECKBOX_ICON_SIZE: Record<ComponentSize, IconSizeKey> = {
151
+ sm: "sm", // 16px / 20px = 80%
152
+ md: "md", // 20px / 24px = 83%
153
+ lg: "lg", // 24px / 32px = 75%
154
+ } as const;
155
+
156
+ /**
157
+ * Button: Size -> Icon mapping (text-adjacent)
158
+ *
159
+ * Icons match label visual weight for optical balance.
160
+ *
161
+ * | Size | Height | Icon | Text Variant |
162
+ * |------|--------|--------|--------------|
163
+ * | sm | 36px | 16px | labelSmall |
164
+ * | md | 44px | 20px | labelMedium |
165
+ * | lg | 52px | 24px | labelLarge |
166
+ */
167
+ export const BUTTON_ICON_SIZE: Record<ComponentSize, IconSizeKey> = {
168
+ sm: "sm", // 16px
169
+ md: "md", // 20px
170
+ lg: "lg", // 24px
171
+ } as const;
172
+
173
+ /**
174
+ * TextInput: Size -> Icon mapping
175
+ *
176
+ * Icons match input text size for visual harmony.
177
+ *
178
+ * | Size | Icon |
179
+ * |------|--------|
180
+ * | sm | 16px |
181
+ * | md | 20px |
182
+ * | lg | 24px |
183
+ */
184
+ export const TEXT_INPUT_ICON_SIZE: Record<ComponentSize, IconSizeKey> = {
185
+ sm: "sm",
186
+ md: "md",
187
+ lg: "lg",
188
+ } as const;
189
+
190
+ // =============================================================================
191
+ // SWITCH SPECIFIC SCALES
192
+ // =============================================================================
193
+
194
+ /**
195
+ * Switch thumb inset in pixels.
196
+ * This is the padding between the track edge and the thumb.
197
+ * Total thumb padding = inset * 2 (both sides)
198
+ *
199
+ * Derived from the 2xs spacing token (2px) for consistency.
200
+ */
201
+ export const SWITCH_THUMB_INSET = 2;
202
+
203
+ /**
204
+ * Switch dimension definitions.
205
+ *
206
+ * Track width:height ratio is maintained at approximately 1.7:1
207
+ * Thumb size is calculated as: trackHeight - (SWITCH_THUMB_INSET * 2)
208
+ *
209
+ * | Size | Track W | Track H | Thumb | W:H Ratio |
210
+ * |------|---------|---------|-------|-----------|
211
+ * | sm | 40px | 24px | 20px | 1.67:1 |
212
+ * | md | 48px | 28px | 24px | 1.71:1 |
213
+ * | lg | 56px | 32px | 28px | 1.75:1 |
214
+ */
215
+ export const SWITCH_DIMENSIONS: Record<
216
+ ComponentSize,
217
+ { trackWidth: number; trackHeight: number }
218
+ > = {
219
+ sm: { trackWidth: 40, trackHeight: 24 },
220
+ md: { trackWidth: 48, trackHeight: 28 },
221
+ lg: { trackWidth: 56, trackHeight: 32 },
222
+ } as const;
223
+
224
+ /**
225
+ * Calculate switch thumb size from track height.
226
+ * Uses SWITCH_THUMB_INSET to maintain consistent padding.
227
+ *
228
+ * @param trackHeight - The track height in pixels
229
+ * @returns The thumb diameter in pixels
230
+ */
231
+ export function calculateSwitchThumbSize(trackHeight: number): number {
232
+ return trackHeight - SWITCH_THUMB_INSET * 2;
233
+ }
234
+
235
+ // =============================================================================
236
+ // TOUCH TARGET UTILITIES
237
+ // =============================================================================
238
+
239
+ /**
240
+ * Minimum touch target size per Apple Human Interface Guidelines.
241
+ * All interactive elements should have at least 44x44px touch area.
242
+ */
243
+ export const MIN_TOUCH_TARGET = 44;
244
+
245
+ /**
246
+ * Calculate hitSlop needed to reach minimum touch target.
247
+ *
248
+ * This function ensures all interactive elements meet the 44px
249
+ * minimum touch target requirement, regardless of their visual size.
250
+ *
251
+ * @param visualSize - The visual size of the element in pixels
252
+ * @returns Insets object to expand the touch target
253
+ *
254
+ * @example
255
+ * // 36px button needs 4px padding each side to reach 44px
256
+ * calculateHitSlop(36) // { top: 4, bottom: 4, left: 4, right: 4 }
257
+ *
258
+ * // 44px button already meets target
259
+ * calculateHitSlop(44) // { top: 0, bottom: 0, left: 0, right: 0 }
260
+ */
261
+ export function calculateHitSlop(visualSize: number): {
262
+ top: number;
263
+ bottom: number;
264
+ left: number;
265
+ right: number;
266
+ } {
267
+ if (visualSize >= MIN_TOUCH_TARGET) {
268
+ return { top: 0, bottom: 0, left: 0, right: 0 };
269
+ }
270
+
271
+ const padding = Math.ceil((MIN_TOUCH_TARGET - visualSize) / 2);
272
+ return { top: padding, bottom: padding, left: padding, right: padding };
273
+ }
274
+
275
+ /**
276
+ * Pre-calculated hitSlop for IconButton container sizes.
277
+ * Uses calculateHitSlop internally.
278
+ *
279
+ * | Size | Container | hitSlop | Total Touch Area |
280
+ * |------|-----------|---------|------------------|
281
+ * | sm | 36px | 4px | 44px |
282
+ * | md | 44px | 0px | 44px |
283
+ * | lg | 56px | 0px | 56px |
284
+ */
285
+ export const ICON_BUTTON_HIT_SLOP: Record<
286
+ ComponentSize,
287
+ { top: number; bottom: number; left: number; right: number }
288
+ > = {
289
+ sm: calculateHitSlop(CONTAINER_SIZES.sm),
290
+ md: calculateHitSlop(CONTAINER_SIZES.md),
291
+ lg: calculateHitSlop(CONTAINER_SIZES.lg),
292
+ } as const;
293
+
294
+ /**
295
+ * Pre-calculated hitSlop for Checkbox container sizes.
296
+ *
297
+ * | Size | Container | hitSlop | Total Touch Area |
298
+ * |------|-----------|---------|------------------|
299
+ * | sm | 20px | 12px | 44px |
300
+ * | md | 24px | 10px | 44px |
301
+ * | lg | 32px | 6px | 44px |
302
+ */
303
+ export const CHECKBOX_HIT_SLOP: Record<
304
+ ComponentSize,
305
+ { top: number; bottom: number; left: number; right: number }
306
+ > = {
307
+ sm: calculateHitSlop(TOGGLE_CONTAINER_SIZES.sm),
308
+ md: calculateHitSlop(TOGGLE_CONTAINER_SIZES.md),
309
+ lg: calculateHitSlop(TOGGLE_CONTAINER_SIZES.lg),
310
+ } as const;
311
+
312
+ /**
313
+ * RadioButton inner dot size mapping.
314
+ *
315
+ * Uses ~40% ratio of container for the inner filled dot.
316
+ *
317
+ * | Size | Container | Inner Dot | Actual Ratio |
318
+ * |------|-----------|-----------|--------------|
319
+ * | sm | 20px | 8px | 40% |
320
+ * | md | 24px | 10px | 42% |
321
+ * | lg | 32px | 14px | 44% |
322
+ */
323
+ export const RADIO_BUTTON_INNER_SIZE: Record<ComponentSize, number> = {
324
+ sm: 8,
325
+ md: 10,
326
+ lg: 14,
327
+ } as const;
328
+
329
+ /**
330
+ * Pre-calculated hitSlop for RadioButton container sizes.
331
+ * Same as Checkbox (uses TOGGLE_CONTAINER_SIZES).
332
+ *
333
+ * | Size | Container | hitSlop | Total Touch Area |
334
+ * |------|-----------|---------|------------------|
335
+ * | sm | 20px | 12px | 44px |
336
+ * | md | 24px | 10px | 44px |
337
+ * | lg | 32px | 6px | 44px |
338
+ */
339
+ export const RADIO_BUTTON_HIT_SLOP: Record<
340
+ ComponentSize,
341
+ { top: number; bottom: number; left: number; right: number }
342
+ > = {
343
+ sm: calculateHitSlop(TOGGLE_CONTAINER_SIZES.sm),
344
+ md: calculateHitSlop(TOGGLE_CONTAINER_SIZES.md),
345
+ lg: calculateHitSlop(TOGGLE_CONTAINER_SIZES.lg),
346
+ } as const;
347
+
348
+ /**
349
+ * Pre-calculated hitSlop for Switch track (uses height for calculation).
350
+ *
351
+ * | Size | Track H | hitSlop | Total Touch Area |
352
+ * |------|---------|---------|------------------|
353
+ * | sm | 24px | 10px | 44px |
354
+ * | md | 28px | 8px | 44px |
355
+ * | lg | 32px | 6px | 44px |
356
+ */
357
+ export const SWITCH_HIT_SLOP: Record<
358
+ ComponentSize,
359
+ { top: number; bottom: number; left: number; right: number }
360
+ > = {
361
+ sm: calculateHitSlop(SWITCH_DIMENSIONS.sm.trackHeight),
362
+ md: calculateHitSlop(SWITCH_DIMENSIONS.md.trackHeight),
363
+ lg: calculateHitSlop(SWITCH_DIMENSIONS.lg.trackHeight),
364
+ } as const;
365
+
366
+ /**
367
+ * hitSlop for ghost IconButton variant.
368
+ * Since ghost has no container, we use the icon visual size.
369
+ *
370
+ * | Size | Icon | hitSlop | Total Touch Area |
371
+ * |------|--------|---------|------------------|
372
+ * | sm | 16px | 14px | 44px |
373
+ * | md | 20px | 12px | 44px |
374
+ * | lg | 24px | 10px | 44px |
375
+ */
376
+ export const GHOST_ICON_BUTTON_HIT_SLOP: Record<
377
+ ComponentSize,
378
+ { top: number; bottom: number; left: number; right: number }
379
+ > = {
380
+ sm: calculateHitSlop(ICON_PIXEL_SIZES.sm),
381
+ md: calculateHitSlop(ICON_PIXEL_SIZES.md),
382
+ lg: calculateHitSlop(ICON_PIXEL_SIZES.lg),
383
+ } as const;
384
+
385
+ // =============================================================================
386
+ // SPINNER SIZE MAPPING
387
+ // =============================================================================
388
+
389
+ /**
390
+ * Spinner size mapping - consistent across all components.
391
+ *
392
+ * | Component Size | Spinner Size |
393
+ * |----------------|--------------|
394
+ * | sm | sm |
395
+ * | md | sm |
396
+ * | lg | lg |
397
+ */
398
+ export const SPINNER_SIZE: Record<ComponentSize, "sm" | "lg"> = {
399
+ sm: "sm",
400
+ md: "sm",
401
+ lg: "lg",
402
+ } as const;
403
+
404
+ // =============================================================================
405
+ // TEXT VARIANT MAPPINGS
406
+ // =============================================================================
407
+
408
+ type TextVariantKey =
409
+ | "labelSmall"
410
+ | "labelMedium"
411
+ | "labelLarge"
412
+ | "bodySmall"
413
+ | "bodyMedium"
414
+ | "bodyLarge";
415
+
416
+ /**
417
+ * Button text variant mapping.
418
+ *
419
+ * | Button Size | Text Variant |
420
+ * |-------------|--------------|
421
+ * | sm | labelSmall |
422
+ * | md | labelMedium |
423
+ * | lg | labelLarge |
424
+ */
425
+ export const BUTTON_TEXT_VARIANT: Record<ComponentSize, TextVariantKey> = {
426
+ sm: "labelSmall",
427
+ md: "labelMedium",
428
+ lg: "labelLarge",
429
+ } as const;
430
+
431
+ // =============================================================================
432
+ // PROGRESS BAR SIZE MAPPING
433
+ // =============================================================================
434
+
435
+ /**
436
+ * Progress bar height mapping.
437
+ *
438
+ * | Size | Height |
439
+ * |------|--------|
440
+ * | sm | 4px |
441
+ * | md | 8px |
442
+ * | lg | 12px |
443
+ */
444
+ export const PROGRESS_BAR_HEIGHTS: Record<ComponentSize, number> = {
445
+ sm: 4,
446
+ md: 8,
447
+ lg: 12,
448
+ } as const;
449
+
450
+ /**
451
+ * Progress bar border radius mapping.
452
+ * Uses half of height for pill shape.
453
+ *
454
+ * | Size | Radius |
455
+ * |------|--------|
456
+ * | sm | 2px |
457
+ * | md | 4px |
458
+ * | lg | 6px |
459
+ */
460
+ export const PROGRESS_BAR_BORDER_RADIUS: Record<ComponentSize, number> = {
461
+ sm: 2,
462
+ md: 4,
463
+ lg: 6,
464
+ } as const;
465
+
466
+ // =============================================================================
467
+ // ANIMATION TOKENS
468
+ // =============================================================================
469
+
470
+ /**
471
+ * Animation spring configuration tokens.
472
+ * Used by animated components for consistent motion.
473
+ */
474
+ export const ANIMATION_TOKENS = {
475
+ /**
476
+ * Switch toggle animation config.
477
+ * Uses spring physics for natural feel.
478
+ */
479
+ switch: {
480
+ /** Main toggle animation */
481
+ toggle: { friction: 8, tension: 100 },
482
+ /** Thumb scale feedback animation */
483
+ thumbScale: { friction: 6, tension: 200 },
484
+ },
485
+ } as const;
486
+
487
+ /**
488
+ * Shadow tokens for elevated components.
489
+ */
490
+ export const SHADOW_TOKENS = {
491
+ /**
492
+ * Switch thumb shadow.
493
+ * Provides depth and separation from track.
494
+ */
495
+ switchThumb: {
496
+ shadowColor: "#000",
497
+ shadowOffset: { width: 0, height: 2 },
498
+ shadowOpacity: 0.2,
499
+ shadowRadius: 2,
500
+ elevation: 2,
501
+ },
502
+ } as const;
503
+
504
+ // =============================================================================
505
+ // SELECT SHEET DIMENSIONS
506
+ // =============================================================================
507
+
508
+ /**
509
+ * SelectSheet industry-standard dimensions based on Material Design 3 and iOS HIG.
510
+ *
511
+ * Design Rationale:
512
+ * - Center dialog: max-width 400px (mobile), 560px (tablet) for readability
513
+ * - Bottom sheet: full width on mobile, max-width on tablet
514
+ * - Header height: 56px (Material Design standard)
515
+ * - Option row height: 52px (between Material 48px and iOS 44px minimum)
516
+ * - Corner radius: 16px (modern rounded aesthetic)
517
+ * - Overlay opacity: 50% (balanced backdrop)
518
+ *
519
+ * Responsive Breakpoints:
520
+ * - Phone: < 768px (compact layout)
521
+ * - Tablet: >= 768px (expanded layout with max-width constraints)
522
+ */
523
+
524
+ /**
525
+ * SelectSheet size scale for different use cases.
526
+ *
527
+ * | Size | Max Width | List Height | Use Case |
528
+ * |------|-----------|-------------|----------|
529
+ * | sm | 320px | 200px | Few options, compact dialogs |
530
+ * | md | 400px | 300px | Default, most use cases |
531
+ * | lg | 560px | 400px | Tablets, many options |
532
+ */
533
+ /**
534
+ * SelectSheet layout constants.
535
+ *
536
+ * Simplified to match Modal pattern:
537
+ * - Phone: Full width minus horizontalMargin (16px)
538
+ * - Tablet: Capped at tabletMaxWidth (480px) for phone-like experience
539
+ */
540
+ export const SELECT_SHEET_LAYOUT = {
541
+ /** Horizontal margin from screen edges */
542
+ horizontalMargin: 16,
543
+ /** Maximum width on tablets (phone-like cap) */
544
+ tabletMaxWidth: 480,
545
+ /** Option row height (touch target minimum) */
546
+ optionRowHeight: 52,
547
+ /** Header height */
548
+ headerHeight: 56,
549
+ /** Footer height (Done button area) */
550
+ footerHeight: 56,
551
+ /** Overlay background opacity */
552
+ overlayOpacity: 0.5,
553
+ /** Default list height - good UX for most cases */
554
+ defaultListHeight: 300,
555
+ /** Tablet breakpoint in pixels */
556
+ tabletBreakpoint: 768,
557
+ } as const;
558
+
559
+ // =============================================================================
560
+ // MODAL COMPONENT SCALES
561
+ // =============================================================================
562
+
563
+ /**
564
+ * Modal spacing and layout constants.
565
+ *
566
+ * Width behavior:
567
+ * - Phone: Full width minus horizontalMargin (16px = spacing.lg) on each side
568
+ * - Tablet (≥768px): Capped at tabletMaxWidth (480px) for phone-like experience
569
+ */
570
+ export const MODAL_LAYOUT = {
571
+ /** Horizontal margin from screen edges (matches spacing.lg = 16px) */
572
+ horizontalMargin: 16,
573
+ /** Max width on tablets — prevents the modal from stretching too wide */
574
+ tabletMaxWidth: 480,
575
+ /** Corner radius for the modal container */
576
+ borderRadius: 16,
577
+ /** Overlay background opacity */
578
+ overlayOpacity: 0.5,
579
+ /** Tablet breakpoint in pixels */
580
+ tabletBreakpoint: 768,
581
+ } as const;
582
+
583
+ // =============================================================================
584
+ // TYPE EXPORTS
585
+ // =============================================================================
586
+
587
+ export type ComponentSizeType = ComponentSize;
588
+ export type IconSizeType = IconSizeKey;