Live video on the AT Protocol
1/**
2 * Theme atoms - Enhanced exports with pairify function for array-style syntax
3 * These provide direct access to static design tokens and support composition
4 */
5
6import { Platform } from "react-native";
7import {
8 animations,
9 borderRadius,
10 colors as rawColors,
11 shadows,
12 spacing,
13 touchTargets,
14 typography,
15} from "./tokens";
16
17// Type for style objects that can be spread
18type StyleValue = Record<string, any>;
19
20/**
21 * Pairify function - converts nested objects into key-value pairs that return style objects
22 * This allows for array-style syntax like style={[a.borders.green[300], a.shadows.xl]}
23 */
24function pairify<T extends Record<string, any>>(
25 obj: T,
26 styleKeyPrefix: string,
27): Record<keyof T, StyleValue> {
28 const result: Record<string, StyleValue> = {};
29
30 for (const [key, value] of Object.entries(obj)) {
31 if (typeof value === "object" && value !== null && !Array.isArray(value)) {
32 // For nested objects (like color scales), create another level
33 result[key] = {};
34 for (const [nestedKey, nestedValue] of Object.entries(value)) {
35 result[key][nestedKey] = { [styleKeyPrefix]: nestedValue };
36 }
37 } else {
38 // For simple values, create the style object directly
39 result[key] = { [styleKeyPrefix]: value };
40 }
41 }
42
43 return result as Record<keyof T, StyleValue>;
44}
45
46/**
47 * Create pairified style atoms for easy composition
48 */
49
50// Re-export static design tokens that don't change with theme
51export { animations, borderRadius, shadows, spacing, touchTargets };
52
53// Export raw color tokens for advanced use cases
54export const colors = rawColors;
55
56// Platform-aware typography helper
57export const getPlatformTypography = () => {
58 if (Platform.OS === "ios") {
59 return typography.ios;
60 } else if (Platform.OS === "android") {
61 return typography.android;
62 }
63 return typography.universal;
64};
65
66// Export all typography scales
67export const typographyAtoms = {
68 platform: getPlatformTypography(),
69 universal: typography.universal,
70 ios: typography.ios,
71 android: typography.android,
72};
73
74// Static icon sizes (colors are handled by theme)
75export const iconSizes = {
76 sm: 16,
77 md: 20,
78 lg: 24,
79 xl: 32,
80};
81
82// Common layout utilities
83export const layout = {
84 flex: {
85 center: {
86 justifyContent: "center" as const,
87 alignItems: "center" as const,
88 },
89 alignCenter: {
90 alignItems: "center" as const,
91 },
92 justifyCenter: {
93 justifyContent: "center" as const,
94 },
95 row: {
96 flexDirection: "row" as const,
97 },
98 column: {
99 flexDirection: "column" as const,
100 },
101 spaceBetween: {
102 justifyContent: "space-between" as const,
103 },
104 spaceAround: {
105 justifyContent: "space-around" as const,
106 },
107 spaceEvenly: {
108 justifyContent: "space-evenly" as const,
109 },
110 },
111 position: {
112 absolute: {
113 position: "absolute" as const,
114 },
115 relative: {
116 position: "relative" as const,
117 },
118 },
119};
120
121// Enhanced border utilities with pairified colors and widths
122export const borders = {
123 width: pairify(
124 {
125 thin: 1,
126 medium: 2,
127 thick: 4,
128 },
129 "borderWidth",
130 ),
131
132 style: {
133 solid: { borderStyle: "solid" as const },
134 dashed: { borderStyle: "dashed" as const },
135 dotted: { borderStyle: "dotted" as const },
136 },
137
138 // Pairified color borders
139 color: pairify(rawColors, "borderColor"),
140
141 // Top border utilities
142 top: {
143 width: pairify(
144 {
145 thin: 1,
146 medium: 2,
147 thick: 4,
148 },
149 "borderTopWidth",
150 ),
151 color: pairify(rawColors, "borderTopColor"),
152 },
153
154 // Bottom border utilities
155 bottom: {
156 width: pairify(
157 {
158 thin: 1,
159 medium: 2,
160 thick: 4,
161 },
162 "borderBottomWidth",
163 ),
164 color: pairify(rawColors, "borderBottomColor"),
165 },
166
167 // Left border utilities
168 left: {
169 width: pairify(
170 {
171 thin: 1,
172 medium: 2,
173 thick: 4,
174 },
175 "borderLeftWidth",
176 ),
177 color: pairify(rawColors, "borderLeftColor"),
178 },
179
180 // Right border utilities
181 right: {
182 width: pairify(
183 {
184 thin: 1,
185 medium: 2,
186 thick: 4,
187 },
188 "borderRightWidth",
189 ),
190 color: pairify(rawColors, "borderRightColor"),
191 },
192};
193
194// Pairified spacing utilities
195export const spacingAtoms = {
196 margin: pairify(spacing, "margin"),
197 marginTop: pairify(spacing, "marginTop"),
198 marginRight: pairify(spacing, "marginRight"),
199 marginBottom: pairify(spacing, "marginBottom"),
200 marginLeft: pairify(spacing, "marginLeft"),
201 marginHorizontal: pairify(spacing, "marginHorizontal"),
202 marginVertical: pairify(spacing, "marginVertical"),
203
204 padding: pairify(spacing, "padding"),
205 paddingTop: pairify(spacing, "paddingTop"),
206 paddingRight: pairify(spacing, "paddingRight"),
207 paddingBottom: pairify(spacing, "paddingBottom"),
208 paddingLeft: pairify(spacing, "paddingLeft"),
209 paddingHorizontal: pairify(spacing, "paddingHorizontal"),
210 paddingVertical: pairify(spacing, "paddingVertical"),
211};
212
213// Pairified border radius utilities
214export const radiusAtoms = {
215 all: pairify(borderRadius, "borderRadius"),
216 top: pairify(borderRadius, "borderTopLeftRadius"),
217 topRight: pairify(borderRadius, "borderTopRightRadius"),
218 bottom: pairify(borderRadius, "borderBottomLeftRadius"),
219 bottomRight: pairify(borderRadius, "borderBottomRightRadius"),
220 left: pairify(borderRadius, "borderTopLeftRadius"),
221 right: pairify(borderRadius, "borderTopRightRadius"),
222};
223
224// Background color utilities
225export const backgrounds = pairify(rawColors, "backgroundColor");
226
227// Text color utilities
228export const textColors = pairify(rawColors, "color");
229
230// Percentage-based sizes
231const percentageSizes = {
232 "10": "10%",
233 "20": "20%",
234 "25": "25%",
235 "30": "30%",
236 "33": "33.333333%",
237 "40": "40%",
238 "50": "50%",
239 "60": "60%",
240 "66": "66.666667%",
241 "70": "70%",
242 "75": "75%",
243 "80": "80%",
244 "90": "90%",
245 "100": "100%",
246} as const;
247
248// Size utilities (width and height)
249export const sizes = {
250 width: {
251 ...pairify(spacing, "width"),
252 percent: pairify(percentageSizes, "width"),
253 },
254 height: {
255 ...pairify(spacing, "height"),
256 percent: pairify(percentageSizes, "height"),
257 },
258 minWidth: {
259 ...pairify(spacing, "minWidth"),
260 percent: pairify(percentageSizes, "minWidth"),
261 },
262 minHeight: {
263 ...pairify(spacing, "minHeight"),
264 percent: pairify(percentageSizes, "minHeight"),
265 },
266 maxWidth: {
267 ...pairify(spacing, "maxWidth"),
268 percent: pairify(percentageSizes, "maxWidth"),
269 },
270 maxHeight: {
271 ...pairify(spacing, "maxHeight"),
272 percent: pairify(percentageSizes, "maxHeight"),
273 },
274};
275
276// Flex utilities
277export const flex = {
278 values: pairify(
279 {
280 0: 0,
281 1: 1,
282 2: 2,
283 3: 3,
284 4: 4,
285 5: 5,
286 },
287 "flex",
288 ),
289
290 grow: pairify(
291 {
292 0: 0,
293 1: 1,
294 },
295 "flexGrow",
296 ),
297
298 shrink: pairify(
299 {
300 0: 0,
301 1: 1,
302 },
303 "flexShrink",
304 ),
305
306 basis: {
307 ...pairify(spacing, "flexBasis"),
308 ...pairify(percentageSizes, "flexBasis"),
309 auto: { flexBasis: "auto" },
310 },
311};
312
313// Opacity utilities
314export const opacity = pairify(
315 {
316 0: 0,
317 5: 0.05,
318 10: 0.1,
319 20: 0.2,
320 25: 0.25,
321 30: 0.3,
322 40: 0.4,
323 50: 0.5,
324 60: 0.6,
325 70: 0.7,
326 75: 0.75,
327 80: 0.8,
328 90: 0.9,
329 95: 0.95,
330 100: 1,
331 },
332 "opacity",
333);
334
335// Z-index utilities
336export const zIndex = pairify(
337 {
338 0: 0,
339 10: 10,
340 20: 20,
341 30: 30,
342 40: 40,
343 50: 50,
344 auto: "auto",
345 },
346 "zIndex",
347);
348
349// Overflow utilities
350export const overflow = {
351 visible: { overflow: "visible" as const },
352 hidden: { overflow: "hidden" as const },
353 scroll: { overflow: "scroll" as const },
354};
355
356// Text alignment utilities
357export const textAlign = {
358 left: { textAlign: "left" as const },
359 center: { textAlign: "center" as const },
360 right: { textAlign: "right" as const },
361 justify: { textAlign: "justify" as const },
362 auto: { textAlign: "auto" as const },
363};
364
365// Font weight utilities
366export const fontWeight = pairify(
367 {
368 thin: "100",
369 extralight: "200",
370 light: "300",
371 normal: "400",
372 medium: "500",
373 semibold: "600",
374 bold: "700",
375 extrabold: "800",
376 black: "900",
377 },
378 "fontWeight",
379);
380
381// Font size utilities (separate from typography for quick access)
382export const fontSize = pairify(
383 {
384 xs: 12,
385 sm: 14,
386 base: 16,
387 lg: 18,
388 xl: 20,
389 "2xl": 24,
390 "3xl": 30,
391 "4xl": 36,
392 "5xl": 48,
393 "6xl": 60,
394 "7xl": 72,
395 "8xl": 96,
396 "9xl": 128,
397 },
398 "fontSize",
399);
400
401// Line height utilities
402export const lineHeight = pairify(
403 {
404 none: 1,
405 tight: 1.25,
406 snug: 1.375,
407 normal: 1.5,
408 relaxed: 1.625,
409 loose: 2,
410 3: 12,
411 4: 16,
412 5: 20,
413 6: 24,
414 7: 28,
415 8: 32,
416 9: 36,
417 10: 40,
418 },
419 "lineHeight",
420);
421
422// Letter spacing utilities
423export const letterSpacing = pairify(
424 {
425 tighter: -0.5,
426 tight: -0.25,
427 normal: 0,
428 wide: 0.25,
429 wider: 0.5,
430 widest: 1,
431 },
432 "letterSpacing",
433);
434
435// Text transform utilities
436export const textTransform = {
437 uppercase: { textTransform: "uppercase" as const },
438 lowercase: { textTransform: "lowercase" as const },
439 capitalize: { textTransform: "capitalize" as const },
440 none: { textTransform: "none" as const },
441};
442
443// Text decoration utilities
444export const textDecoration = {
445 none: { textDecorationLine: "none" as const },
446 underline: { textDecorationLine: "underline" as const },
447 lineThrough: { textDecorationLine: "line-through" as const },
448 underlineLineThrough: {
449 textDecorationLine: "underline line-through" as const,
450 },
451};
452
453// Text align vertical utilities (React Native specific)
454export const textAlignVertical = {
455 auto: { textAlignVertical: "auto" as const },
456 top: { textAlignVertical: "top" as const },
457 bottom: { textAlignVertical: "bottom" as const },
458 center: { textAlignVertical: "center" as const },
459};
460
461// Transform utilities
462export const transforms = {
463 rotate: pairify(
464 {
465 0: 0,
466 1: 1,
467 2: 2,
468 3: 3,
469 6: 6,
470 12: 12,
471 45: 45,
472 90: 90,
473 180: 180,
474 270: 270,
475 },
476 "rotate",
477 ),
478
479 scale: pairify(
480 {
481 0: 0,
482 50: 0.5,
483 75: 0.75,
484 90: 0.9,
485 95: 0.95,
486 100: 1,
487 105: 1.05,
488 110: 1.1,
489 125: 1.25,
490 150: 1.5,
491 200: 2,
492 },
493 "scale",
494 ),
495
496 scaleX: pairify(
497 {
498 0: 0,
499 50: 0.5,
500 75: 0.75,
501 90: 0.9,
502 95: 0.95,
503 100: 1,
504 105: 1.05,
505 110: 1.1,
506 125: 1.25,
507 150: 1.5,
508 200: 2,
509 },
510 "scaleX",
511 ),
512
513 scaleY: pairify(
514 {
515 0: 0,
516 50: 0.5,
517 75: 0.75,
518 90: 0.9,
519 95: 0.95,
520 100: 1,
521 105: 1.05,
522 110: 1.1,
523 125: 1.25,
524 150: 1.5,
525 200: 2,
526 },
527 "scaleY",
528 ),
529
530 translateX: pairify(spacing, "translateX"),
531 translateY: pairify(spacing, "translateY"),
532};
533
534// Absolute positioning utilities
535export const position = {
536 top: pairify(spacing, "top"),
537 right: pairify(spacing, "right"),
538 bottom: pairify(spacing, "bottom"),
539 left: pairify(spacing, "left"),
540
541 // Common position combinations
542 topLeft: (top: number, left: number) => ({
543 position: "absolute" as const,
544 top,
545 left,
546 }),
547 topRight: (top: number, right: number) => ({
548 position: "absolute" as const,
549 top,
550 right,
551 }),
552 bottomLeft: (bottom: number, left: number) => ({
553 position: "absolute" as const,
554 bottom,
555 left,
556 }),
557 bottomRight: (bottom: number, right: number) => ({
558 position: "absolute" as const,
559 bottom,
560 right,
561 }),
562
563 // Percentage-based positioning
564 percent: {
565 top: pairify(
566 {
567 0: "0%",
568 25: "25%",
569 50: "50%",
570 75: "75%",
571 100: "100%",
572 },
573 "top",
574 ),
575 right: pairify(
576 {
577 0: "0%",
578 25: "25%",
579 50: "50%",
580 75: "75%",
581 100: "100%",
582 },
583 "right",
584 ),
585 bottom: pairify(
586 {
587 0: "0%",
588 25: "25%",
589 50: "50%",
590 75: "75%",
591 100: "100%",
592 },
593 "bottom",
594 ),
595 left: pairify(
596 {
597 0: "0%",
598 25: "25%",
599 50: "50%",
600 75: "75%",
601 100: "100%",
602 },
603 "left",
604 ),
605 },
606};
607
608// Aspect ratio utilities (React Native 0.71+)
609export const aspectRatio = pairify(
610 {
611 square: 1,
612 video: 16 / 9,
613 photo: 4 / 3,
614 portrait: 3 / 4,
615 wide: 21 / 9,
616 ultrawide: 32 / 9,
617 "1/1": 1,
618 "3/2": 3 / 2,
619 "4/3": 4 / 3,
620 "16/9": 16 / 9,
621 "21/9": 21 / 9,
622 },
623 "aspectRatio",
624);
625
626// Gap utilities (React Native 0.71+)
627export const gap = {
628 row: pairify(spacing, "rowGap"),
629 column: pairify(spacing, "columnGap"),
630 all: pairify(spacing, "gap"),
631};
632
633// Common layout patterns
634export const layouts = {
635 // Full screen
636 fullScreen: {
637 position: "absolute" as const,
638 top: 0,
639 left: 0,
640 right: 0,
641 bottom: 0,
642 },
643
644 // Centered content
645 centered: {
646 flex: 1,
647 justifyContent: "center" as const,
648 alignItems: "center" as const,
649 },
650
651 // Centered modal/overlay
652 overlay: {
653 position: "absolute" as const,
654 top: 0,
655 left: 0,
656 right: 0,
657 bottom: 0,
658 justifyContent: "center" as const,
659 alignItems: "center" as const,
660 backgroundColor: "rgba(0, 0, 0, 0.5)",
661 },
662
663 // Safe area friendly
664 safeContainer: {
665 flex: 1,
666 paddingTop: Platform.OS === "ios" ? 44 : 0, // Status bar height
667 },
668
669 // Row with space between
670 spaceBetweenRow: {
671 flexDirection: "row" as const,
672 justifyContent: "space-between" as const,
673 alignItems: "center" as const,
674 },
675
676 // Sticky header
677 stickyHeader: {
678 position: "absolute" as const,
679 top: 0,
680 left: 0,
681 right: 0,
682 zIndex: 10,
683 },
684
685 // Bottom sheet style
686 bottomSheet: {
687 position: "absolute" as const,
688 bottom: 0,
689 left: 0,
690 right: 0,
691 borderTopLeftRadius: 16,
692 borderTopRightRadius: 16,
693 },
694};
695
696// Export everything as a combined atoms object for convenience
697export const atoms = {
698 colors: rawColors,
699 spacing,
700 borderRadius,
701 radius: radiusAtoms,
702 typography: typographyAtoms,
703 shadows,
704 touchTargets,
705 animations,
706 iconSizes,
707 layout,
708 borders,
709 backgrounds,
710 textColors,
711 spacingAtoms,
712 sizes,
713 flex,
714 opacity,
715 zIndex,
716 overflow,
717 textAlign,
718 fontWeight,
719 fontSize,
720 lineHeight,
721 letterSpacing,
722 textTransform,
723 textDecoration,
724 textAlignVertical,
725 transforms,
726 position,
727 aspectRatio,
728 gap,
729 layouts,
730};
731
732// Convenient shorthand aliases
733export const a = atoms;
734export const bg = backgrounds;
735export const text = textColors;
736export const m = spacingAtoms.margin;
737export const mt = spacingAtoms.marginTop;
738export const mr = spacingAtoms.marginRight;
739export const mb = spacingAtoms.marginBottom;
740export const ml = spacingAtoms.marginLeft;
741export const mx = spacingAtoms.marginHorizontal;
742export const my = spacingAtoms.marginVertical;
743export const p = spacingAtoms.padding;
744export const pt = spacingAtoms.paddingTop;
745export const pr = spacingAtoms.paddingRight;
746export const pb = spacingAtoms.paddingBottom;
747export const pl = spacingAtoms.paddingLeft;
748export const px = spacingAtoms.paddingHorizontal;
749export const py = spacingAtoms.paddingVertical;
750export const w = sizes.width;
751export const h = sizes.height;
752export const r = radiusAtoms.all;
753export const top = position.top;
754export const right = position.right;
755export const bottom = position.bottom;
756export const left = position.left;
757export const rotate = transforms.rotate;
758export const scale = transforms.scale;
759export const translateX = transforms.translateX;
760export const translateY = transforms.translateY;