Bluesky's "Application Layout Framework"

Add initial version of README and API reference

+848 -1
+78 -1
README.md
··· 1 1 # Bluesky's "Application Layout Framework" AKA "ALF" 2 2 3 - No docs for u. 3 + Bluesky's design system and styling framework for React Native. ALF provides utility-first, atomic style objects that work across web, iOS, and Android. 4 + 5 + You use ALF by combining static atoms (frozen style objects) with dynamic theme atoms that adapt to the active color scheme. No runtime CSS generation, no style strings. You compose styles via arrays, the same way you already do in React Native. 6 + 7 + ## Install 8 + 9 + ```bash 10 + yarn add @bsky.app/alf 11 + ``` 12 + 13 + Peer dependencies: `react@19`, `react-native@^0.81.1`. 14 + 15 + ## Quick start 16 + 17 + Wrap your app in the `Provider` and pass it your themes: 18 + 19 + ```tsx 20 + import { Provider, themes } from '@bsky.app/alf' 21 + 22 + function App() { 23 + return ( 24 + <Provider activeTheme="light" themes={themes}> 25 + <Root /> 26 + </Provider> 27 + ) 28 + } 29 + ``` 30 + 31 + Then use `atoms` for static layout and `useTheme()` for color: 32 + 33 + ```tsx 34 + import { atoms as a, useTheme } from '@bsky.app/alf' 35 + 36 + function Card() { 37 + const t = useTheme() 38 + return ( 39 + <View style={[a.flex_row, a.gap_md, a.p_lg, a.rounded_md, t.atoms.bg]}> 40 + <Text style={[a.text_md, a.font_bold, t.atoms.text]}>Hello</Text> 41 + </View> 42 + ) 43 + } 44 + ``` 45 + 46 + ## Exports 47 + 48 + Everything ships from one entry point: `@bsky.app/alf`. 49 + 50 + | Export | Description | 51 + |--------|-------------| 52 + | `atoms` | 300+ frozen style objects for layout, spacing, typography, borders | 53 + | `useTheme()` | Hook returning the active `Theme` with color-adaptive `atoms` | 54 + | `Provider` | React context provider for theme selection | 55 + | `tokens` | Namespaced design tokens (spacing, font sizes, line heights, radii, weights) | 56 + | `utils` | Namespaced utilities: `alpha`, `leading`, `flatten`, `select` | 57 + | `themes` | Pre-built light, dark, and dim theme objects | 58 + | `createTheme()` | Build a custom theme from a palette | 59 + | `createThemes()` | Build light/dark/dim variants from two palettes | 60 + | `Palette` | Type and defaults for color palettes | 61 + | `invertPalette()` | Flip a palette's color scale for dark mode | 62 + | `isWeb`, `isNative`, `isIOS`, `isAndroid` | Platform detection booleans | 63 + | `web()`, `native()`, `ios()`, `android()` | Platform-gated value selectors | 64 + | `platform()` | `Platform.select()` equivalent | 65 + 66 + ## Platform behavior 67 + 68 + ALF uses React Native's file extension convention (`.native.ts`) to resolve platform-specific code at build time. 69 + 70 + Notable differences on native: 71 + 72 + - `fixed` resolves to `position: 'absolute'` (fixed positioning not supported) 73 + - `sticky` resolves to an empty object 74 + - Border widths use `StyleSheet.hairlineWidth` instead of 1px 75 + - Shadows use native shadow props with `elevation`. On Fabric (the new architecture), shadows resolve to empty objects. 76 + - Web-only atoms (`inline`, `block`, `pointer`) resolve to empty objects 77 + 78 + ## API reference 79 + 80 + See [api-reference.md](./api-reference.md) for the complete API.
+770
api-reference.md
··· 1 + # API Reference 2 + 3 + Complete reference for `@bsky.app/alf`. All exports come from a single entry point: 4 + 5 + ```typescript 6 + import { 7 + atoms, 8 + useTheme, 9 + Provider, 10 + Context, 11 + tokens, 12 + utils, 13 + themes, 14 + createTheme, 15 + createThemes, 16 + invertPalette, 17 + isWeb, 18 + isNative, 19 + isIOS, 20 + isAndroid, 21 + isFabric, 22 + web, 23 + native, 24 + ios, 25 + android, 26 + platform, 27 + } from '@bsky.app/alf' 28 + ``` 29 + 30 + --- 31 + 32 + ## Provider and Hook 33 + 34 + ### `Provider` 35 + 36 + React context provider that makes the active theme available to all children via `useTheme()`. 37 + 38 + ```tsx 39 + <Provider activeTheme="light" themes={themes}> 40 + <App /> 41 + </Provider> 42 + ``` 43 + 44 + | Prop | Type | Description | 45 + |------|------|-------------| 46 + | `activeTheme` | `T extends string` | Key into the `themes` object | 47 + | `themes` | `Record<T, Theme>` | Map of theme name to `Theme` object | 48 + | `children` | `ReactNode` | Your app | 49 + 50 + The provider memoizes the context value based on `activeTheme` and `themes`. 51 + 52 + ### `useTheme()` 53 + 54 + Returns the active `Theme` object from context. You access theme-aware color atoms from `t.atoms`: 55 + 56 + ```tsx 57 + const t = useTheme() 58 + <View style={[t.atoms.bg, t.atoms.shadow_md]}> 59 + <Text style={[t.atoms.text]}>Themed text</Text> 60 + </View> 61 + ``` 62 + 63 + Returns: `Theme` 64 + 65 + ### `Context` 66 + 67 + The raw React context (`React.Context<{ theme: Theme }>`). Defaults to `themes.light`. Display name: `'AlfContext'`. 68 + 69 + You rarely need this directly. Use `useTheme()` instead. 70 + 71 + --- 72 + 73 + ## Atoms 74 + 75 + All atoms live on the `atoms` object. They are frozen, static style objects that you combine via arrays. 76 + 77 + ```tsx 78 + import { atoms as a } from '@bsky.app/alf' 79 + 80 + <View style={[a.flex_row, a.gap_md, a.p_lg, a.rounded_md]} /> 81 + ``` 82 + 83 + ### Naming conventions 84 + 85 + - Underscore prefix for sizes starting with a number: `_2xs`, `_2xl`, `_3xl`, etc. 86 + - Axis suffixes: `x` (left + right), `y` (top + bottom) 87 + - Side suffixes: `t` (top), `b` (bottom), `l` (left), `r` (right) 88 + 89 + ### Debug 90 + 91 + | Atom | Style | 92 + |------|-------| 93 + | `debug` | `borderColor: 'red', borderWidth: 1` | 94 + 95 + ### Positioning 96 + 97 + | Atom | Style | 98 + |------|-------| 99 + | `fixed` | `position: 'fixed'` (native: `'absolute'`) | 100 + | `absolute` | `position: 'absolute'` | 101 + | `relative` | `position: 'relative'` | 102 + | `static` | `position: 'static'` | 103 + | `sticky` | `position: 'sticky'` (native: empty) | 104 + | `inset_0` | `top/right/bottom/left: 0` | 105 + | `top_0` | `top: 0` | 106 + | `right_0` | `right: 0` | 107 + | `bottom_0` | `bottom: 0` | 108 + | `left_0` | `left: 0` | 109 + 110 + ### Z-index 111 + 112 + | Atom | Value | 113 + |------|-------| 114 + | `z_10` | `zIndex: 10` | 115 + | `z_20` | `zIndex: 20` | 116 + | `z_30` | `zIndex: 30` | 117 + | `z_40` | `zIndex: 40` | 118 + | `z_50` | `zIndex: 50` | 119 + 120 + ### Overflow 121 + 122 + | Atom | Style | 123 + |------|-------| 124 + | `overflow_visible` | `overflow: 'visible'` | 125 + | `overflow_hidden` | `overflow: 'hidden'` | 126 + | `overflow_auto` | `overflow: 'auto'` (native: empty) | 127 + | `overflow_x_visible` | `overflowX: 'visible'` | 128 + | `overflow_x_hidden` | `overflowX: 'hidden'` | 129 + | `overflow_y_visible` | `overflowY: 'visible'` | 130 + | `overflow_y_hidden` | `overflowY: 'hidden'` | 131 + 132 + ### Width and height 133 + 134 + | Atom | Style | 135 + |------|-------| 136 + | `w_full` | `width: '100%'` | 137 + | `h_full` | `height: '100%'` | 138 + | `h_full_vh` | `height: '100vh'` | 139 + | `max_w_full` | `maxWidth: '100%'` | 140 + | `max_h_full` | `maxHeight: '100%'` | 141 + 142 + ### Border radius 143 + 144 + All values come from `tokens.borderRadius`. 145 + 146 + | Atom | Value (px) | 147 + |------|------------| 148 + | `rounded_0` | 0 | 149 + | `rounded_2xs` | 2 | 150 + | `rounded_xs` | 4 | 151 + | `rounded_sm` | 8 | 152 + | `rounded_md` | 12 | 153 + | `rounded_lg` | 16 | 154 + | `rounded_xl` | 20 | 155 + | `rounded_full` | 999 | 156 + 157 + ### Flexbox 158 + 159 + | Atom | Style | 160 + |------|-------| 161 + | `flex` | `display: 'flex'` | 162 + | `flex_col` | `flexDirection: 'column'` | 163 + | `flex_row` | `flexDirection: 'row'` | 164 + | `flex_col_reverse` | `flexDirection: 'column-reverse'` | 165 + | `flex_row_reverse` | `flexDirection: 'row-reverse'` | 166 + | `flex_wrap` | `flexWrap: 'wrap'` | 167 + | `flex_nowrap` | `flexWrap: 'nowrap'` | 168 + | `flex_0` | `flex: '0 0 auto'` (native: `flex: 0`) | 169 + | `flex_1` | `flex: 1` | 170 + | `flex_grow` | `flexGrow: 1` | 171 + | `flex_grow_0` | `flexGrow: 0` | 172 + | `flex_shrink` | `flexShrink: 1` | 173 + | `flex_shrink_0` | `flexShrink: 0` | 174 + 175 + ### Alignment 176 + 177 + | Atom | Style | 178 + |------|-------| 179 + | `justify_start` | `justifyContent: 'flex-start'` | 180 + | `justify_center` | `justifyContent: 'center'` | 181 + | `justify_between` | `justifyContent: 'space-between'` | 182 + | `justify_end` | `justifyContent: 'flex-end'` | 183 + | `align_start` | `alignItems: 'flex-start'` | 184 + | `align_center` | `alignItems: 'center'` | 185 + | `align_end` | `alignItems: 'flex-end'` | 186 + | `align_baseline` | `alignItems: 'baseline'` | 187 + | `align_stretch` | `alignItems: 'stretch'` | 188 + | `self_auto` | `alignSelf: 'auto'` | 189 + | `self_start` | `alignSelf: 'flex-start'` | 190 + | `self_end` | `alignSelf: 'flex-end'` | 191 + | `self_center` | `alignSelf: 'center'` | 192 + | `self_stretch` | `alignSelf: 'stretch'` | 193 + | `self_baseline` | `alignSelf: 'baseline'` | 194 + 195 + ### Gap 196 + 197 + All values come from `tokens.space`. 198 + 199 + | Atom | Value (px) | 200 + |------|------------| 201 + | `gap_0` | 0 | 202 + | `gap_2xs` | 2 | 203 + | `gap_xs` | 4 | 204 + | `gap_sm` | 8 | 205 + | `gap_md` | 12 | 206 + | `gap_lg` | 16 | 207 + | `gap_xl` | 20 | 208 + | `gap_2xl` | 24 | 209 + | `gap_3xl` | 28 | 210 + | `gap_4xl` | 32 | 211 + | `gap_5xl` | 40 | 212 + 213 + ### Typography 214 + 215 + #### Font size 216 + 217 + Each atom sets both `fontSize` and `letterSpacing: 0`. Values come from `tokens.fontSize`. 218 + 219 + | Atom | Size (px) | 220 + |------|-----------| 221 + | `text_2xs` | 9.4 | 222 + | `text_xs` | 11.3 | 223 + | `text_sm` | 13.1 | 224 + | `text_md` | 15 | 225 + | `text_lg` | 16.9 | 226 + | `text_xl` | 18.8 | 227 + | `text_2xl` | 20.6 | 228 + | `text_3xl` | 24.3 | 229 + | `text_4xl` | 30 | 230 + | `text_5xl` | 37.5 | 231 + 232 + #### Text alignment 233 + 234 + | Atom | Style | 235 + |------|-------| 236 + | `text_left` | `textAlign: 'left'` | 237 + | `text_center` | `textAlign: 'center'` | 238 + | `text_right` | `textAlign: 'right'` | 239 + 240 + #### Line height 241 + 242 + Values are unitless multipliers from `tokens.lineHeight`. Use the `utils.leading()` function when you need computed pixel values on native. 243 + 244 + | Atom | Multiplier | 245 + |------|------------| 246 + | `leading_tight` | 1.15 | 247 + | `leading_snug` | 1.3 | 248 + | `leading_relaxed` | 1.5 | 249 + | `leading_normal` | 1.5 (deprecated, use `leading_relaxed`) | 250 + 251 + #### Letter spacing 252 + 253 + | Atom | Style | 254 + |------|-------| 255 + | `tracking_normal` | `letterSpacing: 0` | 256 + 257 + #### Font weight 258 + 259 + | Atom | Weight | 260 + |------|--------| 261 + | `font_normal` | `'400'` | 262 + | `font_medium` | `'500'` | 263 + | `font_semi_bold` | `'600'` | 264 + | `font_bold` | `'700'` | 265 + 266 + #### Font style 267 + 268 + | Atom | Style | 269 + |------|-------| 270 + | `italic` | `fontStyle: 'italic'` | 271 + 272 + ### Borders 273 + 274 + On native, all 1px border atoms use `StyleSheet.hairlineWidth` instead. See the [platform behavior](#native-overrides) section. 275 + 276 + #### Border width 277 + 278 + | Atom | Sides | Width | 279 + |------|-------|-------| 280 + | `border_0` | All | 0 | 281 + | `border` | All | 1 | 282 + | `border_t_0` / `border_t` | Top | 0 / 1 | 283 + | `border_b_0` / `border_b` | Bottom | 0 / 1 | 284 + | `border_l_0` / `border_l` | Left | 0 / 1 | 285 + | `border_r_0` / `border_r` | Right | 0 / 1 | 286 + | `border_x_0` / `border_x` | Left + Right | 0 / 1 | 287 + | `border_y_0` / `border_y` | Top + Bottom | 0 / 1 | 288 + 289 + #### Border color 290 + 291 + | Atom | Style | 292 + |------|-------| 293 + | `border_transparent` | `borderColor: 'transparent'` | 294 + 295 + For theme-aware border colors, use `t.atoms.border_contrast_low`, `t.atoms.border_contrast_medium`, or `t.atoms.border_contrast_high`. 296 + 297 + ### Border curves (iOS only) 298 + 299 + These resolve to empty objects on web and Android. On iOS, they set `borderCurve`. 300 + 301 + | Atom | iOS Style | 302 + |------|-----------| 303 + | `curve_circular` | `borderCurve: 'circular'` | 304 + | `curve_continuous` | `borderCurve: 'continuous'` | 305 + 306 + ### Shadows 307 + 308 + Static shadow atoms are empty objects on web and Fabric. Use theme shadow atoms (`t.atoms.shadow_sm`, etc.) for cross-platform shadows with proper theme colors. 309 + 310 + | Atom | Description | 311 + |------|-------------| 312 + | `shadow_sm` | Small shadow (native only, disabled on Fabric) | 313 + | `shadow_md` | Medium shadow (native only, disabled on Fabric) | 314 + | `shadow_lg` | Large shadow (native only, disabled on Fabric) | 315 + 316 + ### Gutters 317 + 318 + Semantic padding shortcuts. Each has `_x` (horizontal) and `_y` (vertical) variants. 319 + 320 + | Atom | Padding (px) | 321 + |------|-------------| 322 + | `gutter_tight` / `gutter_x_tight` / `gutter_y_tight` | 8 | 323 + | `gutter_snug` / `gutter_x_snug` / `gutter_y_snug` | 12 | 324 + | `gutter_default` / `gutter_x_default` / `gutter_y_default` | 16 | 325 + | `gutter_wide` / `gutter_x_wide` / `gutter_y_wide` | 20 | 326 + | `gutter_extra_wide` / `gutter_x_extra_wide` / `gutter_y_extra_wide` | 24 | 327 + 328 + ### Padding 329 + 330 + All values come from `tokens.space`. Each size has `p_`, `px_`, `py_`, `pt_`, `pb_`, `pl_`, `pr_` variants. 331 + 332 + | Size | Value (px) | 333 + |------|------------| 334 + | `0` | 0 | 335 + | `2xs` | 2 | 336 + | `xs` | 4 | 337 + | `sm` | 8 | 338 + | `md` | 12 | 339 + | `lg` | 16 | 340 + | `xl` | 20 | 341 + | `2xl` | 24 | 342 + | `3xl` | 28 | 343 + | `4xl` | 32 | 344 + | `5xl` | 40 | 345 + 346 + Full atom names follow the pattern: `p_md`, `px_lg`, `py_sm`, `pt_xl`, `pb_2xl`, `pl_xs`, `pr_3xl`. 347 + 348 + ### Margin 349 + 350 + Same size scale as padding. Each size has `m_`, `mx_`, `my_`, `mt_`, `mb_`, `ml_`, `mr_` variants, plus `_auto` variants for centering. 351 + 352 + Auto margins: `m_auto`, `mx_auto`, `my_auto`, `mt_auto`, `mb_auto`, `ml_auto`, `mr_auto`. 353 + 354 + ### Pointer events and user select 355 + 356 + | Atom | Style | 357 + |------|-------| 358 + | `pointer_events_none` | `pointerEvents: 'none'` | 359 + | `pointer_events_auto` | `pointerEvents: 'auto'` | 360 + | `pointer_events_box_only` | `pointerEvents: 'box-only'` | 361 + | `pointer_events_box_none` | `pointerEvents: 'box-none'` | 362 + | `user_select_none` | `userSelect: 'none'` | 363 + | `user_select_text` | `userSelect: 'text'` | 364 + | `user_select_all` | `userSelect: 'all'` | 365 + | `outline_inset_1` | `outlineOffset: -1` | 366 + 367 + ### Text decoration 368 + 369 + | Atom | Style | 370 + |------|-------| 371 + | `underline` | `textDecorationLine: 'underline'` | 372 + | `strike_through` | `textDecorationLine: 'line-through'` | 373 + 374 + ### Display 375 + 376 + | Atom | Style | Platform | 377 + |------|-------|----------| 378 + | `hidden` | `display: 'none'` | All | 379 + | `contents` | `display: 'contents'` | All | 380 + | `inline` | `display: 'inline'` | Web only (native: empty) | 381 + | `block` | `display: 'block'` | Web only (native: empty) | 382 + 383 + ### Cursor 384 + 385 + | Atom | Style | Platform | 386 + |------|-------|----------| 387 + | `pointer` | `cursor: 'pointer'` | Web only (native: empty) | 388 + 389 + --- 390 + 391 + ## Theme Atoms 392 + 393 + These live on `t.atoms` (where `t = useTheme()`). They adapt to the active theme's color palette. 394 + 395 + Theme atoms only cover the **contrast** color scale — the neutral grays used for text, backgrounds, and borders on nearly every component. Primary, positive, and negative colors are more situational, so they are accessed directly from the palette instead: 396 + 397 + ```tsx 398 + const t = useTheme() 399 + 400 + // Contrast colors → theme atoms 401 + <View style={[t.atoms.bg, t.atoms.border_contrast_medium]}> 402 + <Text style={[t.atoms.text]}>Neutral text</Text> 403 + </View> 404 + 405 + // Primary / positive / negative → palette 406 + <View style={{ backgroundColor: t.palette.primary_500 }}> 407 + <Text style={{ color: t.palette.positive_500 }}>Success</Text> 408 + </View> 409 + ``` 410 + 411 + Palette values still adapt to the active theme automatically via `invertPalette()`, so `t.palette.primary_500` returns the correct shade in light, dark, and dim modes. 412 + 413 + ### Text 414 + 415 + | Atom | Palette source | 416 + |------|---------------| 417 + | `t.atoms.text` | `contrast_1000` | 418 + | `t.atoms.text_contrast_low` | `contrast_400` | 419 + | `t.atoms.text_contrast_medium` | `contrast_700` | 420 + | `t.atoms.text_contrast_high` | `contrast_900` | 421 + | `t.atoms.text_inverted` | `contrast_0` | 422 + 423 + ### Background 424 + 425 + | Atom | Palette source | 426 + |------|---------------| 427 + | `t.atoms.bg` | `contrast_0` | 428 + | `t.atoms.bg_contrast_25` | `contrast_25` | 429 + | `t.atoms.bg_contrast_50` | `contrast_50` | 430 + | `t.atoms.bg_contrast_100` | `contrast_100` | 431 + | `t.atoms.bg_contrast_200` | `contrast_200` | 432 + | `t.atoms.bg_contrast_300` | `contrast_300` | 433 + | `t.atoms.bg_contrast_400` | `contrast_400` | 434 + | `t.atoms.bg_contrast_500` | `contrast_500` | 435 + | `t.atoms.bg_contrast_600` | `contrast_600` | 436 + | `t.atoms.bg_contrast_700` | `contrast_700` | 437 + | `t.atoms.bg_contrast_800` | `contrast_800` | 438 + | `t.atoms.bg_contrast_900` | `contrast_900` | 439 + | `t.atoms.bg_contrast_950` | `contrast_950` | 440 + | `t.atoms.bg_contrast_975` | `contrast_975` | 441 + 442 + ### Border 443 + 444 + | Atom | Palette source | 445 + |------|---------------| 446 + | `t.atoms.border_contrast_low` | `contrast_100` | 447 + | `t.atoms.border_contrast_medium` | `contrast_200` | 448 + | `t.atoms.border_contrast_high` | `contrast_300` | 449 + 450 + ### Shadow 451 + 452 + | Atom | Description | 453 + |------|-------------| 454 + | `t.atoms.shadow_sm` | Small shadow with theme-appropriate opacity | 455 + | `t.atoms.shadow_md` | Medium shadow | 456 + | `t.atoms.shadow_lg` | Large shadow | 457 + 458 + Shadow atoms include native shadow props and a `boxShadow` CSS string for web. Light themes use 0.1 shadow opacity, dark/dim themes use 0.4. 459 + 460 + --- 461 + 462 + ## Tokens 463 + 464 + Imported as a namespace: `import { tokens } from '@bsky.app/alf'`. All values are pixel-based numbers (no rem/em). 465 + 466 + ### `tokens.space` 467 + 468 + | Key | Value | 469 + |-----|-------| 470 + | `_2xs` | 2 | 471 + | `xs` | 4 | 472 + | `sm` | 8 | 473 + | `md` | 12 | 474 + | `lg` | 16 | 475 + | `xl` | 20 | 476 + | `_2xl` | 24 | 477 + | `_3xl` | 28 | 478 + | `_4xl` | 32 | 479 + | `_5xl` | 40 | 480 + 481 + ### `tokens.fontSize` 482 + 483 + | Key | Value | 484 + |-----|-------| 485 + | `_2xs` | 9.4 | 486 + | `xs` | 11.3 | 487 + | `sm` | 13.1 | 488 + | `md` | 15 | 489 + | `lg` | 16.9 | 490 + | `xl` | 18.8 | 491 + | `_2xl` | 20.6 | 492 + | `_3xl` | 24.3 | 493 + | `_4xl` | 30 | 494 + | `_5xl` | 37.5 | 495 + 496 + ### `tokens.lineHeight` 497 + 498 + | Key | Value | 499 + |-----|-------| 500 + | `tight` | 1.15 | 501 + | `snug` | 1.3 | 502 + | `relaxed` | 1.5 | 503 + 504 + ### `tokens.borderRadius` 505 + 506 + | Key | Value | 507 + |-----|-------| 508 + | `_2xs` | 2 | 509 + | `xs` | 4 | 510 + | `sm` | 8 | 511 + | `md` | 12 | 512 + | `lg` | 16 | 513 + | `xl` | 20 | 514 + | `full` | 999 | 515 + 516 + ### `tokens.fontWeight` 517 + 518 + | Key | Value | 519 + |-----|-------| 520 + | `normal` | `'400'` | 521 + | `medium` | `'500'` | 522 + | `semiBold` | `'600'` | 523 + | `bold` | `'700'` | 524 + 525 + ### `tokens.labelerColor` 526 + 527 + | Key | Value | 528 + |-----|-------| 529 + | `purple` | `rgb(105 0 255)` | 530 + | `purple_dark` | `rgb(83 0 202)` | 531 + 532 + ### `tokens.TRACKING` 533 + 534 + Letter-spacing constant. Value: `0`. 535 + 536 + --- 537 + 538 + ## Palette 539 + 540 + ### `Palette` type 541 + 542 + Defines all color values for a theme. Four color families, each with shades: 543 + 544 + - **`contrast_*`** (neutrals): 0, 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, 975, 1000 545 + - **`primary_*`** (brand blue): 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, 975 546 + - **`positive_*`** (green): same scale as primary 547 + - **`negative_*`** (red): same scale as primary 548 + 549 + Plus `white`, `black`, and `like` (pink, `#EC4899`). 550 + 551 + All values are CSS color strings. 552 + 553 + ### `DEFAULT_PALETTE` 554 + 555 + The standard light-mode palette. Contrast ranges from `#FFFFFF` (0) to `#000000` (1000). Primary blues range from `#F5F9FF` (25) to `#001533` (975). 556 + 557 + ### `DEFAULT_SUBDUED_PALETTE` 558 + 559 + A softer alternative used by the dim theme. Same structure, lower contrast. The darkest contrast value is `#151D28` instead of pure black. 560 + 561 + ### `invertPalette(palette)` 562 + 563 + Flips a palette's color scales for dark mode. Swaps contrast_0 with contrast_1000, contrast_25 with contrast_975, and so on. Keeps `white`, `black`, and `like` unchanged. 564 + 565 + ```typescript 566 + const darkPalette = invertPalette(DEFAULT_PALETTE) 567 + ``` 568 + 569 + Returns: `Palette` 570 + 571 + --- 572 + 573 + ## Themes 574 + 575 + ### `Theme` type 576 + 577 + ```typescript 578 + type Theme = { 579 + scheme: ThemeScheme // 'light' | 'dark' 580 + name: ThemeName // 'light' | 'dark' | 'dim' 581 + palette: Palette 582 + atoms: ThemeAtoms 583 + } 584 + ``` 585 + 586 + ### `ThemeScheme` 587 + 588 + `'light' | 'dark'` 589 + 590 + ### `ThemeName` 591 + 592 + `'light' | 'dark' | 'dim'` 593 + 594 + ### `themes` 595 + 596 + Pre-built theme objects, ready to pass to `Provider`: 597 + 598 + ```typescript 599 + themes.light // Light scheme, DEFAULT_PALETTE 600 + themes.dark // Dark scheme, inverted DEFAULT_PALETTE, 0.4 shadow opacity 601 + themes.dim // Dark scheme, inverted DEFAULT_SUBDUED_PALETTE, 0.4 shadow opacity 602 + ``` 603 + 604 + ### `createTheme({ scheme, name, palette, options? })` 605 + 606 + Builds a `Theme` from a palette. 607 + 608 + | Param | Type | Description | 609 + |-------|------|-------------| 610 + | `scheme` | `ThemeScheme` | `'light'` or `'dark'` | 611 + | `name` | `ThemeName` | `'light'`, `'dark'`, or `'dim'` | 612 + | `palette` | `Palette` | Color palette to use | 613 + | `options.shadowOpacity` | `number` | Shadow opacity, defaults to `0.1` | 614 + 615 + Returns: `Theme` 616 + 617 + ### `createThemes({ defaultPalette, subduedPalette })` 618 + 619 + Builds all three theme variants at once. Inverts the palettes automatically for dark and dim. 620 + 621 + | Param | Type | Description | 622 + |-------|------|-------------| 623 + | `defaultPalette` | `Palette` | Used for light and dark themes | 624 + | `subduedPalette` | `Palette` | Used for the dim theme | 625 + 626 + Returns: `{ light: Theme, dark: Theme, dim: Theme }` 627 + 628 + --- 629 + 630 + ## Platform 631 + 632 + ### Detection booleans 633 + 634 + These resolve at build time via platform-split files (`.native.ts` vs `.ts`). 635 + 636 + | Export | Web | iOS | Android | 637 + |--------|-----|-----|---------| 638 + | `isWeb` | `true` | `false` | `false` | 639 + | `isNative` | `false` | `true` | `true` | 640 + | `isIOS` | `false` | `true` | `false` | 641 + | `isAndroid` | `false` | `false` | `true` | 642 + | `isFabric` | Runtime check: `Boolean(global?.nativeFabricUIManager)` | 643 + 644 + ### Platform selectors 645 + 646 + Identity functions that return the value on the matching platform and `undefined` everywhere else. 647 + 648 + ```typescript 649 + web({ cursor: 'pointer' }) // returns the object on web, undefined on native 650 + native({ elevation: 4 }) // returns the object on native, undefined on web 651 + ios({ borderCurve: 'continuous' }) 652 + android({ elevation: 8 }) 653 + ``` 654 + 655 + ### `platform(specifics)` 656 + 657 + Works like React Native's `Platform.select()`. On web, returns `specifics.web` or `specifics.default`. 658 + 659 + ```typescript 660 + platform({ web: 16, default: 12 }) 661 + ``` 662 + 663 + --- 664 + 665 + ## Utils 666 + 667 + Imported as a namespace: `import { utils } from '@bsky.app/alf'`. 668 + 669 + ### `utils.alpha(color, opacity)` 670 + 671 + Converts a color string to a transparent variant at the given opacity (0 to 1). 672 + 673 + ```typescript 674 + utils.alpha('#FF0000', 0.5) // '#FF000080' 675 + utils.alpha('rgb(255, 0, 0)', 0.5) // 'rgba(255, 0, 0, 0.5)' 676 + utils.alpha('hsl(0, 100%, 50%)', 0.5) // 'hsla(0, 100%, 50%, 0.5)' 677 + ``` 678 + 679 + Supported formats: `#RGB`, `#RRGGBB`, `rgb()`, `hsl()`. Returns the original color unchanged if the format is not recognized. 680 + 681 + ### `utils.leading(textStyle)` 682 + 683 + Calculates a `lineHeight` value from a text style's `fontSize` and `lineHeight` multiplier. 684 + 685 + ```typescript 686 + utils.leading({ fontSize: 15, lineHeight: 1.5 }) 687 + // Web: { lineHeight: '1.5' } (unitless string) 688 + // Native: { lineHeight: 23 } (rounded pixel value) 689 + ``` 690 + 691 + Defaults to `tokens.fontSize.sm` and `tokens.lineHeight.snug` when values are missing. 692 + 693 + Returns: `Pick<TextStyle, 'lineHeight'>` 694 + 695 + ### `utils.select(name, options)` 696 + 697 + Theme-aware value selector. Pass a `ThemeName` and an object mapping theme names to values. 698 + 699 + ```typescript 700 + utils.select('dark', { 701 + light: '#FFFFFF', 702 + dark: '#000000', 703 + dim: '#1A1A2E', 704 + }) 705 + // Returns '#000000' 706 + ``` 707 + 708 + You can use a `default` fallback instead of specifying every theme: 709 + 710 + ```typescript 711 + utils.select('dim', { light: 'white', default: 'black' }) 712 + // Returns 'black' 713 + ``` 714 + 715 + ### `utils.flatten(style)` 716 + 717 + Merges a style array (or nested arrays) into a single object. Filters out falsy values. 718 + 719 + ```typescript 720 + utils.flatten([a.flex_row, a.gap_md, false && a.p_lg]) 721 + // { flexDirection: 'row', gap: 12 } 722 + ``` 723 + 724 + On web, this uses a custom implementation. On native, it delegates to `StyleSheet.flatten`. 725 + 726 + Returns: merged style object 727 + 728 + --- 729 + 730 + ## Types 731 + 732 + ### `TextStyleProp` 733 + 734 + ```typescript 735 + type TextStyleProp = { style?: StyleProp<TextStyle> } 736 + ``` 737 + 738 + ### `ViewStyleProp` 739 + 740 + ```typescript 741 + type ViewStyleProp = { style?: StyleProp<ViewStyle> } 742 + ``` 743 + 744 + ### `ShadowStyle` 745 + 746 + ```typescript 747 + type ShadowStyle = Pick< 748 + ViewStyle, 749 + 'shadowColor' | 'shadowOpacity' | 'shadowRadius' | 'elevation' | 'shadowOffset' | 'boxShadow' 750 + > 751 + ``` 752 + 753 + --- 754 + 755 + ## Native Overrides 756 + 757 + When running on iOS or Android, these atoms behave differently from their web counterparts: 758 + 759 + | Atom | Web | Native | 760 + |------|-----|--------| 761 + | `fixed` | `position: 'fixed'` | `position: 'absolute'` | 762 + | `sticky` | `position: 'sticky'` | Empty object | 763 + | `overflow_auto` | `overflow: 'auto'` | Empty object | 764 + | `flex_0` | `flex: '0 0 auto'` | `flex: 0` | 765 + | `border`, `border_t`, `border_b`, `border_l`, `border_r`, `border_x`, `border_y` | `borderWidth: 1` | `borderWidth: StyleSheet.hairlineWidth` | 766 + | `curve_circular` | Empty object | iOS: `borderCurve: 'circular'` | 767 + | `curve_continuous` | Empty object | iOS: `borderCurve: 'continuous'` | 768 + | `shadow_sm`, `shadow_md`, `shadow_lg` | Empty object | Shadow props with `elevation` (Fabric: empty) | 769 + | `inline`, `block` | `display: 'inline'` / `'block'` | Empty object | 770 + | `pointer` | `cursor: 'pointer'` | Empty object |