Bluesky's "Application Layout Framework"

Add README and API reference #2

open opened by danielweinmann.com targeting main from danielweinmann.com/alf: docs

Created a complete API reference in a Markdown file and a basic README.

Please let me know if you prefer a different format, squashed commits, or have any other guidance.

Labels

None yet.

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

History

1 round 0 comments
sign up or login to add to the discussion
4 commits
expand
Add initial version of README and API reference
Fix imprecisions on API reference
More improvements to the API reference
More improvements to README and API reference
no conflicts, ready to merge
expand 0 comments