Bluesky app fork with some witchin' additions 💫

Application Layout Framework (#1732)

* Initial library setup

* Add docblocks

* Some cleanup

* New storybook

* Playing around

* Remove silly test, use for...in

* Memo

* Memo

* Add hooks example

* Tweak colors, bit of cleanup

* Improve macro handling

* Add some more examples

* Rename for better diff

* Cleanup

* Add nested context example

* Add todo

* Less break more perf

* Buttons, you get the idea

* Fix test

* Remove temp colors

* Add a few more common macros

* Docs

* Perf improvements

* Alf go brrrr

* Update breakpoint handling

* I think it'll work

* Better naming, better code

* Fix typo

* Some renaming

* More complete pass at Tailwind naming

* Build out storybook

* Playing around with curves

* Revert "Playing around with curves"

This reverts commit 6b0e0e5c9d842a2d9af31b53affe2f6291c3fa0d.

* Smooth brain

* Remove outdated docs

* Some docs, fix line-height values, export tokens

authored by

Eric Bailey and committed by
GitHub
a5b47489 0ee0554b

+1793 -18
+22 -17
src/App.web.tsx
··· 7 7 8 8 import 'view/icons' 9 9 10 + import {ThemeProvider as Alf} from '#/alf' 10 11 import {init as initPersistedState} from '#/state/persisted' 11 12 import {useColorMode} from 'state/shell' 12 13 import {Shell} from 'view/shell/index' ··· 28 29 } from 'state/session' 29 30 import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' 30 31 import * as persisted from '#/state/persisted' 32 + import {useColorModeTheme} from '#/alf/util/useColorModeTheme' 31 33 32 34 function InnerApp() { 33 35 const {isInitialLoad, currentAccount} = useSession() 34 36 const {resumeSession} = useSessionApi() 35 37 const colorMode = useColorMode() 38 + const theme = useColorModeTheme(colorMode) 36 39 37 40 // init 38 41 useEffect(() => { ··· 44 47 if (isInitialLoad) return null 45 48 46 49 return ( 47 - <React.Fragment 48 - // Resets the entire tree below when it changes: 49 - key={currentAccount?.did}> 50 - <LoggedOutViewProvider> 51 - <UnreadNotifsProvider> 52 - <ThemeProvider theme={colorMode}> 53 - {/* All components should be within this provider */} 54 - <RootSiblingParent> 55 - <SafeAreaProvider> 56 - <Shell /> 57 - </SafeAreaProvider> 58 - </RootSiblingParent> 59 - <ToastContainer /> 60 - </ThemeProvider> 61 - </UnreadNotifsProvider> 62 - </LoggedOutViewProvider> 63 - </React.Fragment> 50 + <Alf theme={theme}> 51 + <React.Fragment 52 + // Resets the entire tree below when it changes: 53 + key={currentAccount?.did}> 54 + <LoggedOutViewProvider> 55 + <UnreadNotifsProvider> 56 + <ThemeProvider theme={colorMode}> 57 + {/* All components should be within this provider */} 58 + <RootSiblingParent> 59 + <SafeAreaProvider> 60 + <Shell /> 61 + </SafeAreaProvider> 62 + </RootSiblingParent> 63 + <ToastContainer /> 64 + </ThemeProvider> 65 + </UnreadNotifsProvider> 66 + </LoggedOutViewProvider> 67 + </React.Fragment> 68 + </Alf> 64 69 ) 65 70 } 66 71
+1 -1
src/Navigation.tsx
··· 61 61 import {PostThreadScreen} from './view/screens/PostThread' 62 62 import {PostLikedByScreen} from './view/screens/PostLikedBy' 63 63 import {PostRepostedByScreen} from './view/screens/PostRepostedBy' 64 - import {DebugScreen} from './view/screens/Debug' 64 + import {DebugScreen} from './view/screens/DebugNew' 65 65 import {LogScreen} from './view/screens/Log' 66 66 import {SupportScreen} from './view/screens/Support' 67 67 import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy'
+56
src/alf/README.md
··· 1 + # Application Layout Framework (ALF) 2 + 3 + A set of UI primitives and components. 4 + 5 + ## Usage 6 + 7 + Naming conventions follow Tailwind — delimited with a `_` instead of `-` to 8 + enable object access — with a couple exceptions: 9 + 10 + **Spacing** 11 + 12 + Uses "t-shirt" sizes `xxs`, `xs`, `sm`, `md`, `lg`, `xl` and `xxl` instead of 13 + increments of 4px. We only use a few common spacings, and otherwise typically 14 + rely on many one-off values. 15 + 16 + **Text Size** 17 + 18 + Uses "t-shirt" sizes `xxs`, `xs`, `sm`, `md`, `lg`, `xl` and `xxl` to match our 19 + type scale. 20 + 21 + **Line Height** 22 + 23 + The text size atoms also apply a line-height with the same value as the size, 24 + for a 1:1 ratio. `tight` and `normal` are retained for use in the few places 25 + where we need leading. 26 + 27 + ### Atoms 28 + 29 + An (mostly-complete) set of style definitions that match Tailwind CSS selectors. 30 + These are static and reused throughout the app. 31 + 32 + ```tsx 33 + import { atoms } from '#/alf' 34 + 35 + <View style={[atoms.flex_row]} /> 36 + ``` 37 + 38 + ### Theme 39 + 40 + Any values that rely on the theme, namely colors. 41 + 42 + ```tsx 43 + const t = useTheme() 44 + 45 + <View style={[atoms.flex_row, t.atoms.bg]} /> 46 + ``` 47 + 48 + ### Breakpoints 49 + 50 + ```tsx 51 + const b = useBreakpoints() 52 + 53 + if (b.gtMobile) { 54 + // render tablet or desktop UI 55 + } 56 + ```
+514
src/alf/atoms.ts
··· 1 + import * as tokens from '#/alf/tokens' 2 + 3 + export const atoms = { 4 + /* 5 + * Positioning 6 + */ 7 + absolute: { 8 + position: 'absolute', 9 + }, 10 + relative: { 11 + position: 'relative', 12 + }, 13 + inset_0: { 14 + top: 0, 15 + left: 0, 16 + right: 0, 17 + bottom: 0, 18 + }, 19 + z_10: { 20 + zIndex: 10, 21 + }, 22 + z_20: { 23 + zIndex: 20, 24 + }, 25 + z_30: { 26 + zIndex: 30, 27 + }, 28 + z_40: { 29 + zIndex: 40, 30 + }, 31 + z_50: { 32 + zIndex: 50, 33 + }, 34 + 35 + /* 36 + * Width 37 + */ 38 + w_full: { 39 + width: '100%', 40 + }, 41 + h_full: { 42 + height: '100%', 43 + }, 44 + 45 + /* 46 + * Border radius 47 + */ 48 + rounded_sm: { 49 + borderRadius: tokens.borderRadius.sm, 50 + }, 51 + rounded_md: { 52 + borderRadius: tokens.borderRadius.md, 53 + }, 54 + rounded_full: { 55 + borderRadius: tokens.borderRadius.full, 56 + }, 57 + 58 + /* 59 + * Flex 60 + */ 61 + gap_xxs: { 62 + gap: tokens.space.xxs, 63 + }, 64 + gap_xs: { 65 + gap: tokens.space.xs, 66 + }, 67 + gap_sm: { 68 + gap: tokens.space.sm, 69 + }, 70 + gap_md: { 71 + gap: tokens.space.md, 72 + }, 73 + gap_lg: { 74 + gap: tokens.space.lg, 75 + }, 76 + gap_xl: { 77 + gap: tokens.space.xl, 78 + }, 79 + gap_xxl: { 80 + gap: tokens.space.xxl, 81 + }, 82 + flex: { 83 + display: 'flex', 84 + }, 85 + flex_row: { 86 + flexDirection: 'row', 87 + }, 88 + flex_wrap: { 89 + flexWrap: 'wrap', 90 + }, 91 + flex_1: { 92 + flex: 1, 93 + }, 94 + flex_grow: { 95 + flexGrow: 1, 96 + }, 97 + flex_shrink: { 98 + flexShrink: 1, 99 + }, 100 + justify_center: { 101 + justifyContent: 'center', 102 + }, 103 + justify_between: { 104 + justifyContent: 'space-between', 105 + }, 106 + justify_end: { 107 + justifyContent: 'flex-end', 108 + }, 109 + align_center: { 110 + alignItems: 'center', 111 + }, 112 + align_start: { 113 + alignItems: 'flex-start', 114 + }, 115 + align_end: { 116 + alignItems: 'flex-end', 117 + }, 118 + 119 + /* 120 + * Text 121 + */ 122 + text_center: { 123 + textAlign: 'center', 124 + }, 125 + text_right: { 126 + textAlign: 'right', 127 + }, 128 + text_xxs: { 129 + fontSize: tokens.fontSize.xxs, 130 + lineHeight: tokens.fontSize.xxs, 131 + }, 132 + text_xs: { 133 + fontSize: tokens.fontSize.xs, 134 + lineHeight: tokens.fontSize.xs, 135 + }, 136 + text_sm: { 137 + fontSize: tokens.fontSize.sm, 138 + lineHeight: tokens.fontSize.sm, 139 + }, 140 + text_md: { 141 + fontSize: tokens.fontSize.md, 142 + lineHeight: tokens.fontSize.md, 143 + }, 144 + text_lg: { 145 + fontSize: tokens.fontSize.lg, 146 + lineHeight: tokens.fontSize.lg, 147 + }, 148 + text_xl: { 149 + fontSize: tokens.fontSize.xl, 150 + lineHeight: tokens.fontSize.xl, 151 + }, 152 + text_xxl: { 153 + fontSize: tokens.fontSize.xxl, 154 + lineHeight: tokens.fontSize.xxl, 155 + }, 156 + leading_tight: { 157 + lineHeight: 1.25, 158 + }, 159 + leading_normal: { 160 + lineHeight: 1.5, 161 + }, 162 + font_normal: { 163 + fontWeight: tokens.fontWeight.normal, 164 + }, 165 + font_semibold: { 166 + fontWeight: tokens.fontWeight.semibold, 167 + }, 168 + font_bold: { 169 + fontWeight: tokens.fontWeight.bold, 170 + }, 171 + 172 + /* 173 + * Border 174 + */ 175 + border: { 176 + borderWidth: 1, 177 + }, 178 + border_t: { 179 + borderTopWidth: 1, 180 + }, 181 + border_b: { 182 + borderBottomWidth: 1, 183 + }, 184 + 185 + /* 186 + * Padding 187 + */ 188 + p_xxs: { 189 + padding: tokens.space.xxs, 190 + }, 191 + p_xs: { 192 + padding: tokens.space.xs, 193 + }, 194 + p_sm: { 195 + padding: tokens.space.sm, 196 + }, 197 + p_md: { 198 + padding: tokens.space.md, 199 + }, 200 + p_lg: { 201 + padding: tokens.space.lg, 202 + }, 203 + p_xl: { 204 + padding: tokens.space.xl, 205 + }, 206 + p_xxl: { 207 + padding: tokens.space.xxl, 208 + }, 209 + px_xxs: { 210 + paddingLeft: tokens.space.xxs, 211 + paddingRight: tokens.space.xxs, 212 + }, 213 + px_xs: { 214 + paddingLeft: tokens.space.xs, 215 + paddingRight: tokens.space.xs, 216 + }, 217 + px_sm: { 218 + paddingLeft: tokens.space.sm, 219 + paddingRight: tokens.space.sm, 220 + }, 221 + px_md: { 222 + paddingLeft: tokens.space.md, 223 + paddingRight: tokens.space.md, 224 + }, 225 + px_lg: { 226 + paddingLeft: tokens.space.lg, 227 + paddingRight: tokens.space.lg, 228 + }, 229 + px_xl: { 230 + paddingLeft: tokens.space.xl, 231 + paddingRight: tokens.space.xl, 232 + }, 233 + px_xxl: { 234 + paddingLeft: tokens.space.xxl, 235 + paddingRight: tokens.space.xxl, 236 + }, 237 + py_xxs: { 238 + paddingTop: tokens.space.xxs, 239 + paddingBottom: tokens.space.xxs, 240 + }, 241 + py_xs: { 242 + paddingTop: tokens.space.xs, 243 + paddingBottom: tokens.space.xs, 244 + }, 245 + py_sm: { 246 + paddingTop: tokens.space.sm, 247 + paddingBottom: tokens.space.sm, 248 + }, 249 + py_md: { 250 + paddingTop: tokens.space.md, 251 + paddingBottom: tokens.space.md, 252 + }, 253 + py_lg: { 254 + paddingTop: tokens.space.lg, 255 + paddingBottom: tokens.space.lg, 256 + }, 257 + py_xl: { 258 + paddingTop: tokens.space.xl, 259 + paddingBottom: tokens.space.xl, 260 + }, 261 + py_xxl: { 262 + paddingTop: tokens.space.xxl, 263 + paddingBottom: tokens.space.xxl, 264 + }, 265 + pt_xxs: { 266 + paddingTop: tokens.space.xxs, 267 + }, 268 + pt_xs: { 269 + paddingTop: tokens.space.xs, 270 + }, 271 + pt_sm: { 272 + paddingTop: tokens.space.sm, 273 + }, 274 + pt_md: { 275 + paddingTop: tokens.space.md, 276 + }, 277 + pt_lg: { 278 + paddingTop: tokens.space.lg, 279 + }, 280 + pt_xl: { 281 + paddingTop: tokens.space.xl, 282 + }, 283 + pt_xxl: { 284 + paddingTop: tokens.space.xxl, 285 + }, 286 + pb_xxs: { 287 + paddingBottom: tokens.space.xxs, 288 + }, 289 + pb_xs: { 290 + paddingBottom: tokens.space.xs, 291 + }, 292 + pb_sm: { 293 + paddingBottom: tokens.space.sm, 294 + }, 295 + pb_md: { 296 + paddingBottom: tokens.space.md, 297 + }, 298 + pb_lg: { 299 + paddingBottom: tokens.space.lg, 300 + }, 301 + pb_xl: { 302 + paddingBottom: tokens.space.xl, 303 + }, 304 + pb_xxl: { 305 + paddingBottom: tokens.space.xxl, 306 + }, 307 + pl_xxs: { 308 + paddingLeft: tokens.space.xxs, 309 + }, 310 + pl_xs: { 311 + paddingLeft: tokens.space.xs, 312 + }, 313 + pl_sm: { 314 + paddingLeft: tokens.space.sm, 315 + }, 316 + pl_md: { 317 + paddingLeft: tokens.space.md, 318 + }, 319 + pl_lg: { 320 + paddingLeft: tokens.space.lg, 321 + }, 322 + pl_xl: { 323 + paddingLeft: tokens.space.xl, 324 + }, 325 + pl_xxl: { 326 + paddingLeft: tokens.space.xxl, 327 + }, 328 + pr_xxs: { 329 + paddingRight: tokens.space.xxs, 330 + }, 331 + pr_xs: { 332 + paddingRight: tokens.space.xs, 333 + }, 334 + pr_sm: { 335 + paddingRight: tokens.space.sm, 336 + }, 337 + pr_md: { 338 + paddingRight: tokens.space.md, 339 + }, 340 + pr_lg: { 341 + paddingRight: tokens.space.lg, 342 + }, 343 + pr_xl: { 344 + paddingRight: tokens.space.xl, 345 + }, 346 + pr_xxl: { 347 + paddingRight: tokens.space.xxl, 348 + }, 349 + 350 + /* 351 + * Margin 352 + */ 353 + m_xxs: { 354 + margin: tokens.space.xxs, 355 + }, 356 + m_xs: { 357 + margin: tokens.space.xs, 358 + }, 359 + m_sm: { 360 + margin: tokens.space.sm, 361 + }, 362 + m_md: { 363 + margin: tokens.space.md, 364 + }, 365 + m_lg: { 366 + margin: tokens.space.lg, 367 + }, 368 + m_xl: { 369 + margin: tokens.space.xl, 370 + }, 371 + m_xxl: { 372 + margin: tokens.space.xxl, 373 + }, 374 + mx_xxs: { 375 + marginLeft: tokens.space.xxs, 376 + marginRight: tokens.space.xxs, 377 + }, 378 + mx_xs: { 379 + marginLeft: tokens.space.xs, 380 + marginRight: tokens.space.xs, 381 + }, 382 + mx_sm: { 383 + marginLeft: tokens.space.sm, 384 + marginRight: tokens.space.sm, 385 + }, 386 + mx_md: { 387 + marginLeft: tokens.space.md, 388 + marginRight: tokens.space.md, 389 + }, 390 + mx_lg: { 391 + marginLeft: tokens.space.lg, 392 + marginRight: tokens.space.lg, 393 + }, 394 + mx_xl: { 395 + marginLeft: tokens.space.xl, 396 + marginRight: tokens.space.xl, 397 + }, 398 + mx_xxl: { 399 + marginLeft: tokens.space.xxl, 400 + marginRight: tokens.space.xxl, 401 + }, 402 + my_xxs: { 403 + marginTop: tokens.space.xxs, 404 + marginBottom: tokens.space.xxs, 405 + }, 406 + my_xs: { 407 + marginTop: tokens.space.xs, 408 + marginBottom: tokens.space.xs, 409 + }, 410 + my_sm: { 411 + marginTop: tokens.space.sm, 412 + marginBottom: tokens.space.sm, 413 + }, 414 + my_md: { 415 + marginTop: tokens.space.md, 416 + marginBottom: tokens.space.md, 417 + }, 418 + my_lg: { 419 + marginTop: tokens.space.lg, 420 + marginBottom: tokens.space.lg, 421 + }, 422 + my_xl: { 423 + marginTop: tokens.space.xl, 424 + marginBottom: tokens.space.xl, 425 + }, 426 + my_xxl: { 427 + marginTop: tokens.space.xxl, 428 + marginBottom: tokens.space.xxl, 429 + }, 430 + mt_xxs: { 431 + marginTop: tokens.space.xxs, 432 + }, 433 + mt_xs: { 434 + marginTop: tokens.space.xs, 435 + }, 436 + mt_sm: { 437 + marginTop: tokens.space.sm, 438 + }, 439 + mt_md: { 440 + marginTop: tokens.space.md, 441 + }, 442 + mt_lg: { 443 + marginTop: tokens.space.lg, 444 + }, 445 + mt_xl: { 446 + marginTop: tokens.space.xl, 447 + }, 448 + mt_xxl: { 449 + marginTop: tokens.space.xxl, 450 + }, 451 + mb_xxs: { 452 + marginBottom: tokens.space.xxs, 453 + }, 454 + mb_xs: { 455 + marginBottom: tokens.space.xs, 456 + }, 457 + mb_sm: { 458 + marginBottom: tokens.space.sm, 459 + }, 460 + mb_md: { 461 + marginBottom: tokens.space.md, 462 + }, 463 + mb_lg: { 464 + marginBottom: tokens.space.lg, 465 + }, 466 + mb_xl: { 467 + marginBottom: tokens.space.xl, 468 + }, 469 + mb_xxl: { 470 + marginBottom: tokens.space.xxl, 471 + }, 472 + ml_xxs: { 473 + marginLeft: tokens.space.xxs, 474 + }, 475 + ml_xs: { 476 + marginLeft: tokens.space.xs, 477 + }, 478 + ml_sm: { 479 + marginLeft: tokens.space.sm, 480 + }, 481 + ml_md: { 482 + marginLeft: tokens.space.md, 483 + }, 484 + ml_lg: { 485 + marginLeft: tokens.space.lg, 486 + }, 487 + ml_xl: { 488 + marginLeft: tokens.space.xl, 489 + }, 490 + ml_xxl: { 491 + marginLeft: tokens.space.xxl, 492 + }, 493 + mr_xxs: { 494 + marginRight: tokens.space.xxs, 495 + }, 496 + mr_xs: { 497 + marginRight: tokens.space.xs, 498 + }, 499 + mr_sm: { 500 + marginRight: tokens.space.sm, 501 + }, 502 + mr_md: { 503 + marginRight: tokens.space.md, 504 + }, 505 + mr_lg: { 506 + marginRight: tokens.space.lg, 507 + }, 508 + mr_xl: { 509 + marginRight: tokens.space.xl, 510 + }, 511 + mr_xxl: { 512 + marginRight: tokens.space.xxl, 513 + }, 514 + } as const
+92
src/alf/index.tsx
··· 1 + import React from 'react' 2 + import {Dimensions} from 'react-native' 3 + import * as themes from '#/alf/themes' 4 + 5 + export * as tokens from '#/alf/tokens' 6 + export {atoms} from '#/alf/atoms' 7 + export * from '#/alf/util/platform' 8 + 9 + type BreakpointName = keyof typeof breakpoints 10 + 11 + /* 12 + * Breakpoints 13 + */ 14 + const breakpoints: { 15 + [key: string]: number 16 + } = { 17 + gtMobile: 800, 18 + gtTablet: 1200, 19 + } 20 + function getActiveBreakpoints({width}: {width: number}) { 21 + const active: (keyof typeof breakpoints)[] = Object.keys(breakpoints).filter( 22 + breakpoint => width >= breakpoints[breakpoint], 23 + ) 24 + 25 + return { 26 + active: active[active.length - 1], 27 + gtMobile: active.includes('gtMobile'), 28 + gtTablet: active.includes('gtTablet'), 29 + } 30 + } 31 + 32 + /* 33 + * Context 34 + */ 35 + export const Context = React.createContext<{ 36 + themeName: themes.ThemeName 37 + theme: themes.Theme 38 + breakpoints: { 39 + active: BreakpointName | undefined 40 + gtMobile: boolean 41 + gtTablet: boolean 42 + } 43 + }>({ 44 + themeName: 'light', 45 + theme: themes.light, 46 + breakpoints: { 47 + active: undefined, 48 + gtMobile: false, 49 + gtTablet: false, 50 + }, 51 + }) 52 + 53 + export function ThemeProvider({ 54 + children, 55 + theme: themeName, 56 + }: React.PropsWithChildren<{theme: themes.ThemeName}>) { 57 + const theme = themes[themeName] 58 + const [breakpoints, setBreakpoints] = React.useState(() => 59 + getActiveBreakpoints({width: Dimensions.get('window').width}), 60 + ) 61 + 62 + React.useEffect(() => { 63 + const listener = Dimensions.addEventListener('change', ({window}) => { 64 + const bp = getActiveBreakpoints({width: window.width}) 65 + if (bp.active !== breakpoints.active) setBreakpoints(bp) 66 + }) 67 + 68 + return listener.remove 69 + }, [breakpoints, setBreakpoints]) 70 + 71 + return ( 72 + <Context.Provider 73 + value={React.useMemo( 74 + () => ({ 75 + themeName: themeName, 76 + theme: theme, 77 + breakpoints, 78 + }), 79 + [theme, themeName, breakpoints], 80 + )}> 81 + {children} 82 + </Context.Provider> 83 + ) 84 + } 85 + 86 + export function useTheme() { 87 + return React.useContext(Context).theme 88 + } 89 + 90 + export function useBreakpoints() { 91 + return React.useContext(Context).breakpoints 92 + }
+108
src/alf/themes.ts
··· 1 + import * as tokens from '#/alf/tokens' 2 + import type {Mutable} from '#/alf/types' 3 + 4 + export type ThemeName = 'light' | 'dark' 5 + export type ReadonlyTheme = typeof light 6 + export type Theme = Mutable<ReadonlyTheme> 7 + 8 + export type Palette = { 9 + primary: string 10 + positive: string 11 + negative: string 12 + } 13 + 14 + export const lightPalette: Palette = { 15 + primary: tokens.color.blue_500, 16 + positive: tokens.color.green_500, 17 + negative: tokens.color.red_500, 18 + } as const 19 + 20 + export const darkPalette: Palette = { 21 + primary: tokens.color.blue_500, 22 + positive: tokens.color.green_400, 23 + negative: tokens.color.red_400, 24 + } as const 25 + 26 + export const light = { 27 + palette: lightPalette, 28 + atoms: { 29 + text: { 30 + color: tokens.color.gray_1000, 31 + }, 32 + text_contrast_700: { 33 + color: tokens.color.gray_700, 34 + }, 35 + text_contrast_500: { 36 + color: tokens.color.gray_500, 37 + }, 38 + text_inverted: { 39 + color: tokens.color.white, 40 + }, 41 + bg: { 42 + backgroundColor: tokens.color.white, 43 + }, 44 + bg_contrast_100: { 45 + backgroundColor: tokens.color.gray_100, 46 + }, 47 + bg_contrast_200: { 48 + backgroundColor: tokens.color.gray_200, 49 + }, 50 + bg_contrast_300: { 51 + backgroundColor: tokens.color.gray_300, 52 + }, 53 + bg_positive: { 54 + backgroundColor: tokens.color.green_500, 55 + }, 56 + bg_negative: { 57 + backgroundColor: tokens.color.red_400, 58 + }, 59 + border: { 60 + borderColor: tokens.color.gray_200, 61 + }, 62 + border_contrast_500: { 63 + borderColor: tokens.color.gray_500, 64 + }, 65 + }, 66 + } 67 + 68 + export const dark: Theme = { 69 + palette: darkPalette, 70 + atoms: { 71 + text: { 72 + color: tokens.color.white, 73 + }, 74 + text_contrast_700: { 75 + color: tokens.color.gray_300, 76 + }, 77 + text_contrast_500: { 78 + color: tokens.color.gray_500, 79 + }, 80 + text_inverted: { 81 + color: tokens.color.gray_1000, 82 + }, 83 + bg: { 84 + backgroundColor: tokens.color.gray_1000, 85 + }, 86 + bg_contrast_100: { 87 + backgroundColor: tokens.color.gray_900, 88 + }, 89 + bg_contrast_200: { 90 + backgroundColor: tokens.color.gray_800, 91 + }, 92 + bg_contrast_300: { 93 + backgroundColor: tokens.color.gray_700, 94 + }, 95 + bg_positive: { 96 + backgroundColor: tokens.color.green_400, 97 + }, 98 + bg_negative: { 99 + backgroundColor: tokens.color.red_400, 100 + }, 101 + border: { 102 + borderColor: tokens.color.gray_800, 103 + }, 104 + border_contrast_500: { 105 + borderColor: tokens.color.gray_500, 106 + }, 107 + }, 108 + }
+100
src/alf/tokens.ts
··· 1 + const BLUE_HUE = 211 2 + const GRAYSCALE_SATURATION = 22 3 + 4 + export const color = { 5 + white: '#FFFFFF', 6 + 7 + gray_0: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 100%)`, 8 + gray_100: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 95%)`, 9 + gray_200: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 85%)`, 10 + gray_300: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 75%)`, 11 + gray_400: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 65%)`, 12 + gray_500: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 55%)`, 13 + gray_600: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 45%)`, 14 + gray_700: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 35%)`, 15 + gray_800: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 25%)`, 16 + gray_900: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 15%)`, 17 + gray_1000: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 5%)`, 18 + 19 + blue_0: `hsl(${BLUE_HUE}, 99%, 100%)`, 20 + blue_100: `hsl(${BLUE_HUE}, 99%, 93%)`, 21 + blue_200: `hsl(${BLUE_HUE}, 99%, 83%)`, 22 + blue_300: `hsl(${BLUE_HUE}, 99%, 73%)`, 23 + blue_400: `hsl(${BLUE_HUE}, 99%, 63%)`, 24 + blue_500: `hsl(${BLUE_HUE}, 99%, 53%)`, 25 + blue_600: `hsl(${BLUE_HUE}, 99%, 43%)`, 26 + blue_700: `hsl(${BLUE_HUE}, 99%, 33%)`, 27 + blue_800: `hsl(${BLUE_HUE}, 99%, 23%)`, 28 + blue_900: `hsl(${BLUE_HUE}, 99%, 13%)`, 29 + blue_1000: `hsl(${BLUE_HUE}, 99%, 8%)`, 30 + 31 + green_0: `hsl(130, 60%, 100%)`, 32 + green_100: `hsl(130, 60%, 95%)`, 33 + green_200: `hsl(130, 60%, 85%)`, 34 + green_300: `hsl(130, 60%, 75%)`, 35 + green_400: `hsl(130, 60%, 65%)`, 36 + green_500: `hsl(130, 60%, 55%)`, 37 + green_600: `hsl(130, 60%, 45%)`, 38 + green_700: `hsl(130, 60%, 35%)`, 39 + green_800: `hsl(130, 60%, 25%)`, 40 + green_900: `hsl(130, 60%, 15%)`, 41 + green_1000: `hsl(130, 60%, 5%)`, 42 + 43 + red_0: `hsl(349, 96%, 100%)`, 44 + red_100: `hsl(349, 96%, 95%)`, 45 + red_200: `hsl(349, 96%, 85%)`, 46 + red_300: `hsl(349, 96%, 75%)`, 47 + red_400: `hsl(349, 96%, 65%)`, 48 + red_500: `hsl(349, 96%, 55%)`, 49 + red_600: `hsl(349, 96%, 45%)`, 50 + red_700: `hsl(349, 96%, 35%)`, 51 + red_800: `hsl(349, 96%, 25%)`, 52 + red_900: `hsl(349, 96%, 15%)`, 53 + red_1000: `hsl(349, 96%, 5%)`, 54 + } as const 55 + 56 + export const space = { 57 + xxs: 2, 58 + xs: 4, 59 + sm: 8, 60 + md: 12, 61 + lg: 18, 62 + xl: 24, 63 + xxl: 32, 64 + } as const 65 + 66 + export const fontSize = { 67 + xxs: 10, 68 + xs: 12, 69 + sm: 14, 70 + md: 16, 71 + lg: 18, 72 + xl: 22, 73 + xxl: 26, 74 + } as const 75 + 76 + // TODO test 77 + export const lineHeight = { 78 + none: 1, 79 + normal: 1.5, 80 + relaxed: 1.625, 81 + } as const 82 + 83 + export const borderRadius = { 84 + sm: 8, 85 + md: 12, 86 + full: 999, 87 + } as const 88 + 89 + export const fontWeight = { 90 + normal: '400', 91 + semibold: '600', 92 + bold: '900', 93 + } as const 94 + 95 + export type Color = keyof typeof color 96 + export type Space = keyof typeof space 97 + export type FontSize = keyof typeof fontSize 98 + export type LineHeight = keyof typeof lineHeight 99 + export type BorderRadius = keyof typeof borderRadius 100 + export type FontWeight = keyof typeof fontWeight
+16
src/alf/types.ts
··· 1 + type LiteralToCommon<T extends PropertyKey> = T extends number 2 + ? number 3 + : T extends string 4 + ? string 5 + : T extends symbol 6 + ? symbol 7 + : never 8 + 9 + /** 10 + * @see https://stackoverflow.com/questions/68249999/use-as-const-in-typescript-without-adding-readonly-modifiers 11 + */ 12 + export type Mutable<T> = { 13 + -readonly [K in keyof T]: T[K] extends PropertyKey 14 + ? LiteralToCommon<T[K]> 15 + : Mutable<T[K]> 16 + }
+25
src/alf/util/platform.ts
··· 1 + import {Platform} from 'react-native' 2 + 3 + export function web(value: any) { 4 + return Platform.select({ 5 + web: value, 6 + }) 7 + } 8 + 9 + export function ios(value: any) { 10 + return Platform.select({ 11 + ios: value, 12 + }) 13 + } 14 + 15 + export function android(value: any) { 16 + return Platform.select({ 17 + android: value, 18 + }) 19 + } 20 + 21 + export function native(value: any) { 22 + return Platform.select({ 23 + native: value, 24 + }) 25 + }
+10
src/alf/util/useColorModeTheme.ts
··· 1 + import {useColorScheme} from 'react-native' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + export function useColorModeTheme( 6 + theme: persisted.Schema['colorMode'], 7 + ): 'light' | 'dark' { 8 + const colorScheme = useColorScheme() 9 + return (theme === 'system' ? colorScheme : theme) || 'light' 10 + }
+204
src/view/com/Button.tsx
··· 1 + import React from 'react' 2 + import {Pressable, Text, PressableProps, TextProps} from 'react-native' 3 + import * as tokens from '#/alf/tokens' 4 + import {atoms} from '#/alf' 5 + 6 + export type ButtonType = 7 + | 'primary' 8 + | 'secondary' 9 + | 'tertiary' 10 + | 'positive' 11 + | 'negative' 12 + export type ButtonSize = 'small' | 'large' 13 + 14 + export type VariantProps = { 15 + type?: ButtonType 16 + size?: ButtonSize 17 + } 18 + type ButtonState = { 19 + pressed: boolean 20 + hovered: boolean 21 + focused: boolean 22 + } 23 + export type ButtonProps = Omit<PressableProps, 'children'> & 24 + VariantProps & { 25 + children: 26 + | ((props: { 27 + state: ButtonState 28 + type?: ButtonType 29 + size?: ButtonSize 30 + }) => React.ReactNode) 31 + | React.ReactNode 32 + | string 33 + } 34 + export type ButtonTextProps = TextProps & VariantProps 35 + 36 + export function Button({children, style, type, size, ...rest}: ButtonProps) { 37 + const {baseStyles, hoverStyles} = React.useMemo(() => { 38 + const baseStyles = [] 39 + const hoverStyles = [] 40 + 41 + switch (type) { 42 + case 'primary': 43 + baseStyles.push({ 44 + backgroundColor: tokens.color.blue_500, 45 + }) 46 + break 47 + case 'secondary': 48 + baseStyles.push({ 49 + backgroundColor: tokens.color.gray_200, 50 + }) 51 + hoverStyles.push({ 52 + backgroundColor: tokens.color.gray_100, 53 + }) 54 + break 55 + default: 56 + } 57 + 58 + switch (size) { 59 + case 'large': 60 + baseStyles.push( 61 + atoms.py_md, 62 + atoms.px_xl, 63 + atoms.rounded_md, 64 + atoms.gap_sm, 65 + ) 66 + break 67 + case 'small': 68 + baseStyles.push( 69 + atoms.py_sm, 70 + atoms.px_md, 71 + atoms.rounded_sm, 72 + atoms.gap_xs, 73 + ) 74 + break 75 + default: 76 + } 77 + 78 + return { 79 + baseStyles, 80 + hoverStyles, 81 + } 82 + }, [type, size]) 83 + 84 + const [state, setState] = React.useState({ 85 + pressed: false, 86 + hovered: false, 87 + focused: false, 88 + }) 89 + 90 + const onPressIn = React.useCallback(() => { 91 + setState(s => ({ 92 + ...s, 93 + pressed: true, 94 + })) 95 + }, [setState]) 96 + const onPressOut = React.useCallback(() => { 97 + setState(s => ({ 98 + ...s, 99 + pressed: false, 100 + })) 101 + }, [setState]) 102 + const onHoverIn = React.useCallback(() => { 103 + setState(s => ({ 104 + ...s, 105 + hovered: true, 106 + })) 107 + }, [setState]) 108 + const onHoverOut = React.useCallback(() => { 109 + setState(s => ({ 110 + ...s, 111 + hovered: false, 112 + })) 113 + }, [setState]) 114 + const onFocus = React.useCallback(() => { 115 + setState(s => ({ 116 + ...s, 117 + focused: true, 118 + })) 119 + }, [setState]) 120 + const onBlur = React.useCallback(() => { 121 + setState(s => ({ 122 + ...s, 123 + focused: false, 124 + })) 125 + }, [setState]) 126 + 127 + return ( 128 + <Pressable 129 + {...rest} 130 + style={state => [ 131 + atoms.flex_row, 132 + atoms.align_center, 133 + ...baseStyles, 134 + ...(state.hovered ? hoverStyles : []), 135 + typeof style === 'function' ? style(state) : style, 136 + ]} 137 + onPressIn={onPressIn} 138 + onPressOut={onPressOut} 139 + onHoverIn={onHoverIn} 140 + onHoverOut={onHoverOut} 141 + onFocus={onFocus} 142 + onBlur={onBlur}> 143 + {typeof children === 'string' ? ( 144 + <ButtonText type={type} size={size}> 145 + {children} 146 + </ButtonText> 147 + ) : typeof children === 'function' ? ( 148 + children({state, type, size}) 149 + ) : ( 150 + children 151 + )} 152 + </Pressable> 153 + ) 154 + } 155 + 156 + export function ButtonText({ 157 + children, 158 + style, 159 + type, 160 + size, 161 + ...rest 162 + }: ButtonTextProps) { 163 + const textStyles = React.useMemo(() => { 164 + const base = [] 165 + 166 + switch (type) { 167 + case 'primary': 168 + base.push({color: tokens.color.white}) 169 + break 170 + case 'secondary': 171 + base.push({ 172 + color: tokens.color.gray_700, 173 + }) 174 + break 175 + default: 176 + } 177 + 178 + switch (size) { 179 + case 'small': 180 + base.push(atoms.text_sm, {paddingBottom: 1}) 181 + break 182 + case 'large': 183 + base.push(atoms.text_md, {paddingBottom: 1}) 184 + break 185 + default: 186 + } 187 + 188 + return base 189 + }, [type, size]) 190 + 191 + return ( 192 + <Text 193 + {...rest} 194 + style={[ 195 + atoms.flex_1, 196 + atoms.font_semibold, 197 + atoms.text_center, 198 + ...textStyles, 199 + style, 200 + ]}> 201 + {children} 202 + </Text> 203 + ) 204 + }
+104
src/view/com/Typography.tsx
··· 1 + import React from 'react' 2 + import {Text as RNText, TextProps} from 'react-native' 3 + import {useTheme, atoms, web} from '#/alf' 4 + 5 + export function Text({style, ...rest}: TextProps) { 6 + const t = useTheme() 7 + return <RNText style={[atoms.text_sm, t.atoms.text, style]} {...rest} /> 8 + } 9 + 10 + export function H1({style, ...rest}: TextProps) { 11 + const t = useTheme() 12 + const attr = 13 + web({ 14 + role: 'heading', 15 + 'aria-level': 1, 16 + }) || {} 17 + return ( 18 + <RNText 19 + {...attr} 20 + {...rest} 21 + style={[atoms.text_xl, atoms.font_bold, t.atoms.text, style]} 22 + /> 23 + ) 24 + } 25 + 26 + export function H2({style, ...rest}: TextProps) { 27 + const t = useTheme() 28 + const attr = 29 + web({ 30 + role: 'heading', 31 + 'aria-level': 2, 32 + }) || {} 33 + return ( 34 + <RNText 35 + {...attr} 36 + {...rest} 37 + style={[atoms.text_lg, atoms.font_bold, t.atoms.text, style]} 38 + /> 39 + ) 40 + } 41 + 42 + export function H3({style, ...rest}: TextProps) { 43 + const t = useTheme() 44 + const attr = 45 + web({ 46 + role: 'heading', 47 + 'aria-level': 3, 48 + }) || {} 49 + return ( 50 + <RNText 51 + {...attr} 52 + {...rest} 53 + style={[atoms.text_md, atoms.font_bold, t.atoms.text, style]} 54 + /> 55 + ) 56 + } 57 + 58 + export function H4({style, ...rest}: TextProps) { 59 + const t = useTheme() 60 + const attr = 61 + web({ 62 + role: 'heading', 63 + 'aria-level': 4, 64 + }) || {} 65 + return ( 66 + <RNText 67 + {...attr} 68 + {...rest} 69 + style={[atoms.text_sm, atoms.font_bold, t.atoms.text, style]} 70 + /> 71 + ) 72 + } 73 + 74 + export function H5({style, ...rest}: TextProps) { 75 + const t = useTheme() 76 + const attr = 77 + web({ 78 + role: 'heading', 79 + 'aria-level': 5, 80 + }) || {} 81 + return ( 82 + <RNText 83 + {...attr} 84 + {...rest} 85 + style={[atoms.text_xs, atoms.font_bold, t.atoms.text, style]} 86 + /> 87 + ) 88 + } 89 + 90 + export function H6({style, ...rest}: TextProps) { 91 + const t = useTheme() 92 + const attr = 93 + web({ 94 + role: 'heading', 95 + 'aria-level': 6, 96 + }) || {} 97 + return ( 98 + <RNText 99 + {...attr} 100 + {...rest} 101 + style={[atoms.text_xxs, atoms.font_bold, t.atoms.text, style]} 102 + /> 103 + ) 104 + }
+541
src/view/screens/DebugNew.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {CenteredView, ScrollView} from '#/view/com/util/Views' 4 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 + 6 + import {useSetColorMode} from '#/state/shell' 7 + import * as tokens from '#/alf/tokens' 8 + import {atoms as a, useTheme, useBreakpoints, ThemeProvider as Alf} from '#/alf' 9 + import {Button, ButtonText} from '#/view/com/Button' 10 + import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography' 11 + 12 + function ThemeSelector() { 13 + const setColorMode = useSetColorMode() 14 + 15 + return ( 16 + <View style={[a.flex_row, a.gap_md]}> 17 + <Button 18 + type="secondary" 19 + size="small" 20 + onPress={() => setColorMode('system')}> 21 + System 22 + </Button> 23 + <Button 24 + type="secondary" 25 + size="small" 26 + onPress={() => setColorMode('light')}> 27 + Light 28 + </Button> 29 + <Button 30 + type="secondary" 31 + size="small" 32 + onPress={() => setColorMode('dark')}> 33 + Dark 34 + </Button> 35 + </View> 36 + ) 37 + } 38 + 39 + function BreakpointDebugger() { 40 + const t = useTheme() 41 + const breakpoints = useBreakpoints() 42 + 43 + return ( 44 + <View> 45 + <H3 style={[a.pb_md]}>Breakpoint Debugger</H3> 46 + <Text style={[a.pb_md]}> 47 + Current breakpoint: {!breakpoints.gtMobile && <Text>mobile</Text>} 48 + {breakpoints.gtMobile && !breakpoints.gtTablet && <Text>tablet</Text>} 49 + {breakpoints.gtTablet && <Text>desktop</Text>} 50 + </Text> 51 + <Text 52 + style={[a.p_md, t.atoms.bg_contrast_100, {fontFamily: 'monospace'}]}> 53 + {JSON.stringify(breakpoints, null, 2)} 54 + </Text> 55 + </View> 56 + ) 57 + } 58 + 59 + function ThemedSection() { 60 + const t = useTheme() 61 + 62 + return ( 63 + <View style={[t.atoms.bg, a.gap_md, a.p_xl]}> 64 + <H3 style={[a.font_bold]}>theme.atoms.text</H3> 65 + <View style={[a.flex_1, t.atoms.border, a.border_t]} /> 66 + <H3 style={[a.font_bold, t.atoms.text_contrast_700]}> 67 + theme.atoms.text_contrast_700 68 + </H3> 69 + <View style={[a.flex_1, t.atoms.border, a.border_t]} /> 70 + <H3 style={[a.font_bold, t.atoms.text_contrast_500]}> 71 + theme.atoms.text_contrast_500 72 + </H3> 73 + <View style={[a.flex_1, t.atoms.border_contrast_500, a.border_t]} /> 74 + 75 + <View style={[a.flex_row, a.gap_md]}> 76 + <View 77 + style={[ 78 + a.flex_1, 79 + t.atoms.bg, 80 + a.align_center, 81 + a.justify_center, 82 + {height: 60}, 83 + ]}> 84 + <Text>theme.bg</Text> 85 + </View> 86 + <View 87 + style={[ 88 + a.flex_1, 89 + t.atoms.bg_contrast_100, 90 + a.align_center, 91 + a.justify_center, 92 + {height: 60}, 93 + ]}> 94 + <Text>theme.bg_contrast_100</Text> 95 + </View> 96 + </View> 97 + <View style={[a.flex_row, a.gap_md]}> 98 + <View 99 + style={[ 100 + a.flex_1, 101 + t.atoms.bg_contrast_200, 102 + a.align_center, 103 + a.justify_center, 104 + {height: 60}, 105 + ]}> 106 + <Text>theme.bg_contrast_200</Text> 107 + </View> 108 + <View 109 + style={[ 110 + a.flex_1, 111 + t.atoms.bg_contrast_300, 112 + a.align_center, 113 + a.justify_center, 114 + {height: 60}, 115 + ]}> 116 + <Text>theme.bg_contrast_300</Text> 117 + </View> 118 + </View> 119 + <View style={[a.flex_row, a.gap_md]}> 120 + <View 121 + style={[ 122 + a.flex_1, 123 + t.atoms.bg_positive, 124 + a.align_center, 125 + a.justify_center, 126 + {height: 60}, 127 + ]}> 128 + <Text>theme.bg_positive</Text> 129 + </View> 130 + <View 131 + style={[ 132 + a.flex_1, 133 + t.atoms.bg_negative, 134 + a.align_center, 135 + a.justify_center, 136 + {height: 60}, 137 + ]}> 138 + <Text>theme.bg_negative</Text> 139 + </View> 140 + </View> 141 + </View> 142 + ) 143 + } 144 + 145 + export function DebugScreen() { 146 + const t = useTheme() 147 + 148 + return ( 149 + <ScrollView> 150 + <CenteredView style={[t.atoms.bg]}> 151 + <View style={[a.p_xl, a.gap_xxl, {paddingBottom: 200}]}> 152 + <ThemeSelector /> 153 + 154 + <Alf theme="light"> 155 + <ThemedSection /> 156 + </Alf> 157 + <Alf theme="dark"> 158 + <ThemedSection /> 159 + </Alf> 160 + 161 + <H1>Heading 1</H1> 162 + <H2>Heading 2</H2> 163 + <H3>Heading 3</H3> 164 + <H4>Heading 4</H4> 165 + <H5>Heading 5</H5> 166 + <H6>Heading 6</H6> 167 + 168 + <Text style={[a.text_xxl]}>atoms.text_xxl</Text> 169 + <Text style={[a.text_xl]}>atoms.text_xl</Text> 170 + <Text style={[a.text_lg]}>atoms.text_lg</Text> 171 + <Text style={[a.text_md]}>atoms.text_md</Text> 172 + <Text style={[a.text_sm]}>atoms.text_sm</Text> 173 + <Text style={[a.text_xs]}>atoms.text_xs</Text> 174 + <Text style={[a.text_xxs]}>atoms.text_xxs</Text> 175 + 176 + <View style={[a.gap_md, a.align_start]}> 177 + <Button> 178 + {({state}) => ( 179 + <View style={[a.p_md, a.rounded_full, t.atoms.bg_contrast_300]}> 180 + <Text>Unstyled button, state: {JSON.stringify(state)}</Text> 181 + </View> 182 + )} 183 + </Button> 184 + 185 + <Button type="primary" size="small"> 186 + Button 187 + </Button> 188 + <Button type="secondary" size="small"> 189 + Button 190 + </Button> 191 + 192 + <Button type="primary" size="large"> 193 + Button 194 + </Button> 195 + <Button type="secondary" size="large"> 196 + Button 197 + </Button> 198 + 199 + <Button type="secondary" size="small"> 200 + {({type, size}) => ( 201 + <> 202 + <FontAwesomeIcon icon={['fas', 'plus']} size={12} /> 203 + <ButtonText type={type} size={size}> 204 + With an icon 205 + </ButtonText> 206 + </> 207 + )} 208 + </Button> 209 + <Button type="primary" size="large"> 210 + {({state: _state, ...rest}) => ( 211 + <> 212 + <FontAwesomeIcon icon={['fas', 'plus']} /> 213 + <ButtonText {...rest}>With an icon</ButtonText> 214 + </> 215 + )} 216 + </Button> 217 + </View> 218 + 219 + <View style={[a.gap_md]}> 220 + <View style={[a.flex_row, a.gap_md]}> 221 + <View 222 + style={[ 223 + a.flex_1, 224 + {height: 60, backgroundColor: tokens.color.gray_0}, 225 + ]} 226 + /> 227 + <View 228 + style={[ 229 + a.flex_1, 230 + {height: 60, backgroundColor: tokens.color.gray_100}, 231 + ]} 232 + /> 233 + <View 234 + style={[ 235 + a.flex_1, 236 + {height: 60, backgroundColor: tokens.color.gray_200}, 237 + ]} 238 + /> 239 + <View 240 + style={[ 241 + a.flex_1, 242 + {height: 60, backgroundColor: tokens.color.gray_300}, 243 + ]} 244 + /> 245 + <View 246 + style={[ 247 + a.flex_1, 248 + {height: 60, backgroundColor: tokens.color.gray_400}, 249 + ]} 250 + /> 251 + <View 252 + style={[ 253 + a.flex_1, 254 + {height: 60, backgroundColor: tokens.color.gray_500}, 255 + ]} 256 + /> 257 + <View 258 + style={[ 259 + a.flex_1, 260 + {height: 60, backgroundColor: tokens.color.gray_600}, 261 + ]} 262 + /> 263 + <View 264 + style={[ 265 + a.flex_1, 266 + {height: 60, backgroundColor: tokens.color.gray_700}, 267 + ]} 268 + /> 269 + <View 270 + style={[ 271 + a.flex_1, 272 + {height: 60, backgroundColor: tokens.color.gray_800}, 273 + ]} 274 + /> 275 + <View 276 + style={[ 277 + a.flex_1, 278 + {height: 60, backgroundColor: tokens.color.gray_900}, 279 + ]} 280 + /> 281 + <View 282 + style={[ 283 + a.flex_1, 284 + {height: 60, backgroundColor: tokens.color.gray_1000}, 285 + ]} 286 + /> 287 + </View> 288 + 289 + <View style={[a.flex_row, a.gap_md]}> 290 + <View 291 + style={[ 292 + a.flex_1, 293 + {height: 60, backgroundColor: tokens.color.blue_0}, 294 + ]} 295 + /> 296 + <View 297 + style={[ 298 + a.flex_1, 299 + {height: 60, backgroundColor: tokens.color.blue_100}, 300 + ]} 301 + /> 302 + <View 303 + style={[ 304 + a.flex_1, 305 + {height: 60, backgroundColor: tokens.color.blue_200}, 306 + ]} 307 + /> 308 + <View 309 + style={[ 310 + a.flex_1, 311 + {height: 60, backgroundColor: tokens.color.blue_300}, 312 + ]} 313 + /> 314 + <View 315 + style={[ 316 + a.flex_1, 317 + {height: 60, backgroundColor: tokens.color.blue_400}, 318 + ]} 319 + /> 320 + <View 321 + style={[ 322 + a.flex_1, 323 + {height: 60, backgroundColor: tokens.color.blue_500}, 324 + ]} 325 + /> 326 + <View 327 + style={[ 328 + a.flex_1, 329 + {height: 60, backgroundColor: tokens.color.blue_600}, 330 + ]} 331 + /> 332 + <View 333 + style={[ 334 + a.flex_1, 335 + {height: 60, backgroundColor: tokens.color.blue_700}, 336 + ]} 337 + /> 338 + <View 339 + style={[ 340 + a.flex_1, 341 + {height: 60, backgroundColor: tokens.color.blue_800}, 342 + ]} 343 + /> 344 + <View 345 + style={[ 346 + a.flex_1, 347 + {height: 60, backgroundColor: tokens.color.blue_900}, 348 + ]} 349 + /> 350 + <View 351 + style={[ 352 + a.flex_1, 353 + {height: 60, backgroundColor: tokens.color.blue_1000}, 354 + ]} 355 + /> 356 + </View> 357 + <View style={[a.flex_row, a.gap_md]}> 358 + <View 359 + style={[ 360 + a.flex_1, 361 + {height: 60, backgroundColor: tokens.color.green_0}, 362 + ]} 363 + /> 364 + <View 365 + style={[ 366 + a.flex_1, 367 + {height: 60, backgroundColor: tokens.color.green_100}, 368 + ]} 369 + /> 370 + <View 371 + style={[ 372 + a.flex_1, 373 + {height: 60, backgroundColor: tokens.color.green_200}, 374 + ]} 375 + /> 376 + <View 377 + style={[ 378 + a.flex_1, 379 + {height: 60, backgroundColor: tokens.color.green_300}, 380 + ]} 381 + /> 382 + <View 383 + style={[ 384 + a.flex_1, 385 + {height: 60, backgroundColor: tokens.color.green_400}, 386 + ]} 387 + /> 388 + <View 389 + style={[ 390 + a.flex_1, 391 + {height: 60, backgroundColor: tokens.color.green_500}, 392 + ]} 393 + /> 394 + <View 395 + style={[ 396 + a.flex_1, 397 + {height: 60, backgroundColor: tokens.color.green_600}, 398 + ]} 399 + /> 400 + <View 401 + style={[ 402 + a.flex_1, 403 + {height: 60, backgroundColor: tokens.color.green_700}, 404 + ]} 405 + /> 406 + <View 407 + style={[ 408 + a.flex_1, 409 + {height: 60, backgroundColor: tokens.color.green_800}, 410 + ]} 411 + /> 412 + <View 413 + style={[ 414 + a.flex_1, 415 + {height: 60, backgroundColor: tokens.color.green_900}, 416 + ]} 417 + /> 418 + <View 419 + style={[ 420 + a.flex_1, 421 + {height: 60, backgroundColor: tokens.color.green_1000}, 422 + ]} 423 + /> 424 + </View> 425 + <View style={[a.flex_row, a.gap_md]}> 426 + <View 427 + style={[ 428 + a.flex_1, 429 + {height: 60, backgroundColor: tokens.color.red_0}, 430 + ]} 431 + /> 432 + <View 433 + style={[ 434 + a.flex_1, 435 + {height: 60, backgroundColor: tokens.color.red_100}, 436 + ]} 437 + /> 438 + <View 439 + style={[ 440 + a.flex_1, 441 + {height: 60, backgroundColor: tokens.color.red_200}, 442 + ]} 443 + /> 444 + <View 445 + style={[ 446 + a.flex_1, 447 + {height: 60, backgroundColor: tokens.color.red_300}, 448 + ]} 449 + /> 450 + <View 451 + style={[ 452 + a.flex_1, 453 + {height: 60, backgroundColor: tokens.color.red_400}, 454 + ]} 455 + /> 456 + <View 457 + style={[ 458 + a.flex_1, 459 + {height: 60, backgroundColor: tokens.color.red_500}, 460 + ]} 461 + /> 462 + <View 463 + style={[ 464 + a.flex_1, 465 + {height: 60, backgroundColor: tokens.color.red_600}, 466 + ]} 467 + /> 468 + <View 469 + style={[ 470 + a.flex_1, 471 + {height: 60, backgroundColor: tokens.color.red_700}, 472 + ]} 473 + /> 474 + <View 475 + style={[ 476 + a.flex_1, 477 + {height: 60, backgroundColor: tokens.color.red_800}, 478 + ]} 479 + /> 480 + <View 481 + style={[ 482 + a.flex_1, 483 + {height: 60, backgroundColor: tokens.color.red_900}, 484 + ]} 485 + /> 486 + <View 487 + style={[ 488 + a.flex_1, 489 + {height: 60, backgroundColor: tokens.color.red_1000}, 490 + ]} 491 + /> 492 + </View> 493 + </View> 494 + 495 + <View> 496 + <H3 style={[a.pb_md, a.font_bold]}>Spacing</H3> 497 + 498 + <View style={[a.gap_md]}> 499 + <View style={[a.flex_row, a.align_center]}> 500 + <Text style={{width: 80}}>xxs (2px)</Text> 501 + <View style={[a.flex_1, a.pt_xxs, t.atoms.bg_contrast_300]} /> 502 + </View> 503 + 504 + <View style={[a.flex_row, a.align_center]}> 505 + <Text style={{width: 80}}>xs (4px)</Text> 506 + <View style={[a.flex_1, a.pt_xs, t.atoms.bg_contrast_300]} /> 507 + </View> 508 + 509 + <View style={[a.flex_row, a.align_center]}> 510 + <Text style={{width: 80}}>sm (8px)</Text> 511 + <View style={[a.flex_1, a.pt_sm, t.atoms.bg_contrast_300]} /> 512 + </View> 513 + 514 + <View style={[a.flex_row, a.align_center]}> 515 + <Text style={{width: 80}}>md (12px)</Text> 516 + <View style={[a.flex_1, a.pt_md, t.atoms.bg_contrast_300]} /> 517 + </View> 518 + 519 + <View style={[a.flex_row, a.align_center]}> 520 + <Text style={{width: 80}}>lg (18px)</Text> 521 + <View style={[a.flex_1, a.pt_lg, t.atoms.bg_contrast_300]} /> 522 + </View> 523 + 524 + <View style={[a.flex_row, a.align_center]}> 525 + <Text style={{width: 80}}>xl (24px)</Text> 526 + <View style={[a.flex_1, a.pt_xl, t.atoms.bg_contrast_300]} /> 527 + </View> 528 + 529 + <View style={[a.flex_row, a.align_center]}> 530 + <Text style={{width: 80}}>xxl (32px)</Text> 531 + <View style={[a.flex_1, a.pt_xxl, t.atoms.bg_contrast_300]} /> 532 + </View> 533 + </View> 534 + </View> 535 + 536 + <BreakpointDebugger /> 537 + </View> 538 + </CenteredView> 539 + </ScrollView> 540 + ) 541 + }