Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

New component library based on ALF (#2459)

* Install on native as well

* Add button and link components

* Comments

* Use new prop

* Add some form elements

* Add labels to input

* Fix line height, add suffix

* Date inputs

* Autofill styles

* Clean up InputDate types

* Improve types for InputText, value handling

* Enforce a11y props on buttons

* Add Dialog, Portal

* Dialog contents

* Native dialog

* Clean up

* Fix animations

* Improvements to web modal, exiting still broken

* Clean up dialog types

* Add Prompt, Dialog refinement, mobile refinement

* Integrate new design tokens, reorg storybook

* Button colors

* Dim mode

* Reorg

* Some styles

* Toggles

* Improve a11y

* Autosize dialog, handle max height, Dialog.ScrolLView not working

* Try to use BottomSheet's own APIs

* Scrollable dialogs

* Add web shadow

* Handle overscroll

* Styles

* Dialog text input

* Shadows

* Button focus states

* Button pressed states

* Gradient poc

* Gradient colors and hovers

* Add hrefAttrs to Link

* Some more a11y

* Toggle invalid states

* Update dialog descriptions for demo

* Icons

* WIP Toggle cleanup

* Refactor toggle to not rely on immediate children

* Make Toggle controlled

* Clean up Toggles storybook

* ToggleButton styles

* Improve a11y labels

* ToggleButton hover darkmode

* Some i18n

* Refactor input

* Allow extension of input

* Remove old input

* Improve icons, add CalendarDays

* Refactor DateField, web done

* Add label example

* Clean up old InputDate, DateField android, text area example

* Consistent imports

* Button context, icons

* Add todo

* Add closeAllDialogs control

* Alignment

* Expand color palette

* Hitslops, add shortcut to Storybook in dev

* Fix multiline on ios

* Mark dialog close button as unused

authored by

Eric Bailey and committed by
GitHub
66b8774e 9cbd3c09

+4680 -965
+1
assets/icons/arrowTopRight_stoke2_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8 6a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v9a1 1 0 1 1-2 0V8.414l-9.793 9.793a1 1 0 0 1-1.414-1.414L15.586 7H9a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>
+1
assets/icons/calendarDays_stroke2_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Zm1 16V9h14v10H5ZM5 7h14V5H5v2Zm3 10.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5ZM17.25 12a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM12 13.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5ZM9.25 12a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM12 17.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z" clip-rule="evenodd"/></svg>
+1
assets/icons/colorPalette_stroke2_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 12c0-4.09 3.527-7.5 8-7.5s8 3.41 8 7.5c0 1.579-.419 2.056-.708 2.236-.388.241-1.031.286-2.058.153-.33-.043-.652-.096-.991-.152a65.905 65.905 0 0 0-.531-.087c-.52-.081-1.077-.156-1.61-.164-1.065-.016-2.336.245-2.996 1.567-.418.834-.295 1.67-.078 2.314.18.534.47 1.055.683 1.437v.001l.097.175.01.018C7.432 19.407 4 16.033 4 12Zm8-9.5C6.532 2.5 2 6.7 2 12s4.532 9.5 10 9.5c.401 0 .812-.04 1.166-.193.41-.176.761-.517.866-1.028.085-.416-.03-.796-.118-1.029a5.981 5.981 0 0 0-.351-.73l-.12-.215c-.215-.392-.403-.73-.52-1.078-.13-.387-.111-.614-.029-.78.146-.291.404-.473 1.178-.461.385.005.825.06 1.329.14.15.023.308.05.47.077.36.059.742.122 1.105.17 1.021.132 2.325.213 3.373-.439C21.496 15.22 22 13.874 22 12c0-5.3-4.532-9.5-10-9.5Zm3.5 8.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM9 12.25a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm1.5-2.75a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" clip-rule="evenodd"/></svg>
+1
assets/icons/globe_stroke2_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4.062 11h2.961c.103-2.204.545-4.218 1.235-5.77.06-.136.123-.269.188-.399A8.007 8.007 0 0 0 4.062 11ZM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 2c-.227 0-.518.1-.868.432-.354.337-.719.872-1.047 1.61-.561 1.263-.958 2.991-1.06 4.958h5.95c-.102-1.967-.499-3.695-1.06-4.958-.328-.738-.693-1.273-1.047-1.61C12.518 4.099 12.227 4 12 4Zm4.977 7c-.103-2.204-.545-4.218-1.235-5.77a9.78 9.78 0 0 0-.188-.399A8.006 8.006 0 0 1 19.938 11h-2.961Zm-2.003 2H9.026c.101 1.966.498 3.695 1.06 4.958.327.738.692 1.273 1.046 1.61.35.333.641.432.868.432.227 0 .518-.1.868-.432.354-.337.719-.872 1.047-1.61.561-1.263.958-2.991 1.06-4.958Zm.58 6.169c.065-.13.128-.263.188-.399.69-1.552 1.132-3.566 1.235-5.77h2.961a8.006 8.006 0 0 1-4.384 6.169Zm-7.108 0a9.877 9.877 0 0 1-.188-.399c-.69-1.552-1.132-3.566-1.235-5.77H4.062a8.006 8.006 0 0 0 4.384 6.169Z" clip-rule="evenodd"/></svg>
+19
bskyweb/templates/base.html
··· 39 39 height: calc(100% + env(safe-area-inset-top)); 40 40 } 41 41 42 + /* Remove autofill styles on Webkit */ 43 + input:-webkit-autofill, 44 + input:-webkit-autofill:hover, 45 + input:-webkit-autofill:focus, 46 + textarea:-webkit-autofill, 47 + textarea:-webkit-autofill:hover, 48 + textarea:-webkit-autofill:focus, 49 + select:-webkit-autofill, 50 + select:-webkit-autofill:hover, 51 + select:-webkit-autofill:focus { 52 + border: 0; 53 + -webkit-text-fill-color: transparent; 54 + -webkit-box-shadow: none; 55 + } 56 + /* Force left-align date/time inputs on iOS mobile */ 57 + input::-webkit-date-and-time-value { 58 + text-align: left; 59 + } 60 + 42 61 /* Color theming */ 43 62 :root { 44 63 --text: black;
+1
package.json
··· 70 70 "@segment/analytics-react-native": "^2.10.1", 71 71 "@segment/sovran-react-native": "^0.4.5", 72 72 "@sentry/react-native": "5.5.0", 73 + "@tamagui/focus-scope": "^1.84.1", 73 74 "@tanstack/react-query": "^5.8.1", 74 75 "@tiptap/core": "^2.0.0-beta.220", 75 76 "@tiptap/extension-document": "^2.0.0-beta.220",
+35 -24
src/App.native.tsx
··· 13 13 14 14 import 'view/icons' 15 15 16 + import {ThemeProvider as Alf} from '#/alf' 17 + import {useColorModeTheme} from '#/alf/util/useColorModeTheme' 16 18 import {init as initPersistedState} from '#/state/persisted' 17 19 import {listenSessionDropped} from './state/events' 18 20 import {useColorMode} from 'state/shell' ··· 25 27 import {TestCtrls} from 'view/com/testing/TestCtrls' 26 28 import {Provider as ShellStateProvider} from 'state/shell' 27 29 import {Provider as ModalStateProvider} from 'state/modals' 30 + import {Provider as DialogStateProvider} from 'state/dialogs' 28 31 import {Provider as LightboxStateProvider} from 'state/lightbox' 29 32 import {Provider as MutedThreadsProvider} from 'state/muted-threads' 30 33 import {Provider as InvitesStateProvider} from 'state/invites' ··· 39 42 import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' 40 43 import * as persisted from '#/state/persisted' 41 44 import {Splash} from '#/Splash' 45 + import {Provider as PortalProvider} from '#/components/Portal' 42 46 import {msg} from '@lingui/macro' 43 47 import {useLingui} from '@lingui/react' 44 48 ··· 48 52 const colorMode = useColorMode() 49 53 const {isInitialLoad, currentAccount} = useSession() 50 54 const {resumeSession} = useSessionApi() 55 + const theme = useColorModeTheme(colorMode) 51 56 const {_} = useLingui() 52 57 53 58 // init ··· 63 68 64 69 return ( 65 70 <SafeAreaProvider initialMetrics={initialWindowMetrics}> 66 - <Splash isReady={!isInitialLoad}> 67 - <React.Fragment 68 - // Resets the entire tree below when it changes: 69 - key={currentAccount?.did}> 70 - <LoggedOutViewProvider> 71 - <UnreadNotifsProvider> 72 - <ThemeProvider theme={colorMode}> 73 - {/* All components should be within this provider */} 74 - <RootSiblingParent> 75 - <GestureHandlerRootView style={s.h100pct}> 76 - <TestCtrls /> 77 - <Shell /> 78 - </GestureHandlerRootView> 79 - </RootSiblingParent> 80 - </ThemeProvider> 81 - </UnreadNotifsProvider> 82 - </LoggedOutViewProvider> 83 - </React.Fragment> 84 - </Splash> 71 + <Alf theme={theme}> 72 + <Splash isReady={!isInitialLoad}> 73 + <React.Fragment 74 + // Resets the entire tree below when it changes: 75 + key={currentAccount?.did}> 76 + <LoggedOutViewProvider> 77 + <UnreadNotifsProvider> 78 + <ThemeProvider theme={colorMode}> 79 + {/* All components should be within this provider */} 80 + <RootSiblingParent> 81 + <GestureHandlerRootView style={s.h100pct}> 82 + <TestCtrls /> 83 + <Shell /> 84 + </GestureHandlerRootView> 85 + </RootSiblingParent> 86 + </ThemeProvider> 87 + </UnreadNotifsProvider> 88 + </LoggedOutViewProvider> 89 + </React.Fragment> 90 + </Splash> 91 + </Alf> 85 92 </SafeAreaProvider> 86 93 ) 87 94 } ··· 109 116 <MutedThreadsProvider> 110 117 <InvitesStateProvider> 111 118 <ModalStateProvider> 112 - <LightboxStateProvider> 113 - <I18nProvider> 114 - <InnerApp /> 115 - </I18nProvider> 116 - </LightboxStateProvider> 119 + <DialogStateProvider> 120 + <LightboxStateProvider> 121 + <I18nProvider> 122 + <PortalProvider> 123 + <InnerApp /> 124 + </PortalProvider> 125 + </I18nProvider> 126 + </LightboxStateProvider> 127 + </DialogStateProvider> 117 128 </ModalStateProvider> 118 129 </InvitesStateProvider> 119 130 </MutedThreadsProvider>
+12 -6
src/App.web.tsx
··· 8 8 import 'view/icons' 9 9 10 10 import {ThemeProvider as Alf} from '#/alf' 11 + import {useColorModeTheme} from '#/alf/util/useColorModeTheme' 11 12 import {init as initPersistedState} from '#/state/persisted' 12 13 import {useColorMode} from 'state/shell' 13 14 import {Shell} from 'view/shell/index' ··· 16 17 import {queryClient} from 'lib/react-query' 17 18 import {Provider as ShellStateProvider} from 'state/shell' 18 19 import {Provider as ModalStateProvider} from 'state/modals' 20 + import {Provider as DialogStateProvider} from 'state/dialogs' 19 21 import {Provider as LightboxStateProvider} from 'state/lightbox' 20 22 import {Provider as MutedThreadsProvider} from 'state/muted-threads' 21 23 import {Provider as InvitesStateProvider} from 'state/invites' ··· 29 31 } from 'state/session' 30 32 import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' 31 33 import * as persisted from '#/state/persisted' 32 - import {useColorModeTheme} from '#/alf/util/useColorModeTheme' 34 + import {Provider as PortalProvider} from '#/components/Portal' 33 35 34 36 function InnerApp() { 35 37 const {isInitialLoad, currentAccount} = useSession() ··· 92 94 <MutedThreadsProvider> 93 95 <InvitesStateProvider> 94 96 <ModalStateProvider> 95 - <LightboxStateProvider> 96 - <I18nProvider> 97 - <InnerApp /> 98 - </I18nProvider> 99 - </LightboxStateProvider> 97 + <DialogStateProvider> 98 + <LightboxStateProvider> 99 + <I18nProvider> 100 + <PortalProvider> 101 + <InnerApp /> 102 + </PortalProvider> 103 + </I18nProvider> 104 + </LightboxStateProvider> 105 + </DialogStateProvider> 100 106 </ModalStateProvider> 101 107 </InvitesStateProvider> 102 108 </MutedThreadsProvider>
+3 -3
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/DebugNew' 64 + import {Storybook} from './view/screens/Storybook' 65 65 import {LogScreen} from './view/screens/Log' 66 66 import {SupportScreen} from './view/screens/Support' 67 67 import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy' ··· 200 200 /> 201 201 <Stack.Screen 202 202 name="Debug" 203 - getComponent={() => DebugScreen} 204 - options={{title: title(msg`Debug`), requireAuth: true}} 203 + getComponent={() => Storybook} 204 + options={{title: title(msg`Storybook`), requireAuth: true}} 205 205 /> 206 206 <Stack.Screen 207 207 name="Log"
+266 -78
src/alf/atoms.ts
··· 4 4 /* 5 5 * Positioning 6 6 */ 7 + fixed: { 8 + position: 'fixed', 9 + }, 7 10 absolute: { 8 11 position: 'absolute', 9 12 }, ··· 32 35 zIndex: 50, 33 36 }, 34 37 38 + overflow_hidden: { 39 + overflow: 'hidden', 40 + }, 41 + 35 42 /* 36 43 * Width 37 44 */ ··· 45 52 /* 46 53 * Border radius 47 54 */ 55 + rounded_2xs: { 56 + borderRadius: tokens.borderRadius._2xs, 57 + }, 58 + rounded_xs: { 59 + borderRadius: tokens.borderRadius.xs, 60 + }, 48 61 rounded_sm: { 49 62 borderRadius: tokens.borderRadius.sm, 50 63 }, ··· 58 71 /* 59 72 * Flex 60 73 */ 61 - gap_xxs: { 62 - gap: tokens.space.xxs, 74 + gap_2xs: { 75 + gap: tokens.space._2xs, 63 76 }, 64 77 gap_xs: { 65 78 gap: tokens.space.xs, ··· 76 89 gap_xl: { 77 90 gap: tokens.space.xl, 78 91 }, 79 - gap_xxl: { 80 - gap: tokens.space.xxl, 92 + gap_2xl: { 93 + gap: tokens.space._2xl, 94 + }, 95 + gap_3xl: { 96 + gap: tokens.space._3xl, 97 + }, 98 + gap_4xl: { 99 + gap: tokens.space._4xl, 100 + }, 101 + gap_5xl: { 102 + gap: tokens.space._5xl, 81 103 }, 82 104 flex: { 83 105 display: 'flex', ··· 125 147 text_right: { 126 148 textAlign: 'right', 127 149 }, 128 - text_xxs: { 129 - fontSize: tokens.fontSize.xxs, 130 - lineHeight: tokens.fontSize.xxs, 150 + text_2xs: { 151 + fontSize: tokens.fontSize._2xs, 152 + lineHeight: tokens.fontSize._2xs, 131 153 }, 132 154 text_xs: { 133 155 fontSize: tokens.fontSize.xs, ··· 149 171 fontSize: tokens.fontSize.xl, 150 172 lineHeight: tokens.fontSize.xl, 151 173 }, 152 - text_xxl: { 153 - fontSize: tokens.fontSize.xxl, 154 - lineHeight: tokens.fontSize.xxl, 174 + text_2xl: { 175 + fontSize: tokens.fontSize._2xl, 176 + lineHeight: tokens.fontSize._2xl, 177 + }, 178 + text_3xl: { 179 + fontSize: tokens.fontSize._3xl, 180 + lineHeight: tokens.fontSize._3xl, 181 + }, 182 + text_4xl: { 183 + fontSize: tokens.fontSize._4xl, 184 + lineHeight: tokens.fontSize._4xl, 185 + }, 186 + text_5xl: { 187 + fontSize: tokens.fontSize._5xl, 188 + lineHeight: tokens.fontSize._5xl, 155 189 }, 156 190 leading_tight: { 157 191 lineHeight: 1.25, ··· 162 196 font_normal: { 163 197 fontWeight: tokens.fontWeight.normal, 164 198 }, 165 - font_semibold: { 199 + font_bold: { 166 200 fontWeight: tokens.fontWeight.semibold, 167 - }, 168 - font_bold: { 169 - fontWeight: tokens.fontWeight.bold, 170 201 }, 171 202 172 203 /* ··· 183 214 }, 184 215 185 216 /* 217 + * Shadow 218 + */ 219 + shadow_sm: { 220 + shadowRadius: 8, 221 + shadowOpacity: 0.1, 222 + elevation: 8, 223 + }, 224 + shadow_md: { 225 + shadowRadius: 16, 226 + shadowOpacity: 0.1, 227 + elevation: 16, 228 + }, 229 + shadow_lg: { 230 + shadowRadius: 32, 231 + shadowOpacity: 0.1, 232 + elevation: 24, 233 + }, 234 + 235 + /* 186 236 * Padding 187 237 */ 188 - p_xxs: { 189 - padding: tokens.space.xxs, 238 + p_2xs: { 239 + padding: tokens.space._2xs, 190 240 }, 191 241 p_xs: { 192 242 padding: tokens.space.xs, ··· 203 253 p_xl: { 204 254 padding: tokens.space.xl, 205 255 }, 206 - p_xxl: { 207 - padding: tokens.space.xxl, 256 + p_2xl: { 257 + padding: tokens.space._2xl, 208 258 }, 209 - px_xxs: { 210 - paddingLeft: tokens.space.xxs, 211 - paddingRight: tokens.space.xxs, 259 + p_3xl: { 260 + padding: tokens.space._3xl, 261 + }, 262 + p_4xl: { 263 + padding: tokens.space._4xl, 264 + }, 265 + p_5xl: { 266 + padding: tokens.space._5xl, 267 + }, 268 + px_2xs: { 269 + paddingLeft: tokens.space._2xs, 270 + paddingRight: tokens.space._2xs, 212 271 }, 213 272 px_xs: { 214 273 paddingLeft: tokens.space.xs, ··· 230 289 paddingLeft: tokens.space.xl, 231 290 paddingRight: tokens.space.xl, 232 291 }, 233 - px_xxl: { 234 - paddingLeft: tokens.space.xxl, 235 - paddingRight: tokens.space.xxl, 292 + px_2xl: { 293 + paddingLeft: tokens.space._2xl, 294 + paddingRight: tokens.space._2xl, 295 + }, 296 + px_3xl: { 297 + paddingLeft: tokens.space._3xl, 298 + paddingRight: tokens.space._3xl, 299 + }, 300 + px_4xl: { 301 + paddingLeft: tokens.space._4xl, 302 + paddingRight: tokens.space._4xl, 303 + }, 304 + px_5xl: { 305 + paddingLeft: tokens.space._5xl, 306 + paddingRight: tokens.space._5xl, 236 307 }, 237 - py_xxs: { 238 - paddingTop: tokens.space.xxs, 239 - paddingBottom: tokens.space.xxs, 308 + py_2xs: { 309 + paddingTop: tokens.space._2xs, 310 + paddingBottom: tokens.space._2xs, 240 311 }, 241 312 py_xs: { 242 313 paddingTop: tokens.space.xs, ··· 258 329 paddingTop: tokens.space.xl, 259 330 paddingBottom: tokens.space.xl, 260 331 }, 261 - py_xxl: { 262 - paddingTop: tokens.space.xxl, 263 - paddingBottom: tokens.space.xxl, 332 + py_2xl: { 333 + paddingTop: tokens.space._2xl, 334 + paddingBottom: tokens.space._2xl, 335 + }, 336 + py_3xl: { 337 + paddingTop: tokens.space._3xl, 338 + paddingBottom: tokens.space._3xl, 264 339 }, 265 - pt_xxs: { 266 - paddingTop: tokens.space.xxs, 340 + py_4xl: { 341 + paddingTop: tokens.space._4xl, 342 + paddingBottom: tokens.space._4xl, 343 + }, 344 + py_5xl: { 345 + paddingTop: tokens.space._5xl, 346 + paddingBottom: tokens.space._5xl, 347 + }, 348 + pt_2xs: { 349 + paddingTop: tokens.space._2xs, 267 350 }, 268 351 pt_xs: { 269 352 paddingTop: tokens.space.xs, ··· 280 363 pt_xl: { 281 364 paddingTop: tokens.space.xl, 282 365 }, 283 - pt_xxl: { 284 - paddingTop: tokens.space.xxl, 366 + pt_2xl: { 367 + paddingTop: tokens.space._2xl, 368 + }, 369 + pt_3xl: { 370 + paddingTop: tokens.space._3xl, 371 + }, 372 + pt_4xl: { 373 + paddingTop: tokens.space._4xl, 374 + }, 375 + pt_5xl: { 376 + paddingTop: tokens.space._5xl, 285 377 }, 286 - pb_xxs: { 287 - paddingBottom: tokens.space.xxs, 378 + pb_2xs: { 379 + paddingBottom: tokens.space._2xs, 288 380 }, 289 381 pb_xs: { 290 382 paddingBottom: tokens.space.xs, ··· 301 393 pb_xl: { 302 394 paddingBottom: tokens.space.xl, 303 395 }, 304 - pb_xxl: { 305 - paddingBottom: tokens.space.xxl, 396 + pb_2xl: { 397 + paddingBottom: tokens.space._2xl, 306 398 }, 307 - pl_xxs: { 308 - paddingLeft: tokens.space.xxs, 399 + pb_3xl: { 400 + paddingBottom: tokens.space._3xl, 401 + }, 402 + pb_4xl: { 403 + paddingBottom: tokens.space._4xl, 404 + }, 405 + pb_5xl: { 406 + paddingBottom: tokens.space._5xl, 407 + }, 408 + pl_2xs: { 409 + paddingLeft: tokens.space._2xs, 309 410 }, 310 411 pl_xs: { 311 412 paddingLeft: tokens.space.xs, ··· 322 423 pl_xl: { 323 424 paddingLeft: tokens.space.xl, 324 425 }, 325 - pl_xxl: { 326 - paddingLeft: tokens.space.xxl, 426 + pl_2xl: { 427 + paddingLeft: tokens.space._2xl, 428 + }, 429 + pl_3xl: { 430 + paddingLeft: tokens.space._3xl, 431 + }, 432 + pl_4xl: { 433 + paddingLeft: tokens.space._4xl, 434 + }, 435 + pl_5xl: { 436 + paddingLeft: tokens.space._5xl, 327 437 }, 328 - pr_xxs: { 329 - paddingRight: tokens.space.xxs, 438 + pr_2xs: { 439 + paddingRight: tokens.space._2xs, 330 440 }, 331 441 pr_xs: { 332 442 paddingRight: tokens.space.xs, ··· 343 453 pr_xl: { 344 454 paddingRight: tokens.space.xl, 345 455 }, 346 - pr_xxl: { 347 - paddingRight: tokens.space.xxl, 456 + pr_2xl: { 457 + paddingRight: tokens.space._2xl, 458 + }, 459 + pr_3xl: { 460 + paddingRight: tokens.space._3xl, 461 + }, 462 + pr_4xl: { 463 + paddingRight: tokens.space._4xl, 464 + }, 465 + pr_5xl: { 466 + paddingRight: tokens.space._5xl, 348 467 }, 349 468 350 469 /* 351 470 * Margin 352 471 */ 353 - m_xxs: { 354 - margin: tokens.space.xxs, 472 + m_2xs: { 473 + margin: tokens.space._2xs, 355 474 }, 356 475 m_xs: { 357 476 margin: tokens.space.xs, ··· 368 487 m_xl: { 369 488 margin: tokens.space.xl, 370 489 }, 371 - m_xxl: { 372 - margin: tokens.space.xxl, 490 + m_2xl: { 491 + margin: tokens.space._2xl, 492 + }, 493 + m_3xl: { 494 + margin: tokens.space._3xl, 495 + }, 496 + m_4xl: { 497 + margin: tokens.space._4xl, 498 + }, 499 + m_5xl: { 500 + margin: tokens.space._5xl, 373 501 }, 374 - mx_xxs: { 375 - marginLeft: tokens.space.xxs, 376 - marginRight: tokens.space.xxs, 502 + mx_2xs: { 503 + marginLeft: tokens.space._2xs, 504 + marginRight: tokens.space._2xs, 377 505 }, 378 506 mx_xs: { 379 507 marginLeft: tokens.space.xs, ··· 395 523 marginLeft: tokens.space.xl, 396 524 marginRight: tokens.space.xl, 397 525 }, 398 - mx_xxl: { 399 - marginLeft: tokens.space.xxl, 400 - marginRight: tokens.space.xxl, 526 + mx_2xl: { 527 + marginLeft: tokens.space._2xl, 528 + marginRight: tokens.space._2xl, 529 + }, 530 + mx_3xl: { 531 + marginLeft: tokens.space._3xl, 532 + marginRight: tokens.space._3xl, 533 + }, 534 + mx_4xl: { 535 + marginLeft: tokens.space._4xl, 536 + marginRight: tokens.space._4xl, 537 + }, 538 + mx_5xl: { 539 + marginLeft: tokens.space._5xl, 540 + marginRight: tokens.space._5xl, 401 541 }, 402 - my_xxs: { 403 - marginTop: tokens.space.xxs, 404 - marginBottom: tokens.space.xxs, 542 + my_2xs: { 543 + marginTop: tokens.space._2xs, 544 + marginBottom: tokens.space._2xs, 405 545 }, 406 546 my_xs: { 407 547 marginTop: tokens.space.xs, ··· 423 563 marginTop: tokens.space.xl, 424 564 marginBottom: tokens.space.xl, 425 565 }, 426 - my_xxl: { 427 - marginTop: tokens.space.xxl, 428 - marginBottom: tokens.space.xxl, 566 + my_2xl: { 567 + marginTop: tokens.space._2xl, 568 + marginBottom: tokens.space._2xl, 429 569 }, 430 - mt_xxs: { 431 - marginTop: tokens.space.xxs, 570 + my_3xl: { 571 + marginTop: tokens.space._3xl, 572 + marginBottom: tokens.space._3xl, 573 + }, 574 + my_4xl: { 575 + marginTop: tokens.space._4xl, 576 + marginBottom: tokens.space._4xl, 577 + }, 578 + my_5xl: { 579 + marginTop: tokens.space._5xl, 580 + marginBottom: tokens.space._5xl, 581 + }, 582 + mt_2xs: { 583 + marginTop: tokens.space._2xs, 432 584 }, 433 585 mt_xs: { 434 586 marginTop: tokens.space.xs, ··· 445 597 mt_xl: { 446 598 marginTop: tokens.space.xl, 447 599 }, 448 - mt_xxl: { 449 - marginTop: tokens.space.xxl, 600 + mt_2xl: { 601 + marginTop: tokens.space._2xl, 602 + }, 603 + mt_3xl: { 604 + marginTop: tokens.space._3xl, 605 + }, 606 + mt_4xl: { 607 + marginTop: tokens.space._4xl, 608 + }, 609 + mt_5xl: { 610 + marginTop: tokens.space._5xl, 450 611 }, 451 - mb_xxs: { 452 - marginBottom: tokens.space.xxs, 612 + mb_2xs: { 613 + marginBottom: tokens.space._2xs, 453 614 }, 454 615 mb_xs: { 455 616 marginBottom: tokens.space.xs, ··· 466 627 mb_xl: { 467 628 marginBottom: tokens.space.xl, 468 629 }, 469 - mb_xxl: { 470 - marginBottom: tokens.space.xxl, 630 + mb_2xl: { 631 + marginBottom: tokens.space._2xl, 471 632 }, 472 - ml_xxs: { 473 - marginLeft: tokens.space.xxs, 633 + mb_3xl: { 634 + marginBottom: tokens.space._3xl, 635 + }, 636 + mb_4xl: { 637 + marginBottom: tokens.space._4xl, 638 + }, 639 + mb_5xl: { 640 + marginBottom: tokens.space._5xl, 641 + }, 642 + ml_2xs: { 643 + marginLeft: tokens.space._2xs, 474 644 }, 475 645 ml_xs: { 476 646 marginLeft: tokens.space.xs, ··· 487 657 ml_xl: { 488 658 marginLeft: tokens.space.xl, 489 659 }, 490 - ml_xxl: { 491 - marginLeft: tokens.space.xxl, 660 + ml_2xl: { 661 + marginLeft: tokens.space._2xl, 662 + }, 663 + ml_3xl: { 664 + marginLeft: tokens.space._3xl, 665 + }, 666 + ml_4xl: { 667 + marginLeft: tokens.space._4xl, 668 + }, 669 + ml_5xl: { 670 + marginLeft: tokens.space._5xl, 492 671 }, 493 - mr_xxs: { 494 - marginRight: tokens.space.xxs, 672 + mr_2xs: { 673 + marginRight: tokens.space._2xs, 495 674 }, 496 675 mr_xs: { 497 676 marginRight: tokens.space.xs, ··· 508 687 mr_xl: { 509 688 marginRight: tokens.space.xl, 510 689 }, 511 - mr_xxl: { 512 - marginRight: tokens.space.xxl, 690 + mr_2xl: { 691 + marginRight: tokens.space._2xl, 692 + }, 693 + mr_3xl: { 694 + marginRight: tokens.space._3xl, 695 + }, 696 + mr_4xl: { 697 + marginRight: tokens.space._4xl, 698 + }, 699 + mr_5xl: { 700 + marginRight: tokens.space._5xl, 513 701 }, 514 702 } as const
+1
src/alf/index.tsx
··· 5 5 export * as tokens from '#/alf/tokens' 6 6 export {atoms} from '#/alf/atoms' 7 7 export * from '#/alf/util/platform' 8 + export * from '#/alf/util/flatten' 8 9 9 10 type BreakpointName = keyof typeof breakpoints 10 11
+256 -44
src/alf/themes.ts
··· 1 1 import * as tokens from '#/alf/tokens' 2 2 import type {Mutable} from '#/alf/types' 3 + import {atoms} from '#/alf/atoms' 3 4 4 - export type ThemeName = 'light' | 'dark' 5 + export type ThemeName = 'light' | 'dim' | 'dark' 5 6 export type ReadonlyTheme = typeof light 6 7 export type Theme = Mutable<ReadonlyTheme> 8 + export type ReadonlyPalette = typeof lightPalette 9 + export type Palette = Mutable<ReadonlyPalette> 7 10 8 - export type Palette = { 9 - primary: string 10 - positive: string 11 - negative: string 12 - } 11 + export const lightPalette = { 12 + white: tokens.color.gray_0, 13 + black: tokens.color.gray_1000, 14 + 15 + contrast_25: tokens.color.gray_25, 16 + contrast_50: tokens.color.gray_50, 17 + contrast_100: tokens.color.gray_100, 18 + contrast_200: tokens.color.gray_200, 19 + contrast_300: tokens.color.gray_300, 20 + contrast_400: tokens.color.gray_400, 21 + contrast_500: tokens.color.gray_500, 22 + contrast_600: tokens.color.gray_600, 23 + contrast_700: tokens.color.gray_700, 24 + contrast_800: tokens.color.gray_800, 25 + contrast_900: tokens.color.gray_900, 26 + contrast_950: tokens.color.gray_950, 27 + contrast_975: tokens.color.gray_975, 28 + 29 + primary_25: tokens.color.blue_25, 30 + primary_50: tokens.color.blue_50, 31 + primary_100: tokens.color.blue_100, 32 + primary_200: tokens.color.blue_200, 33 + primary_300: tokens.color.blue_300, 34 + primary_400: tokens.color.blue_400, 35 + primary_500: tokens.color.blue_500, 36 + primary_600: tokens.color.blue_600, 37 + primary_700: tokens.color.blue_700, 38 + primary_800: tokens.color.blue_800, 39 + primary_900: tokens.color.blue_900, 40 + primary_950: tokens.color.blue_950, 41 + primary_975: tokens.color.blue_975, 42 + 43 + positive_25: tokens.color.green_25, 44 + positive_50: tokens.color.green_50, 45 + positive_100: tokens.color.green_100, 46 + positive_200: tokens.color.green_200, 47 + positive_300: tokens.color.green_300, 48 + positive_400: tokens.color.green_400, 49 + positive_500: tokens.color.green_500, 50 + positive_600: tokens.color.green_600, 51 + positive_700: tokens.color.green_700, 52 + positive_800: tokens.color.green_800, 53 + positive_900: tokens.color.green_900, 54 + positive_950: tokens.color.green_950, 55 + positive_975: tokens.color.green_975, 13 56 14 - export const lightPalette: Palette = { 15 - primary: tokens.color.blue_500, 16 - positive: tokens.color.green_500, 17 - negative: tokens.color.red_500, 57 + negative_25: tokens.color.red_25, 58 + negative_50: tokens.color.red_50, 59 + negative_100: tokens.color.red_100, 60 + negative_200: tokens.color.red_200, 61 + negative_300: tokens.color.red_300, 62 + negative_400: tokens.color.red_400, 63 + negative_500: tokens.color.red_500, 64 + negative_600: tokens.color.red_600, 65 + negative_700: tokens.color.red_700, 66 + negative_800: tokens.color.red_800, 67 + negative_900: tokens.color.red_900, 68 + negative_950: tokens.color.red_950, 69 + negative_975: tokens.color.red_975, 18 70 } as const 19 71 20 72 export const darkPalette: Palette = { 21 - primary: tokens.color.blue_500, 22 - positive: tokens.color.green_400, 23 - negative: tokens.color.red_400, 73 + white: tokens.color.gray_0, 74 + black: tokens.color.gray_1000, 75 + 76 + contrast_25: tokens.color.gray_975, 77 + contrast_50: tokens.color.gray_950, 78 + contrast_100: tokens.color.gray_900, 79 + contrast_200: tokens.color.gray_800, 80 + contrast_300: tokens.color.gray_700, 81 + contrast_400: tokens.color.gray_600, 82 + contrast_500: tokens.color.gray_500, 83 + contrast_600: tokens.color.gray_400, 84 + contrast_700: tokens.color.gray_300, 85 + contrast_800: tokens.color.gray_200, 86 + contrast_900: tokens.color.gray_100, 87 + contrast_950: tokens.color.gray_50, 88 + contrast_975: tokens.color.gray_25, 89 + 90 + primary_25: tokens.color.blue_25, 91 + primary_50: tokens.color.blue_50, 92 + primary_100: tokens.color.blue_100, 93 + primary_200: tokens.color.blue_200, 94 + primary_300: tokens.color.blue_300, 95 + primary_400: tokens.color.blue_400, 96 + primary_500: tokens.color.blue_500, 97 + primary_600: tokens.color.blue_600, 98 + primary_700: tokens.color.blue_700, 99 + primary_800: tokens.color.blue_800, 100 + primary_900: tokens.color.blue_900, 101 + primary_950: tokens.color.blue_950, 102 + primary_975: tokens.color.blue_975, 103 + 104 + positive_25: tokens.color.green_25, 105 + positive_50: tokens.color.green_50, 106 + positive_100: tokens.color.green_100, 107 + positive_200: tokens.color.green_200, 108 + positive_300: tokens.color.green_300, 109 + positive_400: tokens.color.green_400, 110 + positive_500: tokens.color.green_500, 111 + positive_600: tokens.color.green_600, 112 + positive_700: tokens.color.green_700, 113 + positive_800: tokens.color.green_800, 114 + positive_900: tokens.color.green_900, 115 + positive_950: tokens.color.green_950, 116 + positive_975: tokens.color.green_975, 117 + 118 + negative_25: tokens.color.red_25, 119 + negative_50: tokens.color.red_50, 120 + negative_100: tokens.color.red_100, 121 + negative_200: tokens.color.red_200, 122 + negative_300: tokens.color.red_300, 123 + negative_400: tokens.color.red_400, 124 + negative_500: tokens.color.red_500, 125 + negative_600: tokens.color.red_600, 126 + negative_700: tokens.color.red_700, 127 + negative_800: tokens.color.red_800, 128 + negative_900: tokens.color.red_900, 129 + negative_950: tokens.color.red_950, 130 + negative_975: tokens.color.red_975, 24 131 } as const 25 132 26 133 export const light = { 134 + name: 'light', 27 135 palette: lightPalette, 28 136 atoms: { 29 137 text: { 30 - color: tokens.color.gray_1000, 138 + color: lightPalette.black, 31 139 }, 32 140 text_contrast_700: { 33 - color: tokens.color.gray_700, 141 + color: lightPalette.contrast_700, 142 + }, 143 + text_contrast_600: { 144 + color: lightPalette.contrast_600, 34 145 }, 35 146 text_contrast_500: { 36 - color: tokens.color.gray_500, 147 + color: lightPalette.contrast_500, 148 + }, 149 + text_contrast_400: { 150 + color: lightPalette.contrast_400, 37 151 }, 38 152 text_inverted: { 39 - color: tokens.color.white, 153 + color: lightPalette.white, 40 154 }, 41 155 bg: { 42 - backgroundColor: tokens.color.white, 156 + backgroundColor: lightPalette.white, 157 + }, 158 + bg_contrast_25: { 159 + backgroundColor: lightPalette.contrast_25, 160 + }, 161 + bg_contrast_50: { 162 + backgroundColor: lightPalette.contrast_50, 43 163 }, 44 164 bg_contrast_100: { 45 - backgroundColor: tokens.color.gray_100, 165 + backgroundColor: lightPalette.contrast_100, 46 166 }, 47 167 bg_contrast_200: { 48 - backgroundColor: tokens.color.gray_200, 168 + backgroundColor: lightPalette.contrast_200, 49 169 }, 50 170 bg_contrast_300: { 51 - backgroundColor: tokens.color.gray_300, 171 + backgroundColor: lightPalette.contrast_300, 172 + }, 173 + border: { 174 + borderColor: lightPalette.contrast_200, 175 + }, 176 + border_contrast: { 177 + borderColor: lightPalette.contrast_400, 178 + }, 179 + shadow_sm: { 180 + ...atoms.shadow_sm, 181 + shadowColor: lightPalette.black, 182 + }, 183 + shadow_md: { 184 + ...atoms.shadow_md, 185 + shadowColor: lightPalette.black, 186 + }, 187 + shadow_lg: { 188 + ...atoms.shadow_lg, 189 + shadowColor: lightPalette.black, 190 + }, 191 + }, 192 + } 193 + 194 + export const dim: Theme = { 195 + name: 'dim', 196 + palette: darkPalette, 197 + atoms: { 198 + text: { 199 + color: darkPalette.white, 200 + }, 201 + text_contrast_700: { 202 + color: darkPalette.contrast_800, 203 + }, 204 + text_contrast_600: { 205 + color: darkPalette.contrast_700, 206 + }, 207 + text_contrast_500: { 208 + color: darkPalette.contrast_600, 209 + }, 210 + text_contrast_400: { 211 + color: darkPalette.contrast_500, 212 + }, 213 + text_inverted: { 214 + color: darkPalette.black, 215 + }, 216 + bg: { 217 + backgroundColor: darkPalette.contrast_50, 52 218 }, 53 - bg_positive: { 54 - backgroundColor: tokens.color.green_500, 219 + bg_contrast_25: { 220 + backgroundColor: darkPalette.contrast_100, 55 221 }, 56 - bg_negative: { 57 - backgroundColor: tokens.color.red_400, 222 + bg_contrast_50: { 223 + backgroundColor: darkPalette.contrast_200, 224 + }, 225 + bg_contrast_100: { 226 + backgroundColor: darkPalette.contrast_300, 227 + }, 228 + bg_contrast_200: { 229 + backgroundColor: darkPalette.contrast_400, 230 + }, 231 + bg_contrast_300: { 232 + backgroundColor: darkPalette.contrast_500, 58 233 }, 59 234 border: { 60 - borderColor: tokens.color.gray_200, 235 + borderColor: darkPalette.contrast_200, 236 + }, 237 + border_contrast: { 238 + borderColor: darkPalette.contrast_400, 239 + }, 240 + shadow_sm: { 241 + ...atoms.shadow_sm, 242 + shadowOpacity: 0.7, 243 + shadowColor: tokens.color.trueBlack, 61 244 }, 62 - border_contrast_500: { 63 - borderColor: tokens.color.gray_500, 245 + shadow_md: { 246 + ...atoms.shadow_md, 247 + shadowOpacity: 0.7, 248 + shadowColor: tokens.color.trueBlack, 249 + }, 250 + shadow_lg: { 251 + ...atoms.shadow_lg, 252 + shadowOpacity: 0.7, 253 + shadowColor: tokens.color.trueBlack, 64 254 }, 65 255 }, 66 256 } 67 257 68 258 export const dark: Theme = { 259 + name: 'dark', 69 260 palette: darkPalette, 70 261 atoms: { 71 262 text: { 72 - color: tokens.color.white, 263 + color: darkPalette.white, 73 264 }, 74 265 text_contrast_700: { 75 - color: tokens.color.gray_300, 266 + color: darkPalette.contrast_700, 267 + }, 268 + text_contrast_600: { 269 + color: darkPalette.contrast_600, 76 270 }, 77 271 text_contrast_500: { 78 - color: tokens.color.gray_500, 272 + color: darkPalette.contrast_500, 273 + }, 274 + text_contrast_400: { 275 + color: darkPalette.contrast_400, 79 276 }, 80 277 text_inverted: { 81 - color: tokens.color.gray_1000, 278 + color: darkPalette.black, 82 279 }, 83 280 bg: { 84 - backgroundColor: tokens.color.gray_1000, 281 + backgroundColor: darkPalette.contrast_25, 282 + }, 283 + bg_contrast_25: { 284 + backgroundColor: darkPalette.contrast_50, 285 + }, 286 + bg_contrast_50: { 287 + backgroundColor: darkPalette.contrast_100, 85 288 }, 86 289 bg_contrast_100: { 87 - backgroundColor: tokens.color.gray_900, 290 + backgroundColor: darkPalette.contrast_200, 88 291 }, 89 292 bg_contrast_200: { 90 - backgroundColor: tokens.color.gray_800, 293 + backgroundColor: darkPalette.contrast_300, 91 294 }, 92 295 bg_contrast_300: { 93 - backgroundColor: tokens.color.gray_700, 296 + backgroundColor: darkPalette.contrast_400, 297 + }, 298 + border: { 299 + borderColor: darkPalette.contrast_100, 94 300 }, 95 - bg_positive: { 96 - backgroundColor: tokens.color.green_400, 301 + border_contrast: { 302 + borderColor: darkPalette.contrast_300, 97 303 }, 98 - bg_negative: { 99 - backgroundColor: tokens.color.red_400, 304 + shadow_sm: { 305 + ...atoms.shadow_sm, 306 + shadowOpacity: 0.7, 307 + shadowColor: tokens.color.trueBlack, 100 308 }, 101 - border: { 102 - borderColor: tokens.color.gray_800, 309 + shadow_md: { 310 + ...atoms.shadow_md, 311 + shadowOpacity: 0.7, 312 + shadowColor: tokens.color.trueBlack, 103 313 }, 104 - border_contrast_500: { 105 - borderColor: tokens.color.gray_500, 314 + shadow_lg: { 315 + ...atoms.shadow_lg, 316 + shadowOpacity: 0.7, 317 + shadowColor: tokens.color.trueBlack, 106 318 }, 107 319 }, 108 320 }
+121 -53
src/alf/tokens.ts
··· 1 1 const BLUE_HUE = 211 2 - const GRAYSCALE_SATURATION = 22 2 + const RED_HUE = 346 3 + const GREEN_HUE = 152 3 4 4 5 export const color = { 5 - white: '#FFFFFF', 6 + trueBlack: '#000000', 6 7 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%)`, 8 + gray_0: `hsl(${BLUE_HUE}, 20%, 100%)`, 9 + gray_25: `hsl(${BLUE_HUE}, 20%, 97%)`, 10 + gray_50: `hsl(${BLUE_HUE}, 20%, 95%)`, 11 + gray_100: `hsl(${BLUE_HUE}, 20%, 90%)`, 12 + gray_200: `hsl(${BLUE_HUE}, 20%, 80%)`, 13 + gray_300: `hsl(${BLUE_HUE}, 20%, 70%)`, 14 + gray_400: `hsl(${BLUE_HUE}, 20%, 60%)`, 15 + gray_500: `hsl(${BLUE_HUE}, 20%, 50%)`, 16 + gray_600: `hsl(${BLUE_HUE}, 20%, 42%)`, 17 + gray_700: `hsl(${BLUE_HUE}, 20%, 34%)`, 18 + gray_800: `hsl(${BLUE_HUE}, 20%, 26%)`, 19 + gray_900: `hsl(${BLUE_HUE}, 20%, 18%)`, 20 + gray_950: `hsl(${BLUE_HUE}, 20%, 10%)`, 21 + gray_975: `hsl(${BLUE_HUE}, 20%, 7%)`, 22 + gray_1000: `hsl(${BLUE_HUE}, 20%, 4%)`, 18 23 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_25: `hsl(${BLUE_HUE}, 99%, 97%)`, 25 + blue_50: `hsl(${BLUE_HUE}, 99%, 95%)`, 26 + blue_100: `hsl(${BLUE_HUE}, 99%, 90%)`, 27 + blue_200: `hsl(${BLUE_HUE}, 99%, 80%)`, 28 + blue_300: `hsl(${BLUE_HUE}, 99%, 70%)`, 29 + blue_400: `hsl(${BLUE_HUE}, 99%, 60%)`, 24 30 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%)`, 31 + blue_600: `hsl(${BLUE_HUE}, 99%, 42%)`, 32 + blue_700: `hsl(${BLUE_HUE}, 99%, 34%)`, 33 + blue_800: `hsl(${BLUE_HUE}, 99%, 26%)`, 34 + blue_900: `hsl(${BLUE_HUE}, 99%, 18%)`, 35 + blue_950: `hsl(${BLUE_HUE}, 99%, 10%)`, 36 + blue_975: `hsl(${BLUE_HUE}, 99%, 7%)`, 30 37 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%)`, 38 + green_25: `hsl(${GREEN_HUE}, 82%, 97%)`, 39 + green_50: `hsl(${GREEN_HUE}, 82%, 95%)`, 40 + green_100: `hsl(${GREEN_HUE}, 82%, 90%)`, 41 + green_200: `hsl(${GREEN_HUE}, 82%, 80%)`, 42 + green_300: `hsl(${GREEN_HUE}, 82%, 70%)`, 43 + green_400: `hsl(${GREEN_HUE}, 82%, 60%)`, 44 + green_500: `hsl(${GREEN_HUE}, 82%, 50%)`, 45 + green_600: `hsl(${GREEN_HUE}, 82%, 42%)`, 46 + green_700: `hsl(${GREEN_HUE}, 82%, 34%)`, 47 + green_800: `hsl(${GREEN_HUE}, 82%, 26%)`, 48 + green_900: `hsl(${GREEN_HUE}, 82%, 18%)`, 49 + green_950: `hsl(${GREEN_HUE}, 82%, 10%)`, 50 + green_975: `hsl(${GREEN_HUE}, 82%, 7%)`, 42 51 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%)`, 52 + red_25: `hsl(${RED_HUE}, 91%, 97%)`, 53 + red_50: `hsl(${RED_HUE}, 91%, 95%)`, 54 + red_100: `hsl(${RED_HUE}, 91%, 90%)`, 55 + red_200: `hsl(${RED_HUE}, 91%, 80%)`, 56 + red_300: `hsl(${RED_HUE}, 91%, 70%)`, 57 + red_400: `hsl(${RED_HUE}, 91%, 60%)`, 58 + red_500: `hsl(${RED_HUE}, 91%, 50%)`, 59 + red_600: `hsl(${RED_HUE}, 91%, 42%)`, 60 + red_700: `hsl(${RED_HUE}, 91%, 34%)`, 61 + red_800: `hsl(${RED_HUE}, 91%, 26%)`, 62 + red_900: `hsl(${RED_HUE}, 91%, 18%)`, 63 + red_950: `hsl(${RED_HUE}, 91%, 10%)`, 64 + red_975: `hsl(${RED_HUE}, 91%, 7%)`, 54 65 } as const 55 66 56 67 export const space = { 57 - xxs: 2, 68 + _2xs: 2, 58 69 xs: 4, 59 70 sm: 8, 60 71 md: 12, 61 - lg: 18, 62 - xl: 24, 63 - xxl: 32, 72 + lg: 16, 73 + xl: 20, 74 + _2xl: 24, 75 + _3xl: 28, 76 + _4xl: 32, 77 + _5xl: 40, 64 78 } as const 65 79 66 80 export const fontSize = { 67 - xxs: 10, 81 + _2xs: 10, 68 82 xs: 12, 69 83 sm: 14, 70 84 md: 16, 71 85 lg: 18, 72 - xl: 22, 73 - xxl: 26, 86 + xl: 20, 87 + _2xl: 22, 88 + _3xl: 26, 89 + _4xl: 32, 90 + _5xl: 40, 74 91 } as const 75 92 76 - // TODO test 77 93 export const lineHeight = { 78 94 none: 1, 79 95 normal: 1.5, ··· 81 97 } as const 82 98 83 99 export const borderRadius = { 100 + _2xs: 2, 101 + xs: 4, 84 102 sm: 8, 85 103 md: 12, 86 104 full: 999, ··· 90 108 normal: '400', 91 109 semibold: '600', 92 110 bold: '900', 111 + } as const 112 + 113 + export const gradients = { 114 + sky: { 115 + values: [ 116 + [0, '#0A7AFF'], 117 + [1, '#59B9FF'], 118 + ], 119 + hover_value: '#0A7AFF', 120 + }, 121 + midnight: { 122 + values: [ 123 + [0, '#022C5E'], 124 + [1, '#4079BC'], 125 + ], 126 + hover_value: '#022C5E', 127 + }, 128 + sunrise: { 129 + values: [ 130 + [0, '#4E90AE'], 131 + [0.4, '#AEA3AB'], 132 + [0.8, '#E6A98F'], 133 + [1, '#F3A84C'], 134 + ], 135 + hover_value: '#AEA3AB', 136 + }, 137 + sunset: { 138 + values: [ 139 + [0, '#6772AF'], 140 + [0.6, '#B88BB6'], 141 + [1, '#FFA6AC'], 142 + ], 143 + hover_value: '#B88BB6', 144 + }, 145 + nordic: { 146 + values: [ 147 + [0, '#083367'], 148 + [1, '#9EE8C1'], 149 + ], 150 + hover_value: '#3A7085', 151 + }, 152 + bonfire: { 153 + values: [ 154 + [0, '#203E4E'], 155 + [0.4, '#755B62'], 156 + [0.8, '#CD7765'], 157 + [1, '#EF956E'], 158 + ], 159 + hover_value: '#755B62', 160 + }, 93 161 } as const 94 162 95 163 export type Color = keyof typeof color
+3
src/alf/util/flatten.ts
··· 1 + import {StyleSheet} from 'react-native' 2 + 3 + export const flatten = StyleSheet.flatten
+507
src/components/Button.tsx
··· 1 + import React from 'react' 2 + import { 3 + Pressable, 4 + Text, 5 + PressableProps, 6 + TextProps, 7 + ViewStyle, 8 + AccessibilityProps, 9 + View, 10 + TextStyle, 11 + StyleSheet, 12 + } from 'react-native' 13 + import LinearGradient from 'react-native-linear-gradient' 14 + 15 + import {useTheme, atoms as a, tokens, web, native} from '#/alf' 16 + import {Props as SVGIconProps} from '#/components/icons/common' 17 + 18 + export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' 19 + export type ButtonColor = 20 + | 'primary' 21 + | 'secondary' 22 + | 'negative' 23 + | 'gradient_sky' 24 + | 'gradient_midnight' 25 + | 'gradient_sunrise' 26 + | 'gradient_sunset' 27 + | 'gradient_nordic' 28 + | 'gradient_bonfire' 29 + export type ButtonSize = 'small' | 'large' 30 + export type VariantProps = { 31 + /** 32 + * The style variation of the button 33 + */ 34 + variant?: ButtonVariant 35 + /** 36 + * The color of the button 37 + */ 38 + color?: ButtonColor 39 + /** 40 + * The size of the button 41 + */ 42 + size?: ButtonSize 43 + } 44 + 45 + export type ButtonProps = React.PropsWithChildren< 46 + Pick<PressableProps, 'disabled' | 'onPress'> & 47 + AccessibilityProps & 48 + VariantProps & { 49 + label: string 50 + } 51 + > 52 + export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} 53 + 54 + const Context = React.createContext< 55 + VariantProps & { 56 + hovered: boolean 57 + focused: boolean 58 + pressed: boolean 59 + disabled: boolean 60 + } 61 + >({ 62 + hovered: false, 63 + focused: false, 64 + pressed: false, 65 + disabled: false, 66 + }) 67 + 68 + export function useButtonContext() { 69 + return React.useContext(Context) 70 + } 71 + 72 + export function Button({ 73 + children, 74 + variant, 75 + color, 76 + size, 77 + label, 78 + disabled = false, 79 + ...rest 80 + }: ButtonProps) { 81 + const t = useTheme() 82 + const [state, setState] = React.useState({ 83 + pressed: false, 84 + hovered: false, 85 + focused: false, 86 + }) 87 + 88 + const onPressIn = React.useCallback(() => { 89 + setState(s => ({ 90 + ...s, 91 + pressed: true, 92 + })) 93 + }, [setState]) 94 + const onPressOut = React.useCallback(() => { 95 + setState(s => ({ 96 + ...s, 97 + pressed: false, 98 + })) 99 + }, [setState]) 100 + const onHoverIn = React.useCallback(() => { 101 + setState(s => ({ 102 + ...s, 103 + hovered: true, 104 + })) 105 + }, [setState]) 106 + const onHoverOut = React.useCallback(() => { 107 + setState(s => ({ 108 + ...s, 109 + hovered: false, 110 + })) 111 + }, [setState]) 112 + const onFocus = React.useCallback(() => { 113 + setState(s => ({ 114 + ...s, 115 + focused: true, 116 + })) 117 + }, [setState]) 118 + const onBlur = React.useCallback(() => { 119 + setState(s => ({ 120 + ...s, 121 + focused: false, 122 + })) 123 + }, [setState]) 124 + 125 + const {baseStyles, hoverStyles, focusStyles} = React.useMemo(() => { 126 + const baseStyles: ViewStyle[] = [] 127 + const hoverStyles: ViewStyle[] = [] 128 + const light = t.name === 'light' 129 + 130 + if (color === 'primary') { 131 + if (variant === 'solid') { 132 + if (!disabled) { 133 + baseStyles.push({ 134 + backgroundColor: t.palette.primary_500, 135 + }) 136 + hoverStyles.push({ 137 + backgroundColor: t.palette.primary_600, 138 + }) 139 + } else { 140 + baseStyles.push({ 141 + backgroundColor: t.palette.primary_700, 142 + }) 143 + } 144 + } else if (variant === 'outline') { 145 + baseStyles.push(a.border, t.atoms.bg, { 146 + borderWidth: 1, 147 + }) 148 + 149 + if (!disabled) { 150 + baseStyles.push(a.border, { 151 + borderColor: tokens.color.blue_500, 152 + }) 153 + hoverStyles.push(a.border, { 154 + backgroundColor: light 155 + ? t.palette.primary_50 156 + : t.palette.primary_950, 157 + }) 158 + } else { 159 + baseStyles.push(a.border, { 160 + borderColor: light ? tokens.color.blue_200 : tokens.color.blue_900, 161 + }) 162 + } 163 + } else if (variant === 'ghost') { 164 + if (!disabled) { 165 + baseStyles.push(t.atoms.bg) 166 + hoverStyles.push({ 167 + backgroundColor: light 168 + ? t.palette.primary_100 169 + : t.palette.primary_900, 170 + }) 171 + } 172 + } 173 + } else if (color === 'secondary') { 174 + if (variant === 'solid') { 175 + if (!disabled) { 176 + baseStyles.push({ 177 + backgroundColor: light 178 + ? tokens.color.gray_100 179 + : tokens.color.gray_900, 180 + }) 181 + hoverStyles.push({ 182 + backgroundColor: light 183 + ? tokens.color.gray_200 184 + : tokens.color.gray_950, 185 + }) 186 + } else { 187 + baseStyles.push({ 188 + backgroundColor: light 189 + ? tokens.color.gray_300 190 + : tokens.color.gray_950, 191 + }) 192 + } 193 + } else if (variant === 'outline') { 194 + baseStyles.push(a.border, t.atoms.bg, { 195 + borderWidth: 1, 196 + }) 197 + 198 + if (!disabled) { 199 + baseStyles.push(a.border, { 200 + borderColor: light ? tokens.color.gray_500 : tokens.color.gray_500, 201 + }) 202 + hoverStyles.push(a.border, t.atoms.bg_contrast_50) 203 + } else { 204 + baseStyles.push(a.border, { 205 + borderColor: light ? tokens.color.gray_200 : tokens.color.gray_800, 206 + }) 207 + } 208 + } else if (variant === 'ghost') { 209 + if (!disabled) { 210 + baseStyles.push(t.atoms.bg) 211 + hoverStyles.push({ 212 + backgroundColor: light 213 + ? tokens.color.gray_100 214 + : tokens.color.gray_900, 215 + }) 216 + } 217 + } 218 + } else if (color === 'negative') { 219 + if (variant === 'solid') { 220 + if (!disabled) { 221 + baseStyles.push({ 222 + backgroundColor: t.palette.negative_400, 223 + }) 224 + hoverStyles.push({ 225 + backgroundColor: t.palette.negative_500, 226 + }) 227 + } else { 228 + baseStyles.push({ 229 + backgroundColor: t.palette.negative_600, 230 + }) 231 + } 232 + } else if (variant === 'outline') { 233 + baseStyles.push(a.border, t.atoms.bg, { 234 + borderWidth: 1, 235 + }) 236 + 237 + if (!disabled) { 238 + baseStyles.push(a.border, { 239 + borderColor: t.palette.negative_400, 240 + }) 241 + hoverStyles.push(a.border, { 242 + backgroundColor: light 243 + ? t.palette.negative_50 244 + : t.palette.negative_975, 245 + }) 246 + } else { 247 + baseStyles.push(a.border, { 248 + borderColor: light 249 + ? t.palette.negative_200 250 + : t.palette.negative_900, 251 + }) 252 + } 253 + } else if (variant === 'ghost') { 254 + if (!disabled) { 255 + baseStyles.push(t.atoms.bg) 256 + hoverStyles.push({ 257 + backgroundColor: light 258 + ? t.palette.negative_100 259 + : t.palette.negative_950, 260 + }) 261 + } 262 + } 263 + } 264 + 265 + if (size === 'large') { 266 + baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_sm) 267 + } else if (size === 'small') { 268 + baseStyles.push({paddingVertical: 9}, a.px_md, a.rounded_sm, a.gap_sm) 269 + } 270 + 271 + return { 272 + baseStyles, 273 + hoverStyles, 274 + focusStyles: [ 275 + ...hoverStyles, 276 + { 277 + outline: 0, 278 + } as ViewStyle, 279 + ], 280 + } 281 + }, [t, variant, color, size, disabled]) 282 + 283 + const {gradientColors, gradientHoverColors, gradientLocations} = 284 + React.useMemo(() => { 285 + const colors: string[] = [] 286 + const hoverColors: string[] = [] 287 + const locations: number[] = [] 288 + const gradient = { 289 + primary: tokens.gradients.sky, 290 + secondary: tokens.gradients.sky, 291 + negative: tokens.gradients.sky, 292 + gradient_sky: tokens.gradients.sky, 293 + gradient_midnight: tokens.gradients.midnight, 294 + gradient_sunrise: tokens.gradients.sunrise, 295 + gradient_sunset: tokens.gradients.sunset, 296 + gradient_nordic: tokens.gradients.nordic, 297 + gradient_bonfire: tokens.gradients.bonfire, 298 + }[color || 'primary'] 299 + 300 + if (variant === 'gradient') { 301 + colors.push(...gradient.values.map(([_, color]) => color)) 302 + hoverColors.push(...gradient.values.map(_ => gradient.hover_value)) 303 + locations.push(...gradient.values.map(([location, _]) => location)) 304 + } 305 + 306 + return { 307 + gradientColors: colors, 308 + gradientHoverColors: hoverColors, 309 + gradientLocations: locations, 310 + } 311 + }, [variant, color]) 312 + 313 + const context = React.useMemo( 314 + () => ({ 315 + ...state, 316 + variant, 317 + color, 318 + size, 319 + disabled: disabled || false, 320 + }), 321 + [state, variant, color, size, disabled], 322 + ) 323 + 324 + return ( 325 + <Pressable 326 + role="button" 327 + accessibilityHint={undefined} // optional 328 + {...rest} 329 + aria-label={label} 330 + aria-pressed={state.pressed} 331 + accessibilityLabel={label} 332 + disabled={disabled || false} 333 + accessibilityState={{ 334 + disabled: disabled || false, 335 + }} 336 + style={[ 337 + a.flex_row, 338 + a.align_center, 339 + a.overflow_hidden, 340 + ...baseStyles, 341 + ...(state.hovered || state.pressed ? hoverStyles : []), 342 + ...(state.focused ? focusStyles : []), 343 + ]} 344 + onPressIn={onPressIn} 345 + onPressOut={onPressOut} 346 + onHoverIn={onHoverIn} 347 + onHoverOut={onHoverOut} 348 + onFocus={onFocus} 349 + onBlur={onBlur}> 350 + {variant === 'gradient' && ( 351 + <LinearGradient 352 + colors={ 353 + state.hovered || state.pressed || state.focused 354 + ? gradientHoverColors 355 + : gradientColors 356 + } 357 + locations={gradientLocations} 358 + start={{x: 0, y: 0}} 359 + end={{x: 1, y: 1}} 360 + style={[a.absolute, a.inset_0]} 361 + /> 362 + )} 363 + <Context.Provider value={context}> 364 + {typeof children === 'string' ? ( 365 + <ButtonText>{children}</ButtonText> 366 + ) : ( 367 + children 368 + )} 369 + </Context.Provider> 370 + </Pressable> 371 + ) 372 + } 373 + 374 + export function useSharedButtonTextStyles() { 375 + const t = useTheme() 376 + const {color, variant, disabled, size} = useButtonContext() 377 + return React.useMemo(() => { 378 + const baseStyles: TextStyle[] = [] 379 + const light = t.name === 'light' 380 + 381 + if (color === 'primary') { 382 + if (variant === 'solid') { 383 + if (!disabled) { 384 + baseStyles.push({color: t.palette.white}) 385 + } else { 386 + baseStyles.push({color: t.palette.white, opacity: 0.5}) 387 + } 388 + } else if (variant === 'outline') { 389 + if (!disabled) { 390 + baseStyles.push({ 391 + color: light ? t.palette.primary_600 : t.palette.primary_500, 392 + }) 393 + } else { 394 + baseStyles.push({color: t.palette.primary_600, opacity: 0.5}) 395 + } 396 + } else if (variant === 'ghost') { 397 + if (!disabled) { 398 + baseStyles.push({color: t.palette.primary_600}) 399 + } else { 400 + baseStyles.push({color: t.palette.primary_600, opacity: 0.5}) 401 + } 402 + } 403 + } else if (color === 'secondary') { 404 + if (variant === 'solid' || variant === 'gradient') { 405 + if (!disabled) { 406 + baseStyles.push({ 407 + color: light ? tokens.color.gray_700 : tokens.color.gray_100, 408 + }) 409 + } else { 410 + baseStyles.push({ 411 + color: light ? tokens.color.gray_400 : tokens.color.gray_700, 412 + }) 413 + } 414 + } else if (variant === 'outline') { 415 + if (!disabled) { 416 + baseStyles.push({ 417 + color: light ? tokens.color.gray_600 : tokens.color.gray_300, 418 + }) 419 + } else { 420 + baseStyles.push({ 421 + color: light ? tokens.color.gray_400 : tokens.color.gray_700, 422 + }) 423 + } 424 + } else if (variant === 'ghost') { 425 + if (!disabled) { 426 + baseStyles.push({ 427 + color: light ? tokens.color.gray_600 : tokens.color.gray_300, 428 + }) 429 + } else { 430 + baseStyles.push({ 431 + color: light ? tokens.color.gray_400 : tokens.color.gray_600, 432 + }) 433 + } 434 + } 435 + } else if (color === 'negative') { 436 + if (variant === 'solid' || variant === 'gradient') { 437 + if (!disabled) { 438 + baseStyles.push({color: t.palette.white}) 439 + } else { 440 + baseStyles.push({color: t.palette.white, opacity: 0.5}) 441 + } 442 + } else if (variant === 'outline') { 443 + if (!disabled) { 444 + baseStyles.push({color: t.palette.negative_400}) 445 + } else { 446 + baseStyles.push({color: t.palette.negative_400, opacity: 0.5}) 447 + } 448 + } else if (variant === 'ghost') { 449 + if (!disabled) { 450 + baseStyles.push({color: t.palette.negative_400}) 451 + } else { 452 + baseStyles.push({color: t.palette.negative_400, opacity: 0.5}) 453 + } 454 + } 455 + } else { 456 + if (!disabled) { 457 + baseStyles.push({color: t.palette.white}) 458 + } else { 459 + baseStyles.push({color: t.palette.white, opacity: 0.5}) 460 + } 461 + } 462 + 463 + if (size === 'large') { 464 + baseStyles.push( 465 + a.text_md, 466 + web({paddingBottom: 1}), 467 + native({marginTop: 2}), 468 + ) 469 + } else { 470 + baseStyles.push( 471 + a.text_md, 472 + web({paddingBottom: 1}), 473 + native({marginTop: 2}), 474 + ) 475 + } 476 + 477 + return StyleSheet.flatten(baseStyles) 478 + }, [t, variant, color, size, disabled]) 479 + } 480 + 481 + export function ButtonText({children, style, ...rest}: ButtonTextProps) { 482 + const textStyles = useSharedButtonTextStyles() 483 + 484 + return ( 485 + <Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}> 486 + {children} 487 + </Text> 488 + ) 489 + } 490 + 491 + export function ButtonIcon({ 492 + icon: Comp, 493 + }: { 494 + icon: React.ComponentType<SVGIconProps> 495 + }) { 496 + const {size} = useButtonContext() 497 + const textStyles = useSharedButtonTextStyles() 498 + 499 + return ( 500 + <View style={[a.z_20]}> 501 + <Comp 502 + size={size === 'large' ? 'md' : 'sm'} 503 + style={[{color: textStyles.color, pointerEvents: 'none'}]} 504 + /> 505 + </View> 506 + ) 507 + }
+35
src/components/Dialog/context.ts
··· 1 + import React from 'react' 2 + 3 + import {useDialogStateContext} from '#/state/dialogs' 4 + import {DialogContextProps, DialogControlProps} from '#/components/Dialog/types' 5 + 6 + export const Context = React.createContext<DialogContextProps>({ 7 + close: () => {}, 8 + }) 9 + 10 + export function useDialogContext() { 11 + return React.useContext(Context) 12 + } 13 + 14 + export function useDialogControl() { 15 + const id = React.useId() 16 + const control = React.useRef<DialogControlProps>({ 17 + open: () => {}, 18 + close: () => {}, 19 + }) 20 + const {activeDialogs} = useDialogStateContext() 21 + 22 + React.useEffect(() => { 23 + activeDialogs.current.set(id, control) 24 + return () => { 25 + // eslint-disable-next-line react-hooks/exhaustive-deps 26 + activeDialogs.current.delete(id) 27 + } 28 + }, [id, activeDialogs]) 29 + 30 + return { 31 + ref: control, 32 + open: () => control.current.open(), 33 + close: () => control.current.close(), 34 + } 35 + }
+162
src/components/Dialog/index.tsx
··· 1 + import React, {useImperativeHandle} from 'react' 2 + import {View, Dimensions} from 'react-native' 3 + import BottomSheet, { 4 + BottomSheetBackdrop, 5 + BottomSheetScrollView, 6 + BottomSheetTextInput, 7 + BottomSheetView, 8 + } from '@gorhom/bottom-sheet' 9 + import {useSafeAreaInsets} from 'react-native-safe-area-context' 10 + 11 + import {useTheme, atoms as a} from '#/alf' 12 + import {Portal} from '#/components/Portal' 13 + import {createInput} from '#/components/forms/TextField' 14 + 15 + import { 16 + DialogOuterProps, 17 + DialogControlProps, 18 + DialogInnerProps, 19 + } from '#/components/Dialog/types' 20 + import {Context} from '#/components/Dialog/context' 21 + 22 + export {useDialogControl, useDialogContext} from '#/components/Dialog/context' 23 + export * from '#/components/Dialog/types' 24 + // @ts-ignore 25 + export const Input = createInput(BottomSheetTextInput) 26 + 27 + export function Outer({ 28 + children, 29 + control, 30 + onClose, 31 + nativeOptions, 32 + }: React.PropsWithChildren<DialogOuterProps>) { 33 + const t = useTheme() 34 + const sheet = React.useRef<BottomSheet>(null) 35 + const sheetOptions = nativeOptions?.sheet || {} 36 + const hasSnapPoints = !!sheetOptions.snapPoints 37 + 38 + const open = React.useCallback<DialogControlProps['open']>((i = 0) => { 39 + sheet.current?.snapToIndex(i) 40 + }, []) 41 + 42 + const close = React.useCallback(() => { 43 + sheet.current?.close() 44 + onClose?.() 45 + }, [onClose]) 46 + 47 + useImperativeHandle( 48 + control.ref, 49 + () => ({ 50 + open, 51 + close, 52 + }), 53 + [open, close], 54 + ) 55 + 56 + const context = React.useMemo(() => ({close}), [close]) 57 + 58 + return ( 59 + <Portal> 60 + <BottomSheet 61 + enableDynamicSizing={!hasSnapPoints} 62 + enablePanDownToClose 63 + keyboardBehavior="interactive" 64 + android_keyboardInputMode="adjustResize" 65 + keyboardBlurBehavior="restore" 66 + {...sheetOptions} 67 + ref={sheet} 68 + index={-1} 69 + backgroundStyle={{backgroundColor: 'transparent'}} 70 + backdropComponent={props => ( 71 + <BottomSheetBackdrop 72 + opacity={0.4} 73 + appearsOnIndex={0} 74 + disappearsOnIndex={-1} 75 + {...props} 76 + /> 77 + )} 78 + handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} 79 + handleStyle={{display: 'none'}} 80 + onClose={onClose}> 81 + <Context.Provider value={context}> 82 + <View 83 + style={[ 84 + a.absolute, 85 + a.inset_0, 86 + t.atoms.bg, 87 + { 88 + borderTopLeftRadius: 40, 89 + borderTopRightRadius: 40, 90 + height: Dimensions.get('window').height * 2, 91 + }, 92 + ]} 93 + /> 94 + {children} 95 + </Context.Provider> 96 + </BottomSheet> 97 + </Portal> 98 + ) 99 + } 100 + 101 + // TODO a11y props here, or is that handled by the sheet? 102 + export function Inner(props: DialogInnerProps) { 103 + const insets = useSafeAreaInsets() 104 + return ( 105 + <BottomSheetView 106 + style={[ 107 + a.p_lg, 108 + a.pt_3xl, 109 + { 110 + borderTopLeftRadius: 40, 111 + borderTopRightRadius: 40, 112 + paddingBottom: insets.bottom + a.pb_5xl.paddingBottom, 113 + }, 114 + ]}> 115 + {props.children} 116 + </BottomSheetView> 117 + ) 118 + } 119 + 120 + export function ScrollableInner(props: DialogInnerProps) { 121 + const insets = useSafeAreaInsets() 122 + return ( 123 + <BottomSheetScrollView 124 + style={[ 125 + a.flex_1, // main diff is this 126 + a.p_lg, 127 + a.pt_3xl, 128 + { 129 + borderTopLeftRadius: 40, 130 + borderTopRightRadius: 40, 131 + }, 132 + ]}> 133 + {props.children} 134 + <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} /> 135 + </BottomSheetScrollView> 136 + ) 137 + } 138 + 139 + export function Handle() { 140 + const t = useTheme() 141 + return ( 142 + <View 143 + style={[ 144 + a.absolute, 145 + a.rounded_sm, 146 + a.z_10, 147 + { 148 + top: a.pt_lg.paddingTop, 149 + width: 35, 150 + height: 4, 151 + alignSelf: 'center', 152 + backgroundColor: t.palette.contrast_900, 153 + opacity: 0.5, 154 + }, 155 + ]} 156 + /> 157 + ) 158 + } 159 + 160 + export function Close() { 161 + return null 162 + }
+194
src/components/Dialog/index.web.tsx
··· 1 + import React, {useImperativeHandle} from 'react' 2 + import {View, TouchableWithoutFeedback} from 'react-native' 3 + import {FocusScope} from '@tamagui/focus-scope' 4 + import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated' 5 + import {msg} from '@lingui/macro' 6 + import {useLingui} from '@lingui/react' 7 + 8 + import {useTheme, atoms as a, useBreakpoints, web} from '#/alf' 9 + import {Portal} from '#/components/Portal' 10 + 11 + import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types' 12 + import {Context} from '#/components/Dialog/context' 13 + 14 + export {useDialogControl, useDialogContext} from '#/components/Dialog/context' 15 + export * from '#/components/Dialog/types' 16 + export {Input} from '#/components/forms/TextField' 17 + 18 + const stopPropagation = (e: any) => e.stopPropagation() 19 + 20 + export function Outer({ 21 + control, 22 + onClose, 23 + children, 24 + }: React.PropsWithChildren<DialogOuterProps>) { 25 + const {_} = useLingui() 26 + const t = useTheme() 27 + const {gtMobile} = useBreakpoints() 28 + const [isOpen, setIsOpen] = React.useState(false) 29 + const [isVisible, setIsVisible] = React.useState(true) 30 + 31 + const open = React.useCallback(() => { 32 + setIsOpen(true) 33 + }, [setIsOpen]) 34 + 35 + const close = React.useCallback(async () => { 36 + setIsVisible(false) 37 + await new Promise(resolve => setTimeout(resolve, 150)) 38 + setIsOpen(false) 39 + setIsVisible(true) 40 + onClose?.() 41 + }, [onClose, setIsOpen]) 42 + 43 + useImperativeHandle( 44 + control.ref, 45 + () => ({ 46 + open, 47 + close, 48 + }), 49 + [open, close], 50 + ) 51 + 52 + React.useEffect(() => { 53 + if (!isOpen) return 54 + 55 + function handler(e: KeyboardEvent) { 56 + if (e.key === 'Escape') close() 57 + } 58 + 59 + document.addEventListener('keydown', handler) 60 + 61 + return () => document.removeEventListener('keydown', handler) 62 + }, [isOpen, close]) 63 + 64 + const context = React.useMemo( 65 + () => ({ 66 + close, 67 + }), 68 + [close], 69 + ) 70 + 71 + return ( 72 + <> 73 + {isOpen && ( 74 + <Portal> 75 + <Context.Provider value={context}> 76 + <TouchableWithoutFeedback 77 + accessibilityHint={undefined} 78 + accessibilityLabel={_(msg`Close active dialog`)} 79 + onPress={close}> 80 + <View 81 + style={[ 82 + web(a.fixed), 83 + a.inset_0, 84 + a.z_10, 85 + a.align_center, 86 + gtMobile ? a.p_lg : a.p_md, 87 + {overflowY: 'auto'}, 88 + ]}> 89 + {isVisible && ( 90 + <Animated.View 91 + entering={FadeIn.duration(150)} 92 + // exiting={FadeOut.duration(150)} 93 + style={[ 94 + web(a.fixed), 95 + a.inset_0, 96 + {opacity: 0.5, backgroundColor: t.palette.black}, 97 + ]} 98 + /> 99 + )} 100 + 101 + <View 102 + style={[ 103 + a.w_full, 104 + a.z_20, 105 + a.justify_center, 106 + a.align_center, 107 + { 108 + minHeight: web('calc(90vh - 36px)') || undefined, 109 + }, 110 + ]}> 111 + {isVisible ? children : null} 112 + </View> 113 + </View> 114 + </TouchableWithoutFeedback> 115 + </Context.Provider> 116 + </Portal> 117 + )} 118 + </> 119 + ) 120 + } 121 + 122 + export function Inner({ 123 + children, 124 + style, 125 + label, 126 + accessibilityLabelledBy, 127 + accessibilityDescribedBy, 128 + }: DialogInnerProps) { 129 + const t = useTheme() 130 + const {gtMobile} = useBreakpoints() 131 + return ( 132 + <FocusScope loop enabled trapped> 133 + <Animated.View 134 + role="dialog" 135 + aria-role="dialog" 136 + aria-label={label} 137 + aria-labelledby={accessibilityLabelledBy} 138 + aria-describedby={accessibilityDescribedBy} 139 + // @ts-ignore web only -prf 140 + onClick={stopPropagation} 141 + onStartShouldSetResponder={_ => true} 142 + onTouchEnd={stopPropagation} 143 + entering={FadeInDown.duration(100)} 144 + // exiting={FadeOut.duration(100)} 145 + style={[ 146 + a.relative, 147 + a.rounded_md, 148 + a.w_full, 149 + a.border, 150 + gtMobile ? a.p_xl : a.p_lg, 151 + t.atoms.bg, 152 + { 153 + maxWidth: 600, 154 + borderColor: t.palette.contrast_200, 155 + shadowColor: t.palette.black, 156 + shadowOpacity: t.name === 'light' ? 0.1 : 0.4, 157 + shadowRadius: 30, 158 + }, 159 + ...(Array.isArray(style) ? style : [style || {}]), 160 + ]}> 161 + {children} 162 + </Animated.View> 163 + </FocusScope> 164 + ) 165 + } 166 + 167 + export const ScrollableInner = Inner 168 + 169 + export function Handle() { 170 + return null 171 + } 172 + 173 + /** 174 + * TODO(eric) unused rn 175 + */ 176 + // export function Close() { 177 + // const {_} = useLingui() 178 + // const t = useTheme() 179 + // const {close} = useDialogContext() 180 + // return ( 181 + // <View 182 + // style={[ 183 + // a.absolute, 184 + // a.z_10, 185 + // { 186 + // top: a.pt_lg.paddingTop, 187 + // right: a.pr_lg.paddingRight, 188 + // }, 189 + // ]}> 190 + // <Button onPress={close} label={_(msg`Close active dialog`)}> 191 + // </Button> 192 + // </View> 193 + // ) 194 + // }
+43
src/components/Dialog/types.ts
··· 1 + import React from 'react' 2 + import type {ViewStyle, AccessibilityProps} from 'react-native' 3 + import {BottomSheetProps} from '@gorhom/bottom-sheet' 4 + 5 + type A11yProps = Required<AccessibilityProps> 6 + 7 + export type DialogContextProps = { 8 + close: () => void 9 + } 10 + 11 + export type DialogControlProps = { 12 + open: (index?: number) => void 13 + close: () => void 14 + } 15 + 16 + export type DialogOuterProps = { 17 + control: { 18 + ref: React.RefObject<DialogControlProps> 19 + open: (index?: number) => void 20 + close: () => void 21 + } 22 + onClose?: () => void 23 + nativeOptions?: { 24 + sheet?: Omit<BottomSheetProps, 'children'> 25 + } 26 + webOptions?: {} 27 + } 28 + 29 + type DialogInnerPropsBase<T> = React.PropsWithChildren<{ 30 + style?: ViewStyle 31 + }> & 32 + T 33 + export type DialogInnerProps = 34 + | DialogInnerPropsBase<{ 35 + label?: undefined 36 + accessibilityLabelledBy: A11yProps['aria-labelledby'] 37 + accessibilityDescribedBy: string 38 + }> 39 + | DialogInnerPropsBase<{ 40 + label: string 41 + accessibilityLabelledBy?: undefined 42 + accessibilityDescribedBy?: undefined 43 + }>
+191
src/components/Link.tsx
··· 1 + import React from 'react' 2 + import { 3 + Text, 4 + TextStyle, 5 + StyleProp, 6 + GestureResponderEvent, 7 + Linking, 8 + } from 'react-native' 9 + import { 10 + useLinkProps, 11 + useNavigation, 12 + StackActions, 13 + } from '@react-navigation/native' 14 + import {sanitizeUrl} from '@braintree/sanitize-url' 15 + 16 + import {isWeb} from '#/platform/detection' 17 + import {useTheme, web, flatten} from '#/alf' 18 + import {Button, ButtonProps, useButtonContext} from '#/components/Button' 19 + import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types' 20 + import { 21 + convertBskyAppUrlIfNeeded, 22 + isExternalUrl, 23 + linkRequiresWarning, 24 + } from '#/lib/strings/url-helpers' 25 + import {useModalControls} from '#/state/modals' 26 + import {router} from '#/routes' 27 + 28 + export type LinkProps = Omit< 29 + ButtonProps, 30 + 'style' | 'onPress' | 'disabled' | 'label' 31 + > & { 32 + /** 33 + * `TextStyle` to apply to the anchor element itself. Does not apply to any children. 34 + */ 35 + style?: StyleProp<TextStyle> 36 + /** 37 + * The React Navigation `StackAction` to perform when the link is pressed. 38 + */ 39 + action?: 'push' | 'replace' | 'navigate' 40 + /** 41 + * If true, will warn the user if the link text does not match the href. Only 42 + * works for Links with children that are strings i.e. text links. 43 + */ 44 + warnOnMismatchingTextChild?: boolean 45 + label?: ButtonProps['label'] 46 + } & Pick<Parameters<typeof useLinkProps<AllNavigatorParams>>[0], 'to'> 47 + 48 + /** 49 + * A interactive element that renders as a `<a>` tag on the web. On mobile it 50 + * will translate the `href` to navigator screens and params and dispatch a 51 + * navigation action. 52 + * 53 + * Intended to behave as a web anchor tag. For more complex routing, use a 54 + * `Button`. 55 + */ 56 + export function Link({ 57 + children, 58 + to, 59 + action = 'push', 60 + warnOnMismatchingTextChild, 61 + style, 62 + ...rest 63 + }: LinkProps) { 64 + const navigation = useNavigation<NavigationProp>() 65 + const {href} = useLinkProps<AllNavigatorParams>({ 66 + to: 67 + typeof to === 'string' ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) : to, 68 + }) 69 + const isExternal = isExternalUrl(href) 70 + const {openModal, closeModal} = useModalControls() 71 + const onPress = React.useCallback( 72 + (e: GestureResponderEvent) => { 73 + const stringChildren = typeof children === 'string' ? children : '' 74 + const requiresWarning = Boolean( 75 + warnOnMismatchingTextChild && 76 + stringChildren && 77 + isExternal && 78 + linkRequiresWarning(href, stringChildren), 79 + ) 80 + 81 + if (requiresWarning) { 82 + e.preventDefault() 83 + 84 + openModal({ 85 + name: 'link-warning', 86 + text: stringChildren, 87 + href: href, 88 + }) 89 + } else { 90 + e.preventDefault() 91 + 92 + if (isExternal) { 93 + Linking.openURL(href) 94 + } else { 95 + /** 96 + * A `GestureResponderEvent`, but cast to `any` to avoid using a bunch 97 + * of @ts-ignore below. 98 + */ 99 + const event = e as any 100 + const isMiddleClick = isWeb && event.button === 1 101 + const isMetaKey = 102 + isWeb && 103 + (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) 104 + const shouldOpenInNewTab = isMetaKey || isMiddleClick 105 + 106 + if ( 107 + shouldOpenInNewTab || 108 + href.startsWith('http') || 109 + href.startsWith('mailto') 110 + ) { 111 + Linking.openURL(href) 112 + } else { 113 + closeModal() // close any active modals 114 + 115 + if (action === 'push') { 116 + navigation.dispatch(StackActions.push(...router.matchPath(href))) 117 + } else if (action === 'replace') { 118 + navigation.dispatch( 119 + StackActions.replace(...router.matchPath(href)), 120 + ) 121 + } else if (action === 'navigate') { 122 + // @ts-ignore 123 + navigation.navigate(...router.matchPath(href)) 124 + } else { 125 + throw Error('Unsupported navigator action.') 126 + } 127 + } 128 + } 129 + } 130 + }, 131 + [ 132 + href, 133 + isExternal, 134 + warnOnMismatchingTextChild, 135 + navigation, 136 + action, 137 + children, 138 + closeModal, 139 + openModal, 140 + ], 141 + ) 142 + 143 + return ( 144 + <Button 145 + label={href} 146 + {...rest} 147 + role="link" 148 + accessibilityRole="link" 149 + href={href} 150 + onPress={onPress} 151 + {...web({ 152 + hrefAttrs: { 153 + target: isExternal ? 'blank' : undefined, 154 + rel: isExternal ? 'noopener noreferrer' : undefined, 155 + }, 156 + dataSet: { 157 + // default to no underline, apply this ourselves 158 + noUnderline: '1', 159 + }, 160 + })}> 161 + {typeof children === 'string' ? ( 162 + <LinkText style={style}>{children}</LinkText> 163 + ) : ( 164 + children 165 + )} 166 + </Button> 167 + ) 168 + } 169 + 170 + function LinkText({ 171 + children, 172 + style, 173 + }: React.PropsWithChildren<{ 174 + style?: StyleProp<TextStyle> 175 + }>) { 176 + const t = useTheme() 177 + const {hovered} = useButtonContext() 178 + return ( 179 + <Text 180 + style={[ 181 + {color: t.palette.primary_500}, 182 + hovered && { 183 + textDecorationLine: 'underline', 184 + textDecorationColor: t.palette.primary_500, 185 + }, 186 + flatten(style), 187 + ]}> 188 + {children as string} 189 + </Text> 190 + ) 191 + }
+56
src/components/Portal.tsx
··· 1 + import React from 'react' 2 + 3 + type Component = React.ReactElement 4 + 5 + type ContextType = { 6 + outlet: Component | null 7 + append(id: string, component: Component): void 8 + remove(id: string): void 9 + } 10 + 11 + type ComponentMap = { 12 + [id: string]: Component 13 + } 14 + 15 + export const Context = React.createContext<ContextType>({ 16 + outlet: null, 17 + append: () => {}, 18 + remove: () => {}, 19 + }) 20 + 21 + export function Provider(props: React.PropsWithChildren<{}>) { 22 + const map = React.useRef<ComponentMap>({}) 23 + const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null) 24 + 25 + const append = React.useCallback<ContextType['append']>((id, component) => { 26 + if (map.current[id]) return 27 + map.current[id] = <React.Fragment key={id}>{component}</React.Fragment> 28 + setOutlet(<>{Object.values(map.current)}</>) 29 + }, []) 30 + 31 + const remove = React.useCallback<ContextType['remove']>(id => { 32 + delete map.current[id] 33 + setOutlet(<>{Object.values(map.current)}</>) 34 + }, []) 35 + 36 + return ( 37 + <Context.Provider value={{outlet, append, remove}}> 38 + {props.children} 39 + </Context.Provider> 40 + ) 41 + } 42 + 43 + export function Outlet() { 44 + const ctx = React.useContext(Context) 45 + return ctx.outlet 46 + } 47 + 48 + export function Portal({children}: React.PropsWithChildren<{}>) { 49 + const {append, remove} = React.useContext(Context) 50 + const id = React.useId() 51 + React.useEffect(() => { 52 + append(id, children as Component) 53 + return () => remove(id) 54 + }, [id, children, append, remove]) 55 + return null 56 + }
+119
src/components/Prompt.tsx
··· 1 + import React from 'react' 2 + import {View, PressableProps} from 'react-native' 3 + import {msg} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + 6 + import {useTheme, atoms as a} from '#/alf' 7 + import {H4, P} from '#/components/Typography' 8 + import {Button} from '#/components/Button' 9 + 10 + import * as Dialog from '#/components/Dialog' 11 + 12 + export {useDialogControl as usePromptControl} from '#/components/Dialog' 13 + 14 + const Context = React.createContext<{ 15 + titleId: string 16 + descriptionId: string 17 + }>({ 18 + titleId: '', 19 + descriptionId: '', 20 + }) 21 + 22 + export function Outer({ 23 + children, 24 + control, 25 + }: React.PropsWithChildren<{ 26 + control: Dialog.DialogOuterProps['control'] 27 + }>) { 28 + const titleId = React.useId() 29 + const descriptionId = React.useId() 30 + 31 + const context = React.useMemo( 32 + () => ({titleId, descriptionId}), 33 + [titleId, descriptionId], 34 + ) 35 + 36 + return ( 37 + <Dialog.Outer control={control}> 38 + <Context.Provider value={context}> 39 + <Dialog.Handle /> 40 + 41 + <Dialog.Inner 42 + accessibilityLabelledBy={titleId} 43 + accessibilityDescribedBy={descriptionId} 44 + style={{width: 'auto', maxWidth: 400}}> 45 + {children} 46 + </Dialog.Inner> 47 + </Context.Provider> 48 + </Dialog.Outer> 49 + ) 50 + } 51 + 52 + export function Title({children}: React.PropsWithChildren<{}>) { 53 + const t = useTheme() 54 + const {titleId} = React.useContext(Context) 55 + return ( 56 + <H4 57 + nativeID={titleId} 58 + style={[a.font_bold, t.atoms.text_contrast_700, a.pb_sm]}> 59 + {children} 60 + </H4> 61 + ) 62 + } 63 + 64 + export function Description({children}: React.PropsWithChildren<{}>) { 65 + const t = useTheme() 66 + const {descriptionId} = React.useContext(Context) 67 + return ( 68 + <P nativeID={descriptionId} style={[t.atoms.text, a.pb_lg]}> 69 + {children} 70 + </P> 71 + ) 72 + } 73 + 74 + export function Actions({children}: React.PropsWithChildren<{}>) { 75 + return ( 76 + <View style={[a.w_full, a.flex_row, a.gap_sm, a.justify_end]}> 77 + {children} 78 + </View> 79 + ) 80 + } 81 + 82 + export function Cancel({ 83 + children, 84 + }: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) { 85 + const {_} = useLingui() 86 + const {close} = Dialog.useDialogContext() 87 + return ( 88 + <Button 89 + variant="solid" 90 + color="secondary" 91 + size="small" 92 + label={_(msg`Cancel`)} 93 + onPress={close}> 94 + {children} 95 + </Button> 96 + ) 97 + } 98 + 99 + export function Action({ 100 + children, 101 + onPress, 102 + }: React.PropsWithChildren<{onPress?: () => void}>) { 103 + const {_} = useLingui() 104 + const {close} = Dialog.useDialogContext() 105 + const handleOnPress = React.useCallback(() => { 106 + close() 107 + onPress?.() 108 + }, [close, onPress]) 109 + return ( 110 + <Button 111 + variant="solid" 112 + color="primary" 113 + size="small" 114 + label={_(msg`Confirm`)} 115 + onPress={handleOnPress}> 116 + {children} 117 + </Button> 118 + ) 119 + }
+108
src/components/forms/DateField/index.android.tsx
··· 1 + import React from 'react' 2 + import {View, Pressable} from 'react-native' 3 + import DateTimePicker, { 4 + BaseProps as DateTimePickerProps, 5 + } from '@react-native-community/datetimepicker' 6 + 7 + import {useTheme, atoms} from '#/alf' 8 + import {Text} from '#/components/Typography' 9 + import {useInteractionState} from '#/components/hooks/useInteractionState' 10 + import * as TextField from '#/components/forms/TextField' 11 + import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' 12 + 13 + import {DateFieldProps} from '#/components/forms/DateField/types' 14 + import { 15 + localizeDate, 16 + toSimpleDateString, 17 + } from '#/components/forms/DateField/utils' 18 + 19 + export * as utils from '#/components/forms/DateField/utils' 20 + export const Label = TextField.Label 21 + 22 + export function DateField({ 23 + value, 24 + onChangeDate, 25 + label, 26 + isInvalid, 27 + testID, 28 + }: DateFieldProps) { 29 + const t = useTheme() 30 + const [open, setOpen] = React.useState(false) 31 + const { 32 + state: pressed, 33 + onIn: onPressIn, 34 + onOut: onPressOut, 35 + } = useInteractionState() 36 + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 37 + 38 + const {chromeFocus, chromeError, chromeErrorHover} = 39 + TextField.useSharedInputStyles() 40 + 41 + const onChangeInternal = React.useCallback< 42 + Required<DateTimePickerProps>['onChange'] 43 + >( 44 + (_event, date) => { 45 + setOpen(false) 46 + 47 + if (date) { 48 + const formatted = toSimpleDateString(date) 49 + onChangeDate(formatted) 50 + } 51 + }, 52 + [onChangeDate, setOpen], 53 + ) 54 + 55 + return ( 56 + <View style={[atoms.relative, atoms.w_full]}> 57 + <Pressable 58 + aria-label={label} 59 + accessibilityLabel={label} 60 + accessibilityHint={undefined} 61 + onPress={() => setOpen(true)} 62 + onPressIn={onPressIn} 63 + onPressOut={onPressOut} 64 + onFocus={onFocus} 65 + onBlur={onBlur} 66 + style={[ 67 + { 68 + paddingTop: 16, 69 + paddingBottom: 16, 70 + borderColor: 'transparent', 71 + borderWidth: 2, 72 + }, 73 + atoms.flex_row, 74 + atoms.flex_1, 75 + atoms.w_full, 76 + atoms.px_lg, 77 + atoms.rounded_sm, 78 + t.atoms.bg_contrast_50, 79 + focused || pressed ? chromeFocus : {}, 80 + isInvalid ? chromeError : {}, 81 + isInvalid && (focused || pressed) ? chromeErrorHover : {}, 82 + ]}> 83 + <TextField.Icon icon={CalendarDays} /> 84 + 85 + <Text 86 + style={[atoms.text_md, atoms.pl_xs, t.atoms.text, {paddingTop: 3}]}> 87 + {localizeDate(value)} 88 + </Text> 89 + </Pressable> 90 + 91 + {open && ( 92 + <DateTimePicker 93 + aria-label={label} 94 + accessibilityLabel={label} 95 + accessibilityHint={undefined} 96 + testID={`${testID}-datepicker`} 97 + mode="date" 98 + timeZoneName={'Etc/UTC'} 99 + display="spinner" 100 + // @ts-ignore applies in iOS only -prf 101 + themeVariant={t.name === 'dark' ? 'dark' : 'light'} 102 + value={new Date(value)} 103 + onChange={onChangeInternal} 104 + /> 105 + )} 106 + </View> 107 + ) 108 + }
+56
src/components/forms/DateField/index.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import DateTimePicker, { 4 + DateTimePickerEvent, 5 + } from '@react-native-community/datetimepicker' 6 + 7 + import {useTheme, atoms} from '#/alf' 8 + import * as TextField from '#/components/forms/TextField' 9 + import {toSimpleDateString} from '#/components/forms/DateField/utils' 10 + import {DateFieldProps} from '#/components/forms/DateField/types' 11 + 12 + export * as utils from '#/components/forms/DateField/utils' 13 + export const Label = TextField.Label 14 + 15 + /** 16 + * Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date 17 + * changes in the same format. 18 + * 19 + * For dates of unknown format, convert with the 20 + * `utils.toSimpleDateString(Date)` export of this file. 21 + */ 22 + export function DateField({ 23 + value, 24 + onChangeDate, 25 + testID, 26 + label, 27 + }: DateFieldProps) { 28 + const t = useTheme() 29 + 30 + const onChangeInternal = React.useCallback( 31 + (event: DateTimePickerEvent, date: Date | undefined) => { 32 + if (date) { 33 + const formatted = toSimpleDateString(date) 34 + onChangeDate(formatted) 35 + } 36 + }, 37 + [onChangeDate], 38 + ) 39 + 40 + return ( 41 + <View style={[atoms.relative, atoms.w_full]}> 42 + <DateTimePicker 43 + aria-label={label} 44 + accessibilityLabel={label} 45 + accessibilityHint={undefined} 46 + testID={`${testID}-datepicker`} 47 + mode="date" 48 + timeZoneName={'Etc/UTC'} 49 + display="spinner" 50 + themeVariant={t.name === 'dark' ? 'dark' : 'light'} 51 + value={new Date(value)} 52 + onChange={onChangeInternal} 53 + /> 54 + </View> 55 + ) 56 + }
+64
src/components/forms/DateField/index.web.tsx
··· 1 + import React from 'react' 2 + import {TextInput, TextInputProps, StyleSheet} from 'react-native' 3 + // @ts-ignore 4 + import {unstable_createElement} from 'react-native-web' 5 + 6 + import * as TextField from '#/components/forms/TextField' 7 + import {toSimpleDateString} from '#/components/forms/DateField/utils' 8 + import {DateFieldProps} from '#/components/forms/DateField/types' 9 + 10 + export * as utils from '#/components/forms/DateField/utils' 11 + export const Label = TextField.Label 12 + 13 + const InputBase = React.forwardRef<HTMLInputElement, TextInputProps>( 14 + ({style, ...props}, ref) => { 15 + return unstable_createElement('input', { 16 + ...props, 17 + ref, 18 + type: 'date', 19 + style: [ 20 + StyleSheet.flatten(style), 21 + { 22 + background: 'transparent', 23 + border: 0, 24 + }, 25 + ], 26 + }) 27 + }, 28 + ) 29 + 30 + InputBase.displayName = 'InputBase' 31 + 32 + const Input = TextField.createInput(InputBase as unknown as typeof TextInput) 33 + 34 + export function DateField({ 35 + value, 36 + onChangeDate, 37 + label, 38 + isInvalid, 39 + testID, 40 + }: DateFieldProps) { 41 + const handleOnChange = React.useCallback( 42 + (e: any) => { 43 + const date = e.target.valueAsDate || e.target.value 44 + 45 + if (date) { 46 + const formatted = toSimpleDateString(date) 47 + onChangeDate(formatted) 48 + } 49 + }, 50 + [onChangeDate], 51 + ) 52 + 53 + return ( 54 + <TextField.Root isInvalid={isInvalid}> 55 + <Input 56 + value={value} 57 + label={label} 58 + onChange={handleOnChange} 59 + onChangeText={() => {}} 60 + testID={testID} 61 + /> 62 + </TextField.Root> 63 + ) 64 + }
+7
src/components/forms/DateField/types.ts
··· 1 + export type DateFieldProps = { 2 + value: string 3 + onChangeDate: (date: string) => void 4 + label: string 5 + isInvalid?: boolean 6 + testID?: string 7 + }
+16
src/components/forms/DateField/utils.ts
··· 1 + import {getLocales} from 'expo-localization' 2 + 3 + const LOCALE = getLocales()[0] 4 + 5 + // we need the date in the form yyyy-MM-dd to pass to the input 6 + export function toSimpleDateString(date: Date | string): string { 7 + const _date = typeof date === 'string' ? new Date(date) : date 8 + return _date.toISOString().split('T')[0] 9 + } 10 + 11 + export function localizeDate(date: Date | string): string { 12 + const _date = typeof date === 'string' ? new Date(date) : date 13 + return new Intl.DateTimeFormat(LOCALE.languageTag, { 14 + timeZone: 'UTC', 15 + }).format(_date) 16 + }
+43
src/components/forms/InputGroup.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + 4 + import {atoms, useTheme} from '#/alf' 5 + 6 + /** 7 + * NOT FINISHED, just here as a reference 8 + */ 9 + export function InputGroup(props: React.PropsWithChildren<{}>) { 10 + const t = useTheme() 11 + const children = React.Children.toArray(props.children) 12 + const total = children.length 13 + return ( 14 + <View style={[atoms.w_full]}> 15 + {children.map((child, i) => { 16 + return React.isValidElement(child) ? ( 17 + <React.Fragment key={i}> 18 + {i > 0 ? ( 19 + <View 20 + style={[atoms.border_b, {borderColor: t.palette.contrast_500}]} 21 + /> 22 + ) : null} 23 + {React.cloneElement(child, { 24 + // @ts-ignore 25 + style: [ 26 + ...(Array.isArray(child.props?.style) 27 + ? child.props.style 28 + : [child.props.style || {}]), 29 + { 30 + borderTopLeftRadius: i > 0 ? 0 : undefined, 31 + borderTopRightRadius: i > 0 ? 0 : undefined, 32 + borderBottomLeftRadius: i < total - 1 ? 0 : undefined, 33 + borderBottomRightRadius: i < total - 1 ? 0 : undefined, 34 + borderBottomWidth: i < total - 1 ? 0 : undefined, 35 + }, 36 + ], 37 + })} 38 + </React.Fragment> 39 + ) : null 40 + })} 41 + </View> 42 + ) 43 + }
+334
src/components/forms/TextField.tsx
··· 1 + import React from 'react' 2 + import { 3 + View, 4 + TextInput, 5 + TextInputProps, 6 + TextStyle, 7 + ViewStyle, 8 + Pressable, 9 + StyleSheet, 10 + AccessibilityProps, 11 + } from 'react-native' 12 + 13 + import {HITSLOP_20} from 'lib/constants' 14 + import {isWeb} from '#/platform/detection' 15 + import {useTheme, atoms as a, web, tokens, android} from '#/alf' 16 + import {Text} from '#/components/Typography' 17 + import {useInteractionState} from '#/components/hooks/useInteractionState' 18 + import {Props as SVGIconProps} from '#/components/icons/common' 19 + 20 + const Context = React.createContext<{ 21 + inputRef: React.RefObject<TextInput> | null 22 + isInvalid: boolean 23 + hovered: boolean 24 + onHoverIn: () => void 25 + onHoverOut: () => void 26 + focused: boolean 27 + onFocus: () => void 28 + onBlur: () => void 29 + }>({ 30 + inputRef: null, 31 + isInvalid: false, 32 + hovered: false, 33 + onHoverIn: () => {}, 34 + onHoverOut: () => {}, 35 + focused: false, 36 + onFocus: () => {}, 37 + onBlur: () => {}, 38 + }) 39 + 40 + export type RootProps = React.PropsWithChildren<{isInvalid?: boolean}> 41 + 42 + export function Root({children, isInvalid = false}: RootProps) { 43 + const inputRef = React.useRef<TextInput>(null) 44 + const rootRef = React.useRef<View>(null) 45 + const { 46 + state: hovered, 47 + onIn: onHoverIn, 48 + onOut: onHoverOut, 49 + } = useInteractionState() 50 + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 51 + 52 + const context = React.useMemo( 53 + () => ({ 54 + inputRef, 55 + hovered, 56 + onHoverIn, 57 + onHoverOut, 58 + focused, 59 + onFocus, 60 + onBlur, 61 + isInvalid, 62 + }), 63 + [ 64 + inputRef, 65 + hovered, 66 + onHoverIn, 67 + onHoverOut, 68 + focused, 69 + onFocus, 70 + onBlur, 71 + isInvalid, 72 + ], 73 + ) 74 + 75 + React.useLayoutEffect(() => { 76 + const root = rootRef.current 77 + if (!root || !isWeb) return 78 + // @ts-ignore web only 79 + root.tabIndex = -1 80 + }, []) 81 + 82 + return ( 83 + <Context.Provider value={context}> 84 + <Pressable 85 + accessibilityRole="button" 86 + ref={rootRef} 87 + role="none" 88 + style={[ 89 + a.flex_row, 90 + a.align_center, 91 + a.relative, 92 + a.w_full, 93 + a.px_md, 94 + { 95 + paddingVertical: 14, 96 + }, 97 + ]} 98 + // onPressIn/out don't work on android web 99 + onPress={() => inputRef.current?.focus()} 100 + onHoverIn={onHoverIn} 101 + onHoverOut={onHoverOut}> 102 + {children} 103 + </Pressable> 104 + </Context.Provider> 105 + ) 106 + } 107 + 108 + export function useSharedInputStyles() { 109 + const t = useTheme() 110 + return React.useMemo(() => { 111 + const hover: ViewStyle[] = [ 112 + { 113 + borderColor: t.palette.contrast_100, 114 + }, 115 + ] 116 + const focus: ViewStyle[] = [ 117 + { 118 + backgroundColor: t.palette.contrast_50, 119 + borderColor: t.palette.primary_500, 120 + }, 121 + ] 122 + const error: ViewStyle[] = [ 123 + { 124 + backgroundColor: 125 + t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, 126 + borderColor: 127 + t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800, 128 + }, 129 + ] 130 + const errorHover: ViewStyle[] = [ 131 + { 132 + backgroundColor: 133 + t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, 134 + borderColor: tokens.color.red_500, 135 + }, 136 + ] 137 + 138 + return { 139 + chromeHover: StyleSheet.flatten(hover), 140 + chromeFocus: StyleSheet.flatten(focus), 141 + chromeError: StyleSheet.flatten(error), 142 + chromeErrorHover: StyleSheet.flatten(errorHover), 143 + } 144 + }, [t]) 145 + } 146 + 147 + export type InputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & { 148 + label: string 149 + value: string 150 + onChangeText: (value: string) => void 151 + isInvalid?: boolean 152 + } 153 + 154 + export function createInput(Component: typeof TextInput) { 155 + return function Input({ 156 + label, 157 + placeholder, 158 + value, 159 + onChangeText, 160 + isInvalid, 161 + ...rest 162 + }: InputProps) { 163 + const t = useTheme() 164 + const ctx = React.useContext(Context) 165 + const withinRoot = Boolean(ctx.inputRef) 166 + 167 + const {chromeHover, chromeFocus, chromeError, chromeErrorHover} = 168 + useSharedInputStyles() 169 + 170 + if (!withinRoot) { 171 + return ( 172 + <Root isInvalid={isInvalid}> 173 + <Input 174 + label={label} 175 + placeholder={placeholder} 176 + value={value} 177 + onChangeText={onChangeText} 178 + isInvalid={isInvalid} 179 + {...rest} 180 + /> 181 + </Root> 182 + ) 183 + } 184 + 185 + return ( 186 + <> 187 + <Component 188 + accessibilityHint={undefined} 189 + {...rest} 190 + aria-label={label} 191 + accessibilityLabel={label} 192 + ref={ctx.inputRef} 193 + value={value} 194 + onChangeText={onChangeText} 195 + onFocus={ctx.onFocus} 196 + onBlur={ctx.onBlur} 197 + placeholder={placeholder || label} 198 + placeholderTextColor={t.palette.contrast_500} 199 + hitSlop={HITSLOP_20} 200 + style={[ 201 + a.relative, 202 + a.z_20, 203 + a.flex_1, 204 + a.text_md, 205 + t.atoms.text, 206 + a.px_xs, 207 + android({ 208 + paddingBottom: 2, 209 + }), 210 + { 211 + lineHeight: a.text_md.lineHeight * 1.1875, 212 + textAlignVertical: rest.multiline ? 'top' : undefined, 213 + minHeight: rest.multiline ? 60 : undefined, 214 + }, 215 + ]} 216 + /> 217 + 218 + <View 219 + style={[ 220 + a.z_10, 221 + a.absolute, 222 + a.inset_0, 223 + a.rounded_sm, 224 + t.atoms.bg_contrast_25, 225 + {borderColor: 'transparent', borderWidth: 2}, 226 + ctx.hovered ? chromeHover : {}, 227 + ctx.focused ? chromeFocus : {}, 228 + ctx.isInvalid || isInvalid ? chromeError : {}, 229 + (ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused) 230 + ? chromeErrorHover 231 + : {}, 232 + ]} 233 + /> 234 + </> 235 + ) 236 + } 237 + } 238 + 239 + export const Input = createInput(TextInput) 240 + 241 + export function Label({children}: React.PropsWithChildren<{}>) { 242 + const t = useTheme() 243 + return ( 244 + <Text style={[a.text_sm, a.font_bold, t.atoms.text_contrast_600, a.mb_sm]}> 245 + {children} 246 + </Text> 247 + ) 248 + } 249 + 250 + export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) { 251 + const t = useTheme() 252 + const ctx = React.useContext(Context) 253 + const {hover, focus, errorHover, errorFocus} = React.useMemo(() => { 254 + const hover: TextStyle[] = [ 255 + { 256 + color: t.palette.contrast_800, 257 + }, 258 + ] 259 + const focus: TextStyle[] = [ 260 + { 261 + color: t.palette.primary_500, 262 + }, 263 + ] 264 + const errorHover: TextStyle[] = [ 265 + { 266 + color: t.palette.negative_500, 267 + }, 268 + ] 269 + const errorFocus: TextStyle[] = [ 270 + { 271 + color: t.palette.negative_500, 272 + }, 273 + ] 274 + 275 + return { 276 + hover, 277 + focus, 278 + errorHover, 279 + errorFocus, 280 + } 281 + }, [t]) 282 + 283 + return ( 284 + <View style={[a.z_20, a.pr_xs]}> 285 + <Comp 286 + size="md" 287 + style={[ 288 + {color: t.palette.contrast_500, pointerEvents: 'none'}, 289 + ctx.hovered ? hover : {}, 290 + ctx.focused ? focus : {}, 291 + ctx.isInvalid && ctx.hovered ? errorHover : {}, 292 + ctx.isInvalid && ctx.focused ? errorFocus : {}, 293 + ]} 294 + /> 295 + </View> 296 + ) 297 + } 298 + 299 + export function Suffix({ 300 + children, 301 + label, 302 + accessibilityHint, 303 + }: React.PropsWithChildren<{ 304 + label: string 305 + accessibilityHint?: AccessibilityProps['accessibilityHint'] 306 + }>) { 307 + const t = useTheme() 308 + const ctx = React.useContext(Context) 309 + return ( 310 + <Text 311 + aria-label={label} 312 + accessibilityLabel={label} 313 + accessibilityHint={accessibilityHint} 314 + style={[ 315 + a.z_20, 316 + a.pr_sm, 317 + a.text_md, 318 + t.atoms.text_contrast_400, 319 + { 320 + pointerEvents: 'none', 321 + }, 322 + web({ 323 + marginTop: -2, 324 + }), 325 + ctx.hovered || ctx.focused 326 + ? { 327 + color: t.palette.contrast_800, 328 + } 329 + : {}, 330 + ]}> 331 + {children} 332 + </Text> 333 + ) 334 + }
+473
src/components/forms/Toggle.tsx
··· 1 + import React from 'react' 2 + import {Pressable, View, ViewStyle} from 'react-native' 3 + 4 + import {HITSLOP_10} from 'lib/constants' 5 + import {useTheme, atoms as a, web, native} from '#/alf' 6 + import {Text} from '#/components/Typography' 7 + import {useInteractionState} from '#/components/hooks/useInteractionState' 8 + 9 + export type ItemState = { 10 + name: string 11 + selected: boolean 12 + disabled: boolean 13 + isInvalid: boolean 14 + hovered: boolean 15 + pressed: boolean 16 + focused: boolean 17 + } 18 + 19 + const ItemContext = React.createContext<ItemState>({ 20 + name: '', 21 + selected: false, 22 + disabled: false, 23 + isInvalid: false, 24 + hovered: false, 25 + pressed: false, 26 + focused: false, 27 + }) 28 + 29 + const GroupContext = React.createContext<{ 30 + values: string[] 31 + disabled: boolean 32 + type: 'radio' | 'checkbox' 33 + maxSelectionsReached: boolean 34 + setFieldValue: (props: {name: string; value: boolean}) => void 35 + }>({ 36 + type: 'checkbox', 37 + values: [], 38 + disabled: false, 39 + maxSelectionsReached: false, 40 + setFieldValue: () => {}, 41 + }) 42 + 43 + export type GroupProps = React.PropsWithChildren<{ 44 + type?: 'radio' | 'checkbox' 45 + values: string[] 46 + maxSelections?: number 47 + disabled?: boolean 48 + onChange: (value: string[]) => void 49 + label: string 50 + }> 51 + 52 + export type ItemProps = { 53 + type?: 'radio' | 'checkbox' 54 + name: string 55 + label: string 56 + value?: boolean 57 + disabled?: boolean 58 + onChange?: (selected: boolean) => void 59 + isInvalid?: boolean 60 + style?: (state: ItemState) => ViewStyle 61 + children: ((props: ItemState) => React.ReactNode) | React.ReactNode 62 + } 63 + 64 + export function useItemContext() { 65 + return React.useContext(ItemContext) 66 + } 67 + 68 + export function Group({ 69 + children, 70 + values: providedValues, 71 + onChange, 72 + disabled = false, 73 + type = 'checkbox', 74 + maxSelections, 75 + label, 76 + }: GroupProps) { 77 + const groupRole = type === 'radio' ? 'radiogroup' : undefined 78 + const values = type === 'radio' ? providedValues.slice(0, 1) : providedValues 79 + const [maxReached, setMaxReached] = React.useState(false) 80 + 81 + const setFieldValue = React.useCallback< 82 + (props: {name: string; value: boolean}) => void 83 + >( 84 + ({name, value}) => { 85 + if (type === 'checkbox') { 86 + const pruned = values.filter(v => v !== name) 87 + const next = value ? pruned.concat(name) : pruned 88 + onChange(next) 89 + } else { 90 + onChange([name]) 91 + } 92 + }, 93 + [type, onChange, values], 94 + ) 95 + 96 + React.useEffect(() => { 97 + if (type === 'checkbox') { 98 + if ( 99 + maxSelections && 100 + values.length >= maxSelections && 101 + maxReached === false 102 + ) { 103 + setMaxReached(true) 104 + } else if ( 105 + maxSelections && 106 + values.length < maxSelections && 107 + maxReached === true 108 + ) { 109 + setMaxReached(false) 110 + } 111 + } 112 + }, [type, values.length, maxSelections, maxReached, setMaxReached]) 113 + 114 + const context = React.useMemo( 115 + () => ({ 116 + values, 117 + type, 118 + disabled, 119 + maxSelectionsReached: maxReached, 120 + setFieldValue, 121 + }), 122 + [values, disabled, type, maxReached, setFieldValue], 123 + ) 124 + 125 + return ( 126 + <GroupContext.Provider value={context}> 127 + <View 128 + role={groupRole} 129 + {...(groupRole === 'radiogroup' 130 + ? { 131 + 'aria-label': label, 132 + accessibilityLabel: label, 133 + accessibilityRole: groupRole, 134 + } 135 + : {})}> 136 + {children} 137 + </View> 138 + </GroupContext.Provider> 139 + ) 140 + } 141 + 142 + export function Item({ 143 + children, 144 + name, 145 + value = false, 146 + disabled: itemDisabled = false, 147 + onChange, 148 + isInvalid, 149 + style, 150 + type = 'checkbox', 151 + label, 152 + ...rest 153 + }: ItemProps) { 154 + const { 155 + values: selectedValues, 156 + type: groupType, 157 + disabled: groupDisabled, 158 + setFieldValue, 159 + maxSelectionsReached, 160 + } = React.useContext(GroupContext) 161 + const { 162 + state: hovered, 163 + onIn: onHoverIn, 164 + onOut: onHoverOut, 165 + } = useInteractionState() 166 + const { 167 + state: pressed, 168 + onIn: onPressIn, 169 + onOut: onPressOut, 170 + } = useInteractionState() 171 + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 172 + 173 + const role = groupType === 'radio' ? 'radio' : type 174 + const selected = selectedValues.includes(name) || !!value 175 + const disabled = 176 + groupDisabled || itemDisabled || (!selected && maxSelectionsReached) 177 + 178 + const onPress = React.useCallback(() => { 179 + const next = !selected 180 + setFieldValue({name, value: next}) 181 + onChange?.(next) 182 + }, [name, selected, onChange, setFieldValue]) 183 + 184 + const state = React.useMemo( 185 + () => ({ 186 + name, 187 + selected, 188 + disabled: disabled ?? false, 189 + isInvalid: isInvalid ?? false, 190 + hovered, 191 + pressed, 192 + focused, 193 + }), 194 + [name, selected, disabled, hovered, pressed, focused, isInvalid], 195 + ) 196 + 197 + return ( 198 + <ItemContext.Provider value={state}> 199 + <Pressable 200 + accessibilityHint={undefined} // optional 201 + hitSlop={HITSLOP_10} 202 + {...rest} 203 + disabled={disabled} 204 + aria-disabled={disabled ?? false} 205 + aria-checked={selected} 206 + aria-invalid={isInvalid} 207 + aria-label={label} 208 + role={role} 209 + accessibilityRole={role} 210 + accessibilityState={{ 211 + disabled: disabled ?? false, 212 + selected: selected, 213 + }} 214 + accessibilityLabel={label} 215 + onPress={onPress} 216 + onHoverIn={onHoverIn} 217 + onHoverOut={onHoverOut} 218 + onPressIn={onPressIn} 219 + onPressOut={onPressOut} 220 + onFocus={onFocus} 221 + onBlur={onBlur} 222 + style={[ 223 + a.flex_row, 224 + a.align_center, 225 + a.gap_sm, 226 + focused ? web({outline: 'none'}) : {}, 227 + style?.(state), 228 + ]}> 229 + {typeof children === 'function' ? children(state) : children} 230 + </Pressable> 231 + </ItemContext.Provider> 232 + ) 233 + } 234 + 235 + export function Label({children}: React.PropsWithChildren<{}>) { 236 + const t = useTheme() 237 + const {disabled} = useItemContext() 238 + return ( 239 + <Text 240 + style={[ 241 + a.font_bold, 242 + { 243 + userSelect: 'none', 244 + color: disabled ? t.palette.contrast_400 : t.palette.contrast_600, 245 + }, 246 + native({ 247 + paddingTop: 3, 248 + }), 249 + ]}> 250 + {children} 251 + </Text> 252 + ) 253 + } 254 + 255 + // TODO(eric) refactor to memoize styles without knowledge of state 256 + export function createSharedToggleStyles({ 257 + theme: t, 258 + hovered, 259 + focused, 260 + selected, 261 + disabled, 262 + isInvalid, 263 + }: { 264 + theme: ReturnType<typeof useTheme> 265 + selected: boolean 266 + hovered: boolean 267 + focused: boolean 268 + disabled: boolean 269 + isInvalid: boolean 270 + }) { 271 + const base: ViewStyle[] = [] 272 + const baseHover: ViewStyle[] = [] 273 + const indicator: ViewStyle[] = [] 274 + 275 + if (selected) { 276 + base.push({ 277 + backgroundColor: 278 + t.name === 'light' ? t.palette.primary_25 : t.palette.primary_900, 279 + borderColor: t.palette.primary_500, 280 + }) 281 + 282 + if (hovered || focused) { 283 + baseHover.push({ 284 + backgroundColor: 285 + t.name === 'light' ? t.palette.primary_100 : t.palette.primary_800, 286 + borderColor: 287 + t.name === 'light' ? t.palette.primary_600 : t.palette.primary_400, 288 + }) 289 + } 290 + } else { 291 + if (hovered || focused) { 292 + baseHover.push({ 293 + backgroundColor: 294 + t.name === 'light' ? t.palette.contrast_50 : t.palette.contrast_100, 295 + borderColor: t.palette.contrast_500, 296 + }) 297 + } 298 + } 299 + 300 + if (isInvalid) { 301 + base.push({ 302 + backgroundColor: 303 + t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, 304 + borderColor: 305 + t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800, 306 + }) 307 + 308 + if (hovered || focused) { 309 + baseHover.push({ 310 + backgroundColor: 311 + t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, 312 + borderColor: t.palette.negative_500, 313 + }) 314 + } 315 + } 316 + 317 + if (disabled) { 318 + base.push({ 319 + backgroundColor: t.palette.contrast_100, 320 + borderColor: t.palette.contrast_400, 321 + }) 322 + } 323 + 324 + return { 325 + baseStyles: base, 326 + baseHoverStyles: disabled ? [] : baseHover, 327 + indicatorStyles: indicator, 328 + } 329 + } 330 + 331 + export function Checkbox() { 332 + const t = useTheme() 333 + const {selected, hovered, focused, disabled, isInvalid} = useItemContext() 334 + const {baseStyles, baseHoverStyles, indicatorStyles} = 335 + createSharedToggleStyles({ 336 + theme: t, 337 + hovered, 338 + focused, 339 + selected, 340 + disabled, 341 + isInvalid, 342 + }) 343 + return ( 344 + <View 345 + style={[ 346 + a.justify_center, 347 + a.align_center, 348 + a.border, 349 + a.rounded_xs, 350 + t.atoms.border_contrast, 351 + { 352 + height: 20, 353 + width: 20, 354 + }, 355 + baseStyles, 356 + hovered || focused ? baseHoverStyles : {}, 357 + ]}> 358 + {selected ? ( 359 + <View 360 + style={[ 361 + a.absolute, 362 + a.rounded_2xs, 363 + {height: 12, width: 12}, 364 + selected 365 + ? { 366 + backgroundColor: t.palette.primary_500, 367 + } 368 + : {}, 369 + indicatorStyles, 370 + ]} 371 + /> 372 + ) : null} 373 + </View> 374 + ) 375 + } 376 + 377 + export function Switch() { 378 + const t = useTheme() 379 + const {selected, hovered, focused, disabled, isInvalid} = useItemContext() 380 + const {baseStyles, baseHoverStyles, indicatorStyles} = 381 + createSharedToggleStyles({ 382 + theme: t, 383 + hovered, 384 + focused, 385 + selected, 386 + disabled, 387 + isInvalid, 388 + }) 389 + return ( 390 + <View 391 + style={[ 392 + a.relative, 393 + a.border, 394 + a.rounded_full, 395 + t.atoms.bg, 396 + t.atoms.border_contrast, 397 + { 398 + height: 20, 399 + width: 30, 400 + }, 401 + baseStyles, 402 + hovered || focused ? baseHoverStyles : {}, 403 + ]}> 404 + <View 405 + style={[ 406 + a.absolute, 407 + a.rounded_full, 408 + { 409 + height: 12, 410 + width: 12, 411 + top: 3, 412 + left: 3, 413 + backgroundColor: t.palette.contrast_400, 414 + }, 415 + selected 416 + ? { 417 + backgroundColor: t.palette.primary_500, 418 + left: 13, 419 + } 420 + : {}, 421 + indicatorStyles, 422 + ]} 423 + /> 424 + </View> 425 + ) 426 + } 427 + 428 + export function Radio() { 429 + const t = useTheme() 430 + const {selected, hovered, focused, disabled, isInvalid} = 431 + React.useContext(ItemContext) 432 + const {baseStyles, baseHoverStyles, indicatorStyles} = 433 + createSharedToggleStyles({ 434 + theme: t, 435 + hovered, 436 + focused, 437 + selected, 438 + disabled, 439 + isInvalid, 440 + }) 441 + return ( 442 + <View 443 + style={[ 444 + a.justify_center, 445 + a.align_center, 446 + a.border, 447 + a.rounded_full, 448 + t.atoms.border_contrast, 449 + { 450 + height: 20, 451 + width: 20, 452 + }, 453 + baseStyles, 454 + hovered || focused ? baseHoverStyles : {}, 455 + ]}> 456 + {selected ? ( 457 + <View 458 + style={[ 459 + a.absolute, 460 + a.rounded_full, 461 + {height: 12, width: 12}, 462 + selected 463 + ? { 464 + backgroundColor: t.palette.primary_500, 465 + } 466 + : {}, 467 + indicatorStyles, 468 + ]} 469 + /> 470 + ) : null} 471 + </View> 472 + ) 473 + }
+124
src/components/forms/ToggleButton.tsx
··· 1 + import React from 'react' 2 + import {View, AccessibilityProps, TextStyle, ViewStyle} from 'react-native' 3 + 4 + import {atoms as a, useTheme, native} from '#/alf' 5 + import {Text} from '#/components/Typography' 6 + 7 + import * as Toggle from '#/components/forms/Toggle' 8 + 9 + export type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> & 10 + AccessibilityProps & 11 + React.PropsWithChildren<{}> 12 + 13 + export type GroupProps = Omit<Toggle.GroupProps, 'style' | 'type'> & { 14 + multiple?: boolean 15 + } 16 + 17 + export function Group({children, multiple, ...props}: GroupProps) { 18 + const t = useTheme() 19 + return ( 20 + <Toggle.Group type={multiple ? 'checkbox' : 'radio'} {...props}> 21 + <View 22 + style={[ 23 + a.flex_row, 24 + a.border, 25 + a.rounded_sm, 26 + a.overflow_hidden, 27 + t.atoms.border, 28 + ]}> 29 + {children} 30 + </View> 31 + </Toggle.Group> 32 + ) 33 + } 34 + 35 + export function Button({children, ...props}: ItemProps) { 36 + return ( 37 + <Toggle.Item {...props}> 38 + <ButtonInner>{children}</ButtonInner> 39 + </Toggle.Item> 40 + ) 41 + } 42 + 43 + function ButtonInner({children}: React.PropsWithChildren<{}>) { 44 + const t = useTheme() 45 + const state = Toggle.useItemContext() 46 + 47 + const {baseStyles, hoverStyles, activeStyles, textStyles} = 48 + React.useMemo(() => { 49 + const base: ViewStyle[] = [] 50 + const hover: ViewStyle[] = [] 51 + const active: ViewStyle[] = [] 52 + const text: TextStyle[] = [] 53 + 54 + hover.push( 55 + t.name === 'light' ? t.atoms.bg_contrast_100 : t.atoms.bg_contrast_25, 56 + ) 57 + 58 + if (state.selected) { 59 + active.push({ 60 + backgroundColor: t.palette.contrast_800, 61 + }) 62 + text.push(t.atoms.text_inverted) 63 + hover.push({ 64 + backgroundColor: t.palette.contrast_800, 65 + }) 66 + 67 + if (state.disabled) { 68 + active.push({ 69 + backgroundColor: t.palette.contrast_500, 70 + }) 71 + } 72 + } 73 + 74 + if (state.disabled) { 75 + base.push({ 76 + backgroundColor: t.palette.contrast_100, 77 + }) 78 + text.push({ 79 + opacity: 0.5, 80 + }) 81 + } 82 + 83 + return { 84 + baseStyles: base, 85 + hoverStyles: hover, 86 + activeStyles: active, 87 + textStyles: text, 88 + } 89 + }, [t, state]) 90 + 91 + return ( 92 + <View 93 + style={[ 94 + { 95 + borderLeftWidth: 1, 96 + marginLeft: -1, 97 + }, 98 + a.px_lg, 99 + a.py_md, 100 + native({ 101 + paddingTop: 14, 102 + }), 103 + t.atoms.bg, 104 + t.atoms.border, 105 + baseStyles, 106 + activeStyles, 107 + (state.hovered || state.focused || state.pressed) && hoverStyles, 108 + ]}> 109 + {typeof children === 'string' ? ( 110 + <Text 111 + style={[ 112 + a.text_center, 113 + a.font_bold, 114 + t.atoms.text_contrast_500, 115 + textStyles, 116 + ]}> 117 + {children} 118 + </Text> 119 + ) : ( 120 + children 121 + )} 122 + </View> 123 + ) 124 + }
+21
src/components/hooks/useInteractionState.ts
··· 1 + import React from 'react' 2 + 3 + export function useInteractionState() { 4 + const [state, setState] = React.useState(false) 5 + 6 + const onIn = React.useCallback(() => { 7 + setState(true) 8 + }, [setState]) 9 + const onOut = React.useCallback(() => { 10 + setState(false) 11 + }, [setState]) 12 + 13 + return React.useMemo( 14 + () => ({ 15 + state, 16 + onIn, 17 + onOut, 18 + }), 19 + [state, onIn, onOut], 20 + ) 21 + }
+5
src/components/icons/ArrowTopRight.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const ArrowTopRight_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + path: 'M8 6a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v9a1 1 0 1 1-2 0V8.414l-9.793 9.793a1 1 0 0 1-1.414-1.414L15.586 7H9a1 1 0 0 1-1-1Z', 5 + })
+5
src/components/icons/CalendarDays.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const CalendarDays_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + path: 'M4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Zm1 16V9h14v10H5ZM5 7h14V5H5v2Zm3 10.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5ZM17.25 12a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM12 13.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5ZM9.25 12a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM12 17.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z', 5 + })
+5
src/components/icons/ColorPalette.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const ColorPalette_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + path: 'M4 12c0-4.09 3.527-7.5 8-7.5s8 3.41 8 7.5c0 1.579-.419 2.056-.708 2.236-.388.241-1.031.286-2.058.153-.33-.043-.652-.096-.991-.152a65.905 65.905 0 0 0-.531-.087c-.52-.081-1.077-.156-1.61-.164-1.065-.016-2.336.245-2.996 1.567-.418.834-.295 1.67-.078 2.314.18.534.47 1.055.683 1.437v.001l.097.175.01.018C7.432 19.407 4 16.033 4 12Zm8-9.5C6.532 2.5 2 6.7 2 12s4.532 9.5 10 9.5c.401 0 .812-.04 1.166-.193.41-.176.761-.517.866-1.028.085-.416-.03-.796-.118-1.029a5.981 5.981 0 0 0-.351-.73l-.12-.215c-.215-.392-.403-.73-.52-1.078-.13-.387-.111-.614-.029-.78.146-.291.404-.473 1.178-.461.385.005.825.06 1.329.14.15.023.308.05.47.077.36.059.742.122 1.105.17 1.021.132 2.325.213 3.373-.439C21.496 15.22 22 13.874 22 12c0-5.3-4.532-9.5-10-9.5Zm3.5 8.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM9 12.25a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm1.5-2.75a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z', 5 + })
+5
src/components/icons/Globe.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const Globe_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + path: 'M4.062 11h2.961c.103-2.204.545-4.218 1.235-5.77.06-.136.123-.269.188-.399A8.007 8.007 0 0 0 4.062 11ZM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 2c-.227 0-.518.1-.868.432-.354.337-.719.872-1.047 1.61-.561 1.263-.958 2.991-1.06 4.958h5.95c-.102-1.967-.499-3.695-1.06-4.958-.328-.738-.693-1.273-1.047-1.61C12.518 4.099 12.227 4 12 4Zm4.977 7c-.103-2.204-.545-4.218-1.235-5.77a9.78 9.78 0 0 0-.188-.399A8.006 8.006 0 0 1 19.938 11h-2.961Zm-2.003 2H9.026c.101 1.966.498 3.695 1.06 4.958.327.738.692 1.273 1.046 1.61.35.333.641.432.868.432.227 0 .518-.1.868-.432.354-.337.719-.872 1.047-1.61.561-1.263.958-2.991 1.06-4.958Zm.58 6.169c.065-.13.128-.263.188-.399.69-1.552 1.132-3.566 1.235-5.77h2.961a8.006 8.006 0 0 1-4.384 6.169Zm-7.108 0a9.877 9.877 0 0 1-.188-.399c-.69-1.552-1.132-3.566-1.235-5.77H4.062a8.006 8.006 0 0 0 4.384 6.169Z', 5 + })
+48
src/components/icons/TEMPLATE.tsx
··· 1 + import React from 'react' 2 + import Svg, {Path} from 'react-native-svg' 3 + 4 + import {useCommonSVGProps, Props} from '#/components/icons/common' 5 + 6 + export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef( 7 + function LogoImpl(props: Props, ref) { 8 + const {fill, size, style, ...rest} = useCommonSVGProps(props) 9 + 10 + return ( 11 + <Svg 12 + fill="none" 13 + {...rest} 14 + // @ts-ignore it's fiiiiine 15 + ref={ref} 16 + viewBox="0 0 24 24" 17 + width={size} 18 + height={size} 19 + style={[style]}> 20 + <Path 21 + fill={fill} 22 + fillRule="evenodd" 23 + clipRule="evenodd" 24 + d="M4.062 11h2.961c.103-2.204.545-4.218 1.235-5.77.06-.136.123-.269.188-.399A8.007 8.007 0 0 0 4.062 11ZM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 2c-.227 0-.518.1-.868.432-.354.337-.719.872-1.047 1.61-.561 1.263-.958 2.991-1.06 4.958h5.95c-.102-1.967-.499-3.695-1.06-4.958-.328-.738-.693-1.273-1.047-1.61C12.518 4.099 12.227 4 12 4Zm4.977 7c-.103-2.204-.545-4.218-1.235-5.77a9.78 9.78 0 0 0-.188-.399A8.006 8.006 0 0 1 19.938 11h-2.961Zm-2.003 2H9.026c.101 1.966.498 3.695 1.06 4.958.327.738.692 1.273 1.046 1.61.35.333.641.432.868.432.227 0 .518-.1.868-.432.354-.337.719-.872 1.047-1.61.561-1.263.958-2.991 1.06-4.958Zm.58 6.169c.065-.13.128-.263.188-.399.69-1.552 1.132-3.566 1.235-5.77h2.961a8.006 8.006 0 0 1-4.384 6.169Zm-7.108 0a9.877 9.877 0 0 1-.188-.399c-.69-1.552-1.132-3.566-1.235-5.77H4.062a8.006 8.006 0 0 0 4.384 6.169Z" 25 + /> 26 + </Svg> 27 + ) 28 + }, 29 + ) 30 + 31 + export function createSinglePathSVG({path}: {path: string}) { 32 + return React.forwardRef<Svg, Props>(function LogoImpl(props, ref) { 33 + const {fill, size, style, ...rest} = useCommonSVGProps(props) 34 + 35 + return ( 36 + <Svg 37 + fill="none" 38 + {...rest} 39 + ref={ref} 40 + viewBox="0 0 24 24" 41 + width={size} 42 + height={size} 43 + style={[style]}> 44 + <Path fill={fill} fillRule="evenodd" clipRule="evenodd" d={path} /> 45 + </Svg> 46 + ) 47 + }) 48 + }
+32
src/components/icons/common.ts
··· 1 + import {StyleSheet, TextProps} from 'react-native' 2 + import type {SvgProps, PathProps} from 'react-native-svg' 3 + 4 + import {tokens} from '#/alf' 5 + 6 + export type Props = { 7 + fill?: PathProps['fill'] 8 + style?: TextProps['style'] 9 + size?: keyof typeof sizes 10 + } & Omit<SvgProps, 'style' | 'size'> 11 + 12 + export const sizes = { 13 + xs: 12, 14 + sm: 16, 15 + md: 20, 16 + lg: 24, 17 + xl: 28, 18 + } 19 + 20 + export function useCommonSVGProps(props: Props) { 21 + const {fill, size, ...rest} = props 22 + const style = StyleSheet.flatten(rest.style) 23 + const _fill = fill || style?.color || tokens.color.blue_500 24 + const _size = Number(size ? sizes[size] : rest.width || sizes.md) 25 + 26 + return { 27 + fill: _fill, 28 + size: _size, 29 + style, 30 + ...rest, 31 + } 32 + }
+44
src/state/dialogs/index.tsx
··· 1 + import React from 'react' 2 + import {DialogControlProps} from '#/components/Dialog' 3 + 4 + const DialogContext = React.createContext<{ 5 + activeDialogs: React.MutableRefObject< 6 + Map<string, React.MutableRefObject<DialogControlProps>> 7 + > 8 + }>({ 9 + activeDialogs: { 10 + current: new Map(), 11 + }, 12 + }) 13 + 14 + const DialogControlContext = React.createContext<{ 15 + closeAllDialogs(): void 16 + }>({ 17 + closeAllDialogs: () => {}, 18 + }) 19 + 20 + export function useDialogStateContext() { 21 + return React.useContext(DialogContext) 22 + } 23 + 24 + export function useDialogStateControlContext() { 25 + return React.useContext(DialogControlContext) 26 + } 27 + 28 + export function Provider({children}: React.PropsWithChildren<{}>) { 29 + const activeDialogs = React.useRef< 30 + Map<string, React.MutableRefObject<DialogControlProps>> 31 + >(new Map()) 32 + const closeAllDialogs = React.useCallback(() => { 33 + activeDialogs.current.forEach(dialog => dialog.current.close()) 34 + }, []) 35 + const context = React.useMemo(() => ({activeDialogs}), []) 36 + const controls = React.useMemo(() => ({closeAllDialogs}), [closeAllDialogs]) 37 + return ( 38 + <DialogContext.Provider value={context}> 39 + <DialogControlContext.Provider value={controls}> 40 + {children} 41 + </DialogControlContext.Provider> 42 + </DialogContext.Provider> 43 + ) 44 + }
-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 - }
+27 -7
src/view/com/Typography.tsx src/components/Typography.tsx
··· 1 1 import React from 'react' 2 2 import {Text as RNText, TextProps} from 'react-native' 3 - import {useTheme, atoms, web} from '#/alf' 3 + 4 + import {useTheme, atoms, web, flatten} from '#/alf' 4 5 5 6 export function Text({style, ...rest}: TextProps) { 6 7 const t = useTheme() ··· 18 19 <RNText 19 20 {...attr} 20 21 {...rest} 21 - style={[atoms.text_xl, atoms.font_bold, t.atoms.text, style]} 22 + style={[atoms.text_5xl, atoms.font_bold, t.atoms.text, flatten(style)]} 22 23 /> 23 24 ) 24 25 } ··· 34 35 <RNText 35 36 {...attr} 36 37 {...rest} 37 - style={[atoms.text_lg, atoms.font_bold, t.atoms.text, style]} 38 + style={[atoms.text_4xl, atoms.font_bold, t.atoms.text, flatten(style)]} 38 39 /> 39 40 ) 40 41 } ··· 50 51 <RNText 51 52 {...attr} 52 53 {...rest} 53 - style={[atoms.text_md, atoms.font_bold, t.atoms.text, style]} 54 + style={[atoms.text_3xl, atoms.font_bold, t.atoms.text, flatten(style)]} 54 55 /> 55 56 ) 56 57 } ··· 66 67 <RNText 67 68 {...attr} 68 69 {...rest} 69 - style={[atoms.text_sm, atoms.font_bold, t.atoms.text, style]} 70 + style={[atoms.text_2xl, atoms.font_bold, t.atoms.text, flatten(style)]} 70 71 /> 71 72 ) 72 73 } ··· 82 83 <RNText 83 84 {...attr} 84 85 {...rest} 85 - style={[atoms.text_xs, atoms.font_bold, t.atoms.text, style]} 86 + style={[atoms.text_xl, atoms.font_bold, t.atoms.text, flatten(style)]} 86 87 /> 87 88 ) 88 89 } ··· 98 99 <RNText 99 100 {...attr} 100 101 {...rest} 101 - style={[atoms.text_xxs, atoms.font_bold, t.atoms.text, style]} 102 + style={[atoms.text_lg, atoms.font_bold, t.atoms.text, flatten(style)]} 103 + /> 104 + ) 105 + } 106 + 107 + export function P({style, ...rest}: TextProps) { 108 + const t = useTheme() 109 + const attr = 110 + web({ 111 + role: 'paragraph', 112 + }) || {} 113 + const _style = flatten(style) 114 + const lineHeight = 115 + (_style?.lineHeight || atoms.text_md.lineHeight) * 116 + atoms.leading_normal.lineHeight 117 + return ( 118 + <RNText 119 + {...attr} 120 + {...rest} 121 + style={[atoms.text_md, t.atoms.text, _style, {lineHeight}]} 102 122 /> 103 123 ) 104 124 }
+21 -2
src/view/com/pager/FeedsTabBarMobile.tsx
··· 20 20 import {NavigationProp} from 'lib/routes/types' 21 21 import {Logo} from '#/view/icons/Logo' 22 22 23 + import {IS_DEV} from '#/env' 24 + import {atoms} from '#/alf' 25 + import {Link as Link2} from '#/components/Link' 26 + import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette' 27 + 23 28 export function FeedsTabBar( 24 29 props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, 25 30 ) { ··· 68 73 headerHeight.value = e.nativeEvent.layout.height 69 74 }}> 70 75 <View style={[pal.view, styles.topBar]}> 71 - <View style={[pal.view]}> 76 + <View style={[pal.view, {width: 100}]}> 72 77 <TouchableOpacity 73 78 testID="viewHeaderDrawerBtn" 74 79 onPress={onPressAvi} ··· 88 93 <View> 89 94 <Logo width={30} /> 90 95 </View> 91 - <View style={[pal.view, {width: 18}]}> 96 + <View 97 + style={[ 98 + atoms.flex_row, 99 + atoms.justify_end, 100 + atoms.align_center, 101 + atoms.gap_md, 102 + pal.view, 103 + {width: 100}, 104 + ]}> 105 + {IS_DEV && ( 106 + <Link2 to="/sys/debug"> 107 + <ColorPalette size="md" /> 108 + </Link2> 109 + )} 110 + 92 111 {hasSession && ( 93 112 <Link 94 113 testID="viewHeaderHomeFeedPrefsBtn"
+6 -3
src/view/icons/Logo.tsx
··· 1 1 import React from 'react' 2 + import {StyleSheet, TextProps} from 'react-native' 2 3 import Svg, { 3 4 Path, 4 5 Defs, ··· 14 15 15 16 type Props = { 16 17 fill?: PathProps['fill'] 17 - } & SvgProps 18 + style?: TextProps['style'] 19 + } & Omit<SvgProps, 'style'> 18 20 19 21 export const Logo = React.forwardRef(function LogoImpl(props: Props, ref) { 20 22 const {fill, ...rest} = props 21 23 const gradient = fill === 'sky' 22 - const _fill = gradient ? 'url(#sky)' : fill || colors.blue3 24 + const styles = StyleSheet.flatten(props.style) 25 + const _fill = gradient ? 'url(#sky)' : fill || styles?.color || colors.blue3 23 26 // @ts-ignore it's fiiiiine 24 27 const size = parseInt(rest.width || 32) 25 28 return ( ··· 29 32 ref={ref} 30 33 viewBox="0 0 64 57" 31 34 {...rest} 32 - style={{width: size, height: size * ratio}}> 35 + style={[{width: size, height: size * ratio}, styles]}> 33 36 {gradient && ( 34 37 <Defs> 35 38 <LinearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
-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 - }
+25
src/view/screens/Storybook/Breakpoints.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + 4 + import {atoms as a, useTheme, useBreakpoints} from '#/alf' 5 + import {Text, H3} from '#/components/Typography' 6 + 7 + export function Breakpoints() { 8 + const t = useTheme() 9 + const breakpoints = useBreakpoints() 10 + 11 + return ( 12 + <View> 13 + <H3 style={[a.pb_md]}>Breakpoint Debugger</H3> 14 + <Text style={[a.pb_md]}> 15 + Current breakpoint: {!breakpoints.gtMobile && <Text>mobile</Text>} 16 + {breakpoints.gtMobile && !breakpoints.gtTablet && <Text>tablet</Text>} 17 + {breakpoints.gtTablet && <Text>desktop</Text>} 18 + </Text> 19 + <Text 20 + style={[a.p_md, t.atoms.bg_contrast_100, {fontFamily: 'monospace'}]}> 21 + {JSON.stringify(breakpoints, null, 2)} 22 + </Text> 23 + </View> 24 + ) 25 + }
+124
src/view/screens/Storybook/Buttons.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + 4 + import {atoms as a} from '#/alf' 5 + import { 6 + Button, 7 + ButtonVariant, 8 + ButtonColor, 9 + ButtonIcon, 10 + ButtonText, 11 + } from '#/components/Button' 12 + import {H1} from '#/components/Typography' 13 + import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight' 14 + import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' 15 + 16 + export function Buttons() { 17 + return ( 18 + <View style={[a.gap_md]}> 19 + <H1>Buttons</H1> 20 + 21 + <View style={[a.flex_row, a.flex_wrap, a.gap_md, a.align_start]}> 22 + {['primary', 'secondary', 'negative'].map(color => ( 23 + <View key={color} style={[a.gap_md, a.align_start]}> 24 + {['solid', 'outline', 'ghost'].map(variant => ( 25 + <React.Fragment key={variant}> 26 + <Button 27 + variant={variant as ButtonVariant} 28 + color={color as ButtonColor} 29 + size="large" 30 + label="Click here"> 31 + Button 32 + </Button> 33 + <Button 34 + disabled 35 + variant={variant as ButtonVariant} 36 + color={color as ButtonColor} 37 + size="large" 38 + label="Click here"> 39 + Button 40 + </Button> 41 + </React.Fragment> 42 + ))} 43 + </View> 44 + ))} 45 + 46 + <View style={[a.flex_row, a.gap_md, a.align_start]}> 47 + <View style={[a.gap_md, a.align_start]}> 48 + {['gradient_sky', 'gradient_midnight', 'gradient_sunrise'].map( 49 + name => ( 50 + <React.Fragment key={name}> 51 + <Button 52 + variant="gradient" 53 + color={name as ButtonColor} 54 + size="large" 55 + label="Click here"> 56 + Button 57 + </Button> 58 + <Button 59 + disabled 60 + variant="gradient" 61 + color={name as ButtonColor} 62 + size="large" 63 + label="Click here"> 64 + Button 65 + </Button> 66 + </React.Fragment> 67 + ), 68 + )} 69 + </View> 70 + <View style={[a.gap_md, a.align_start]}> 71 + {['gradient_sunset', 'gradient_nordic', 'gradient_bonfire'].map( 72 + name => ( 73 + <React.Fragment key={name}> 74 + <Button 75 + variant="gradient" 76 + color={name as ButtonColor} 77 + size="large" 78 + label="Click here"> 79 + Button 80 + </Button> 81 + <Button 82 + disabled 83 + variant="gradient" 84 + color={name as ButtonColor} 85 + size="large" 86 + label="Click here"> 87 + Button 88 + </Button> 89 + </React.Fragment> 90 + ), 91 + )} 92 + </View> 93 + </View> 94 + 95 + <Button 96 + variant="gradient" 97 + color="gradient_sky" 98 + size="large" 99 + label="Link out"> 100 + <ButtonText>Link out</ButtonText> 101 + <ButtonIcon icon={ArrowTopRight} /> 102 + </Button> 103 + 104 + <Button 105 + variant="gradient" 106 + color="gradient_sky" 107 + size="small" 108 + label="Link out"> 109 + <ButtonText>Link out</ButtonText> 110 + <ButtonIcon icon={ArrowTopRight} /> 111 + </Button> 112 + 113 + <Button 114 + variant="gradient" 115 + color="gradient_sky" 116 + size="small" 117 + label="Link out"> 118 + <ButtonIcon icon={Globe} /> 119 + <ButtonText>See the world</ButtonText> 120 + </Button> 121 + </View> 122 + </View> 123 + ) 124 + }
+90
src/view/screens/Storybook/Dialogs.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + 4 + import {atoms as a} from '#/alf' 5 + import {Button} from '#/components/Button' 6 + import {H3, P} from '#/components/Typography' 7 + import * as Dialog from '#/components/Dialog' 8 + import * as Prompt from '#/components/Prompt' 9 + import {useDialogStateControlContext} from '#/state/dialogs' 10 + 11 + export function Dialogs() { 12 + const control = Dialog.useDialogControl() 13 + const prompt = Prompt.usePromptControl() 14 + const {closeAllDialogs} = useDialogStateControlContext() 15 + 16 + return ( 17 + <View style={[a.gap_md]}> 18 + <Button 19 + variant="outline" 20 + color="secondary" 21 + size="small" 22 + onPress={() => { 23 + control.open() 24 + prompt.open() 25 + }} 26 + label="Open basic dialog"> 27 + Open basic dialog 28 + </Button> 29 + 30 + <Button 31 + variant="solid" 32 + color="primary" 33 + size="small" 34 + onPress={() => prompt.open()} 35 + label="Open prompt"> 36 + Open prompt 37 + </Button> 38 + 39 + <Prompt.Outer control={prompt}> 40 + <Prompt.Title>This is a prompt</Prompt.Title> 41 + <Prompt.Description> 42 + This is a generic prompt component. It accepts a title and a 43 + description, as well as two actions. 44 + </Prompt.Description> 45 + <Prompt.Actions> 46 + <Prompt.Cancel>Cancel</Prompt.Cancel> 47 + <Prompt.Action>Confirm</Prompt.Action> 48 + </Prompt.Actions> 49 + </Prompt.Outer> 50 + 51 + <Dialog.Outer 52 + control={control} 53 + nativeOptions={{sheet: {snapPoints: ['90%']}}}> 54 + <Dialog.Handle /> 55 + 56 + <Dialog.ScrollableInner 57 + accessibilityDescribedBy="dialog-description" 58 + accessibilityLabelledBy="dialog-title"> 59 + <View style={[a.relative, a.gap_md, a.w_full]}> 60 + <H3 nativeID="dialog-title">Dialog</H3> 61 + <P nativeID="dialog-description"> 62 + A scrollable dialog with an input within it. 63 + </P> 64 + <Dialog.Input value="" onChangeText={() => {}} label="Type here" /> 65 + 66 + <Button 67 + variant="outline" 68 + color="secondary" 69 + size="small" 70 + onPress={closeAllDialogs} 71 + label="Close all dialogs"> 72 + Close all dialogs 73 + </Button> 74 + <View style={{height: 1000}} /> 75 + <View style={[a.flex_row, a.justify_end]}> 76 + <Button 77 + variant="outline" 78 + color="primary" 79 + size="small" 80 + onPress={() => control.close()} 81 + label="Open basic dialog"> 82 + Close basic dialog 83 + </Button> 84 + </View> 85 + </View> 86 + </Dialog.ScrollableInner> 87 + </Dialog.Outer> 88 + </View> 89 + ) 90 + }
+215
src/view/screens/Storybook/Forms.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + 4 + import {atoms as a} from '#/alf' 5 + import {H1, H3} from '#/components/Typography' 6 + import * as TextField from '#/components/forms/TextField' 7 + import {DateField, Label} from '#/components/forms/DateField' 8 + import * as Toggle from '#/components/forms/Toggle' 9 + import * as ToggleButton from '#/components/forms/ToggleButton' 10 + import {Button} from '#/components/Button' 11 + import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' 12 + 13 + export function Forms() { 14 + const [toggleGroupAValues, setToggleGroupAValues] = React.useState(['a']) 15 + const [toggleGroupBValues, setToggleGroupBValues] = React.useState(['a', 'b']) 16 + const [toggleGroupCValues, setToggleGroupCValues] = React.useState(['a', 'b']) 17 + const [toggleGroupDValues, setToggleGroupDValues] = React.useState(['warn']) 18 + 19 + const [value, setValue] = React.useState('') 20 + const [date, setDate] = React.useState('2001-01-01') 21 + 22 + return ( 23 + <View style={[a.gap_4xl, a.align_start]}> 24 + <H1>Forms</H1> 25 + 26 + <View style={[a.gap_md, a.align_start, a.w_full]}> 27 + <H3>InputText</H3> 28 + 29 + <TextField.Input 30 + value={value} 31 + onChangeText={setValue} 32 + label="Text field" 33 + /> 34 + 35 + <TextField.Root> 36 + <TextField.Icon icon={Globe} /> 37 + <TextField.Input 38 + value={value} 39 + onChangeText={setValue} 40 + label="Text field" 41 + /> 42 + </TextField.Root> 43 + 44 + <View style={[a.w_full]}> 45 + <TextField.Label>Text field</TextField.Label> 46 + <TextField.Root> 47 + <TextField.Icon icon={Globe} /> 48 + <TextField.Input 49 + value={value} 50 + onChangeText={setValue} 51 + label="Text field" 52 + /> 53 + <TextField.Suffix label="@gmail.com">@gmail.com</TextField.Suffix> 54 + </TextField.Root> 55 + </View> 56 + 57 + <View style={[a.w_full]}> 58 + <TextField.Label>Textarea</TextField.Label> 59 + <TextField.Input 60 + multiline 61 + numberOfLines={4} 62 + value={value} 63 + onChangeText={setValue} 64 + label="Text field" 65 + /> 66 + </View> 67 + 68 + <H3>DateField</H3> 69 + 70 + <View style={[a.w_full]}> 71 + <Label>Date</Label> 72 + <DateField 73 + testID="date" 74 + value={date} 75 + onChangeDate={date => { 76 + console.log(date) 77 + setDate(date) 78 + }} 79 + label="Input" 80 + /> 81 + </View> 82 + </View> 83 + 84 + <View style={[a.gap_md, a.align_start, a.w_full]}> 85 + <H3>Toggles</H3> 86 + 87 + <Toggle.Item name="a" label="Click me"> 88 + <Toggle.Checkbox /> 89 + <Toggle.Label>Uncontrolled toggle</Toggle.Label> 90 + </Toggle.Item> 91 + 92 + <Toggle.Group 93 + label="Toggle" 94 + type="checkbox" 95 + maxSelections={2} 96 + values={toggleGroupAValues} 97 + onChange={setToggleGroupAValues}> 98 + <View style={[a.gap_md]}> 99 + <Toggle.Item name="a" label="Click me"> 100 + <Toggle.Switch /> 101 + <Toggle.Label>Click me</Toggle.Label> 102 + </Toggle.Item> 103 + <Toggle.Item name="b" label="Click me"> 104 + <Toggle.Switch /> 105 + <Toggle.Label>Click me</Toggle.Label> 106 + </Toggle.Item> 107 + <Toggle.Item name="c" label="Click me"> 108 + <Toggle.Switch /> 109 + <Toggle.Label>Click me</Toggle.Label> 110 + </Toggle.Item> 111 + <Toggle.Item name="d" disabled label="Click me"> 112 + <Toggle.Switch /> 113 + <Toggle.Label>Click me</Toggle.Label> 114 + </Toggle.Item> 115 + <Toggle.Item name="e" isInvalid label="Click me"> 116 + <Toggle.Switch /> 117 + <Toggle.Label>Click me</Toggle.Label> 118 + </Toggle.Item> 119 + </View> 120 + </Toggle.Group> 121 + 122 + <Toggle.Group 123 + label="Toggle" 124 + type="checkbox" 125 + maxSelections={2} 126 + values={toggleGroupBValues} 127 + onChange={setToggleGroupBValues}> 128 + <View style={[a.gap_md]}> 129 + <Toggle.Item name="a" label="Click me"> 130 + <Toggle.Checkbox /> 131 + <Toggle.Label>Click me</Toggle.Label> 132 + </Toggle.Item> 133 + <Toggle.Item name="b" label="Click me"> 134 + <Toggle.Checkbox /> 135 + <Toggle.Label>Click me</Toggle.Label> 136 + </Toggle.Item> 137 + <Toggle.Item name="c" label="Click me"> 138 + <Toggle.Checkbox /> 139 + <Toggle.Label>Click me</Toggle.Label> 140 + </Toggle.Item> 141 + <Toggle.Item name="d" disabled label="Click me"> 142 + <Toggle.Checkbox /> 143 + <Toggle.Label>Click me</Toggle.Label> 144 + </Toggle.Item> 145 + <Toggle.Item name="e" isInvalid label="Click me"> 146 + <Toggle.Checkbox /> 147 + <Toggle.Label>Click me</Toggle.Label> 148 + </Toggle.Item> 149 + </View> 150 + </Toggle.Group> 151 + 152 + <Toggle.Group 153 + label="Toggle" 154 + type="radio" 155 + values={toggleGroupCValues} 156 + onChange={setToggleGroupCValues}> 157 + <View style={[a.gap_md]}> 158 + <Toggle.Item name="a" label="Click me"> 159 + <Toggle.Radio /> 160 + <Toggle.Label>Click me</Toggle.Label> 161 + </Toggle.Item> 162 + <Toggle.Item name="b" label="Click me"> 163 + <Toggle.Radio /> 164 + <Toggle.Label>Click me</Toggle.Label> 165 + </Toggle.Item> 166 + <Toggle.Item name="c" label="Click me"> 167 + <Toggle.Radio /> 168 + <Toggle.Label>Click me</Toggle.Label> 169 + </Toggle.Item> 170 + <Toggle.Item name="d" disabled label="Click me"> 171 + <Toggle.Radio /> 172 + <Toggle.Label>Click me</Toggle.Label> 173 + </Toggle.Item> 174 + <Toggle.Item name="e" isInvalid label="Click me"> 175 + <Toggle.Radio /> 176 + <Toggle.Label>Click me</Toggle.Label> 177 + </Toggle.Item> 178 + </View> 179 + </Toggle.Group> 180 + </View> 181 + 182 + <Button 183 + variant="gradient" 184 + color="gradient_nordic" 185 + size="small" 186 + label="Reset all toggles" 187 + onPress={() => { 188 + setToggleGroupAValues(['a']) 189 + setToggleGroupBValues(['a', 'b']) 190 + setToggleGroupCValues(['a']) 191 + }}> 192 + Reset all toggles 193 + </Button> 194 + 195 + <View style={[a.gap_md, a.align_start, a.w_full]}> 196 + <H3>ToggleButton</H3> 197 + 198 + <ToggleButton.Group 199 + label="Preferences" 200 + values={toggleGroupDValues} 201 + onChange={setToggleGroupDValues}> 202 + <ToggleButton.Button name="hide" label="Hide"> 203 + Hide 204 + </ToggleButton.Button> 205 + <ToggleButton.Button name="warn" label="Warn"> 206 + Warn 207 + </ToggleButton.Button> 208 + <ToggleButton.Button name="show" label="Show"> 209 + Show 210 + </ToggleButton.Button> 211 + </ToggleButton.Group> 212 + </View> 213 + </View> 214 + ) 215 + }
+41
src/view/screens/Storybook/Icons.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + 4 + import {atoms as a, useTheme} from '#/alf' 5 + import {H1} from '#/components/Typography' 6 + import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' 7 + import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight' 8 + import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' 9 + 10 + export function Icons() { 11 + const t = useTheme() 12 + return ( 13 + <View style={[a.gap_md]}> 14 + <H1>Icons</H1> 15 + 16 + <View style={[a.flex_row, a.gap_xl]}> 17 + <Globe size="xs" fill={t.atoms.text.color} /> 18 + <Globe size="sm" fill={t.atoms.text.color} /> 19 + <Globe size="md" fill={t.atoms.text.color} /> 20 + <Globe size="lg" fill={t.atoms.text.color} /> 21 + <Globe size="xl" fill={t.atoms.text.color} /> 22 + </View> 23 + 24 + <View style={[a.flex_row, a.gap_xl]}> 25 + <ArrowTopRight size="xs" fill={t.atoms.text.color} /> 26 + <ArrowTopRight size="sm" fill={t.atoms.text.color} /> 27 + <ArrowTopRight size="md" fill={t.atoms.text.color} /> 28 + <ArrowTopRight size="lg" fill={t.atoms.text.color} /> 29 + <ArrowTopRight size="xl" fill={t.atoms.text.color} /> 30 + </View> 31 + 32 + <View style={[a.flex_row, a.gap_xl]}> 33 + <CalendarDays size="xs" fill={t.atoms.text.color} /> 34 + <CalendarDays size="sm" fill={t.atoms.text.color} /> 35 + <CalendarDays size="md" fill={t.atoms.text.color} /> 36 + <CalendarDays size="lg" fill={t.atoms.text.color} /> 37 + <CalendarDays size="xl" fill={t.atoms.text.color} /> 38 + </View> 39 + </View> 40 + ) 41 + }
+48
src/view/screens/Storybook/Links.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + 4 + import {atoms as a} from '#/alf' 5 + import {ButtonText} from '#/components/Button' 6 + import {Link} from '#/components/Link' 7 + import {H1, H3} from '#/components/Typography' 8 + 9 + export function Links() { 10 + return ( 11 + <View style={[a.gap_md, a.align_start]}> 12 + <H1>Links</H1> 13 + 14 + <View style={[a.gap_md, a.align_start]}> 15 + <Link 16 + to="https://blueskyweb.xyz" 17 + warnOnMismatchingTextChild 18 + style={[a.text_md]}> 19 + External 20 + </Link> 21 + <Link to="https://blueskyweb.xyz" style={[a.text_md]}> 22 + <H3>External with custom children</H3> 23 + </Link> 24 + <Link 25 + to="https://blueskyweb.xyz" 26 + warnOnMismatchingTextChild 27 + style={[a.text_lg]}> 28 + https://blueskyweb.xyz 29 + </Link> 30 + <Link 31 + to="https://bsky.app/profile/bsky.app" 32 + warnOnMismatchingTextChild 33 + style={[a.text_md]}> 34 + Internal 35 + </Link> 36 + 37 + <Link 38 + variant="solid" 39 + color="primary" 40 + size="large" 41 + label="View @bsky.app's profile" 42 + to="https://bsky.app/profile/bsky.app"> 43 + <ButtonText>Link as a button</ButtonText> 44 + </Link> 45 + </View> 46 + </View> 47 + ) 48 + }
+336
src/view/screens/Storybook/Palette.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + 4 + import * as tokens from '#/alf/tokens' 5 + import {atoms as a} from '#/alf' 6 + 7 + export function Palette() { 8 + return ( 9 + <View style={[a.gap_md]}> 10 + <View style={[a.flex_row, a.gap_md]}> 11 + <View 12 + style={[a.flex_1, {height: 60, backgroundColor: tokens.color.gray_0}]} 13 + /> 14 + <View 15 + style={[ 16 + a.flex_1, 17 + {height: 60, backgroundColor: tokens.color.gray_25}, 18 + ]} 19 + /> 20 + <View 21 + style={[ 22 + a.flex_1, 23 + {height: 60, backgroundColor: tokens.color.gray_50}, 24 + ]} 25 + /> 26 + <View 27 + style={[ 28 + a.flex_1, 29 + {height: 60, backgroundColor: tokens.color.gray_100}, 30 + ]} 31 + /> 32 + <View 33 + style={[ 34 + a.flex_1, 35 + {height: 60, backgroundColor: tokens.color.gray_200}, 36 + ]} 37 + /> 38 + <View 39 + style={[ 40 + a.flex_1, 41 + {height: 60, backgroundColor: tokens.color.gray_300}, 42 + ]} 43 + /> 44 + <View 45 + style={[ 46 + a.flex_1, 47 + {height: 60, backgroundColor: tokens.color.gray_400}, 48 + ]} 49 + /> 50 + <View 51 + style={[ 52 + a.flex_1, 53 + {height: 60, backgroundColor: tokens.color.gray_500}, 54 + ]} 55 + /> 56 + <View 57 + style={[ 58 + a.flex_1, 59 + {height: 60, backgroundColor: tokens.color.gray_600}, 60 + ]} 61 + /> 62 + <View 63 + style={[ 64 + a.flex_1, 65 + {height: 60, backgroundColor: tokens.color.gray_700}, 66 + ]} 67 + /> 68 + <View 69 + style={[ 70 + a.flex_1, 71 + {height: 60, backgroundColor: tokens.color.gray_800}, 72 + ]} 73 + /> 74 + <View 75 + style={[ 76 + a.flex_1, 77 + {height: 60, backgroundColor: tokens.color.gray_900}, 78 + ]} 79 + /> 80 + <View 81 + style={[ 82 + a.flex_1, 83 + {height: 60, backgroundColor: tokens.color.gray_950}, 84 + ]} 85 + /> 86 + <View 87 + style={[ 88 + a.flex_1, 89 + {height: 60, backgroundColor: tokens.color.gray_975}, 90 + ]} 91 + /> 92 + <View 93 + style={[ 94 + a.flex_1, 95 + {height: 60, backgroundColor: tokens.color.gray_1000}, 96 + ]} 97 + /> 98 + </View> 99 + 100 + <View style={[a.flex_row, a.gap_md]}> 101 + <View 102 + style={[ 103 + a.flex_1, 104 + {height: 60, backgroundColor: tokens.color.blue_25}, 105 + ]} 106 + /> 107 + <View 108 + style={[ 109 + a.flex_1, 110 + {height: 60, backgroundColor: tokens.color.blue_50}, 111 + ]} 112 + /> 113 + <View 114 + style={[ 115 + a.flex_1, 116 + {height: 60, backgroundColor: tokens.color.blue_100}, 117 + ]} 118 + /> 119 + <View 120 + style={[ 121 + a.flex_1, 122 + {height: 60, backgroundColor: tokens.color.blue_200}, 123 + ]} 124 + /> 125 + <View 126 + style={[ 127 + a.flex_1, 128 + {height: 60, backgroundColor: tokens.color.blue_300}, 129 + ]} 130 + /> 131 + <View 132 + style={[ 133 + a.flex_1, 134 + {height: 60, backgroundColor: tokens.color.blue_400}, 135 + ]} 136 + /> 137 + <View 138 + style={[ 139 + a.flex_1, 140 + {height: 60, backgroundColor: tokens.color.blue_500}, 141 + ]} 142 + /> 143 + <View 144 + style={[ 145 + a.flex_1, 146 + {height: 60, backgroundColor: tokens.color.blue_600}, 147 + ]} 148 + /> 149 + <View 150 + style={[ 151 + a.flex_1, 152 + {height: 60, backgroundColor: tokens.color.blue_700}, 153 + ]} 154 + /> 155 + <View 156 + style={[ 157 + a.flex_1, 158 + {height: 60, backgroundColor: tokens.color.blue_800}, 159 + ]} 160 + /> 161 + <View 162 + style={[ 163 + a.flex_1, 164 + {height: 60, backgroundColor: tokens.color.blue_900}, 165 + ]} 166 + /> 167 + <View 168 + style={[ 169 + a.flex_1, 170 + {height: 60, backgroundColor: tokens.color.blue_950}, 171 + ]} 172 + /> 173 + <View 174 + style={[ 175 + a.flex_1, 176 + {height: 60, backgroundColor: tokens.color.blue_975}, 177 + ]} 178 + /> 179 + </View> 180 + <View style={[a.flex_row, a.gap_md]}> 181 + <View 182 + style={[ 183 + a.flex_1, 184 + {height: 60, backgroundColor: tokens.color.green_25}, 185 + ]} 186 + /> 187 + <View 188 + style={[ 189 + a.flex_1, 190 + {height: 60, backgroundColor: tokens.color.green_50}, 191 + ]} 192 + /> 193 + <View 194 + style={[ 195 + a.flex_1, 196 + {height: 60, backgroundColor: tokens.color.green_100}, 197 + ]} 198 + /> 199 + <View 200 + style={[ 201 + a.flex_1, 202 + {height: 60, backgroundColor: tokens.color.green_200}, 203 + ]} 204 + /> 205 + <View 206 + style={[ 207 + a.flex_1, 208 + {height: 60, backgroundColor: tokens.color.green_300}, 209 + ]} 210 + /> 211 + <View 212 + style={[ 213 + a.flex_1, 214 + {height: 60, backgroundColor: tokens.color.green_400}, 215 + ]} 216 + /> 217 + <View 218 + style={[ 219 + a.flex_1, 220 + {height: 60, backgroundColor: tokens.color.green_500}, 221 + ]} 222 + /> 223 + <View 224 + style={[ 225 + a.flex_1, 226 + {height: 60, backgroundColor: tokens.color.green_600}, 227 + ]} 228 + /> 229 + <View 230 + style={[ 231 + a.flex_1, 232 + {height: 60, backgroundColor: tokens.color.green_700}, 233 + ]} 234 + /> 235 + <View 236 + style={[ 237 + a.flex_1, 238 + {height: 60, backgroundColor: tokens.color.green_800}, 239 + ]} 240 + /> 241 + <View 242 + style={[ 243 + a.flex_1, 244 + {height: 60, backgroundColor: tokens.color.green_900}, 245 + ]} 246 + /> 247 + <View 248 + style={[ 249 + a.flex_1, 250 + {height: 60, backgroundColor: tokens.color.green_950}, 251 + ]} 252 + /> 253 + <View 254 + style={[ 255 + a.flex_1, 256 + {height: 60, backgroundColor: tokens.color.green_975}, 257 + ]} 258 + /> 259 + </View> 260 + <View style={[a.flex_row, a.gap_md]}> 261 + <View 262 + style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_25}]} 263 + /> 264 + <View 265 + style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_50}]} 266 + /> 267 + <View 268 + style={[ 269 + a.flex_1, 270 + {height: 60, backgroundColor: tokens.color.red_100}, 271 + ]} 272 + /> 273 + <View 274 + style={[ 275 + a.flex_1, 276 + {height: 60, backgroundColor: tokens.color.red_200}, 277 + ]} 278 + /> 279 + <View 280 + style={[ 281 + a.flex_1, 282 + {height: 60, backgroundColor: tokens.color.red_300}, 283 + ]} 284 + /> 285 + <View 286 + style={[ 287 + a.flex_1, 288 + {height: 60, backgroundColor: tokens.color.red_400}, 289 + ]} 290 + /> 291 + <View 292 + style={[ 293 + a.flex_1, 294 + {height: 60, backgroundColor: tokens.color.red_500}, 295 + ]} 296 + /> 297 + <View 298 + style={[ 299 + a.flex_1, 300 + {height: 60, backgroundColor: tokens.color.red_600}, 301 + ]} 302 + /> 303 + <View 304 + style={[ 305 + a.flex_1, 306 + {height: 60, backgroundColor: tokens.color.red_700}, 307 + ]} 308 + /> 309 + <View 310 + style={[ 311 + a.flex_1, 312 + {height: 60, backgroundColor: tokens.color.red_800}, 313 + ]} 314 + /> 315 + <View 316 + style={[ 317 + a.flex_1, 318 + {height: 60, backgroundColor: tokens.color.red_900}, 319 + ]} 320 + /> 321 + <View 322 + style={[ 323 + a.flex_1, 324 + {height: 60, backgroundColor: tokens.color.red_950}, 325 + ]} 326 + /> 327 + <View 328 + style={[ 329 + a.flex_1, 330 + {height: 60, backgroundColor: tokens.color.red_975}, 331 + ]} 332 + /> 333 + </View> 334 + </View> 335 + ) 336 + }
+53
src/view/screens/Storybook/Shadows.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + 4 + import {atoms as a, useTheme} from '#/alf' 5 + import {H1, Text} from '#/components/Typography' 6 + 7 + export function Shadows() { 8 + const t = useTheme() 9 + 10 + return ( 11 + <View style={[a.gap_md]}> 12 + <H1>Shadows</H1> 13 + 14 + <View style={[a.flex_row, a.gap_5xl]}> 15 + <View 16 + style={[ 17 + a.flex_1, 18 + a.justify_center, 19 + a.px_lg, 20 + a.py_2xl, 21 + t.atoms.bg, 22 + t.atoms.shadow_sm, 23 + ]}> 24 + <Text>shadow_sm</Text> 25 + </View> 26 + 27 + <View 28 + style={[ 29 + a.flex_1, 30 + a.justify_center, 31 + a.px_lg, 32 + a.py_2xl, 33 + t.atoms.bg, 34 + t.atoms.shadow_md, 35 + ]}> 36 + <Text>shadow_md</Text> 37 + </View> 38 + 39 + <View 40 + style={[ 41 + a.flex_1, 42 + a.justify_center, 43 + a.px_lg, 44 + a.py_2xl, 45 + t.atoms.bg, 46 + t.atoms.shadow_lg, 47 + ]}> 48 + <Text>shadow_lg</Text> 49 + </View> 50 + </View> 51 + </View> 52 + ) 53 + }
+64
src/view/screens/Storybook/Spacing.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + 4 + import {atoms as a, useTheme} from '#/alf' 5 + import {Text, H1} from '#/components/Typography' 6 + 7 + export function Spacing() { 8 + const t = useTheme() 9 + return ( 10 + <View style={[a.gap_md]}> 11 + <H1>Spacing</H1> 12 + 13 + <View style={[a.flex_row, a.align_center]}> 14 + <Text style={{width: 80}}>2xs (2px)</Text> 15 + <View style={[a.flex_1, a.pt_2xs, t.atoms.bg_contrast_300]} /> 16 + </View> 17 + 18 + <View style={[a.flex_row, a.align_center]}> 19 + <Text style={{width: 80}}>xs (4px)</Text> 20 + <View style={[a.flex_1, a.pt_xs, t.atoms.bg_contrast_300]} /> 21 + </View> 22 + 23 + <View style={[a.flex_row, a.align_center]}> 24 + <Text style={{width: 80}}>sm (8px)</Text> 25 + <View style={[a.flex_1, a.pt_sm, t.atoms.bg_contrast_300]} /> 26 + </View> 27 + 28 + <View style={[a.flex_row, a.align_center]}> 29 + <Text style={{width: 80}}>md (12px)</Text> 30 + <View style={[a.flex_1, a.pt_md, t.atoms.bg_contrast_300]} /> 31 + </View> 32 + 33 + <View style={[a.flex_row, a.align_center]}> 34 + <Text style={{width: 80}}>lg (16px)</Text> 35 + <View style={[a.flex_1, a.pt_lg, t.atoms.bg_contrast_300]} /> 36 + </View> 37 + 38 + <View style={[a.flex_row, a.align_center]}> 39 + <Text style={{width: 80}}>xl (20px)</Text> 40 + <View style={[a.flex_1, a.pt_xl, t.atoms.bg_contrast_300]} /> 41 + </View> 42 + 43 + <View style={[a.flex_row, a.align_center]}> 44 + <Text style={{width: 80}}>2xl (24px)</Text> 45 + <View style={[a.flex_1, a.pt_2xl, t.atoms.bg_contrast_300]} /> 46 + </View> 47 + 48 + <View style={[a.flex_row, a.align_center]}> 49 + <Text style={{width: 80}}>3xl (28px)</Text> 50 + <View style={[a.flex_1, a.pt_3xl, t.atoms.bg_contrast_300]} /> 51 + </View> 52 + 53 + <View style={[a.flex_row, a.align_center]}> 54 + <Text style={{width: 80}}>4xl (32px)</Text> 55 + <View style={[a.flex_1, a.pt_4xl, t.atoms.bg_contrast_300]} /> 56 + </View> 57 + 58 + <View style={[a.flex_row, a.align_center]}> 59 + <Text style={{width: 80}}>5xl (40px)</Text> 60 + <View style={[a.flex_1, a.pt_5xl, t.atoms.bg_contrast_300]} /> 61 + </View> 62 + </View> 63 + ) 64 + }
+56
src/view/screens/Storybook/Theming.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + 4 + import {atoms as a, useTheme} from '#/alf' 5 + import {Text} from '#/components/Typography' 6 + import {Palette} from './Palette' 7 + 8 + export function Theming() { 9 + const t = useTheme() 10 + 11 + return ( 12 + <View style={[t.atoms.bg, a.gap_lg, a.p_xl]}> 13 + <Palette /> 14 + 15 + <Text style={[a.font_bold, a.pt_xl, a.px_md]}>theme.atoms.text</Text> 16 + 17 + <View style={[a.flex_1, t.atoms.border, a.border_t]} /> 18 + <Text style={[a.font_bold, t.atoms.text_contrast_600, a.px_md]}> 19 + theme.atoms.text_contrast_600 20 + </Text> 21 + 22 + <View style={[a.flex_1, t.atoms.border, a.border_t]} /> 23 + <Text style={[a.font_bold, t.atoms.text_contrast_500, a.px_md]}> 24 + theme.atoms.text_contrast_500 25 + </Text> 26 + 27 + <View style={[a.flex_1, t.atoms.border, a.border_t]} /> 28 + <Text style={[a.font_bold, t.atoms.text_contrast_400, a.px_md]}> 29 + theme.atoms.text_contrast_400 30 + </Text> 31 + 32 + <View style={[a.flex_1, t.atoms.border_contrast, a.border_t]} /> 33 + 34 + <View style={[a.w_full, a.gap_md]}> 35 + <View style={[t.atoms.bg, a.justify_center, a.p_md]}> 36 + <Text>theme.atoms.bg</Text> 37 + </View> 38 + <View style={[t.atoms.bg_contrast_25, a.justify_center, a.p_md]}> 39 + <Text>theme.atoms.bg_contrast_25</Text> 40 + </View> 41 + <View style={[t.atoms.bg_contrast_50, a.justify_center, a.p_md]}> 42 + <Text>theme.atoms.bg_contrast_50</Text> 43 + </View> 44 + <View style={[t.atoms.bg_contrast_100, a.justify_center, a.p_md]}> 45 + <Text>theme.atoms.bg_contrast_100</Text> 46 + </View> 47 + <View style={[t.atoms.bg_contrast_200, a.justify_center, a.p_md]}> 48 + <Text>theme.atoms.bg_contrast_200</Text> 49 + </View> 50 + <View style={[t.atoms.bg_contrast_300, a.justify_center, a.p_md]}> 51 + <Text>theme.atoms.bg_contrast_300</Text> 52 + </View> 53 + </View> 54 + </View> 55 + ) 56 + }
+30
src/view/screens/Storybook/Typography.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + 4 + import {atoms as a} from '#/alf' 5 + import {Text, H1, H2, H3, H4, H5, H6, P} from '#/components/Typography' 6 + 7 + export function Typography() { 8 + return ( 9 + <View style={[a.gap_md]}> 10 + <H1>H1 Heading</H1> 11 + <H2>H2 Heading</H2> 12 + <H3>H3 Heading</H3> 13 + <H4>H4 Heading</H4> 14 + <H5>H5 Heading</H5> 15 + <H6>H6 Heading</H6> 16 + <P>P Paragraph</P> 17 + 18 + <Text style={[a.text_5xl]}>atoms.text_5xl</Text> 19 + <Text style={[a.text_4xl]}>atoms.text_4xl</Text> 20 + <Text style={[a.text_3xl]}>atoms.text_3xl</Text> 21 + <Text style={[a.text_2xl]}>atoms.text_2xl</Text> 22 + <Text style={[a.text_xl]}>atoms.text_xl</Text> 23 + <Text style={[a.text_lg]}>atoms.text_lg</Text> 24 + <Text style={[a.text_md]}>atoms.text_md</Text> 25 + <Text style={[a.text_sm]}>atoms.text_sm</Text> 26 + <Text style={[a.text_xs]}>atoms.text_xs</Text> 27 + <Text style={[a.text_2xs]}>atoms.text_2xs</Text> 28 + </View> 29 + ) 30 + }
+78
src/view/screens/Storybook/index.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {CenteredView, ScrollView} from '#/view/com/util/Views' 4 + 5 + import {atoms as a, useTheme, ThemeProvider} from '#/alf' 6 + import {useSetColorMode} from '#/state/shell' 7 + import {Button} from '#/components/Button' 8 + 9 + import {Theming} from './Theming' 10 + import {Typography} from './Typography' 11 + import {Spacing} from './Spacing' 12 + import {Buttons} from './Buttons' 13 + import {Links} from './Links' 14 + import {Forms} from './Forms' 15 + import {Dialogs} from './Dialogs' 16 + import {Breakpoints} from './Breakpoints' 17 + import {Shadows} from './Shadows' 18 + import {Icons} from './Icons' 19 + 20 + export function Storybook() { 21 + const t = useTheme() 22 + const setColorMode = useSetColorMode() 23 + 24 + return ( 25 + <ScrollView> 26 + <CenteredView style={[t.atoms.bg]}> 27 + <View style={[a.p_xl, a.gap_5xl, {paddingBottom: 200}]}> 28 + <View style={[a.flex_row, a.align_start, a.gap_md]}> 29 + <Button 30 + variant="outline" 31 + color="primary" 32 + size="small" 33 + label='Set theme to "system"' 34 + onPress={() => setColorMode('system')}> 35 + System 36 + </Button> 37 + <Button 38 + variant="solid" 39 + color="secondary" 40 + size="small" 41 + label='Set theme to "system"' 42 + onPress={() => setColorMode('light')}> 43 + Light 44 + </Button> 45 + <Button 46 + variant="solid" 47 + color="secondary" 48 + size="small" 49 + label='Set theme to "system"' 50 + onPress={() => setColorMode('dark')}> 51 + Dark 52 + </Button> 53 + </View> 54 + 55 + <ThemeProvider theme="light"> 56 + <Theming /> 57 + </ThemeProvider> 58 + <ThemeProvider theme="dim"> 59 + <Theming /> 60 + </ThemeProvider> 61 + <ThemeProvider theme="dark"> 62 + <Theming /> 63 + </ThemeProvider> 64 + 65 + <Typography /> 66 + <Spacing /> 67 + <Shadows /> 68 + <Buttons /> 69 + <Icons /> 70 + <Links /> 71 + <Forms /> 72 + <Dialogs /> 73 + <Breakpoints /> 74 + </View> 75 + </CenteredView> 76 + </ScrollView> 77 + ) 78 + }
+2
src/view/shell/index.tsx
··· 28 28 import {useSession} from '#/state/session' 29 29 import {useCloseAnyActiveElement} from '#/state/util' 30 30 import * as notifications from 'lib/notifications/notifications' 31 + import {Outlet as PortalOutlet} from '#/components/Portal' 31 32 32 33 function ShellInner() { 33 34 const isDrawerOpen = useIsDrawerOpen() ··· 94 95 </View> 95 96 <Composer winHeight={winDim.height} /> 96 97 <ModalsContainer /> 98 + <PortalOutlet /> 97 99 <Lightbox /> 98 100 </> 99 101 )
+2
src/view/shell/index.web.tsx
··· 15 15 import {t} from '@lingui/macro' 16 16 import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' 17 17 import {useCloseAllActiveElements} from '#/state/util' 18 + import {Outlet as PortalOutlet} from '#/components/Portal' 18 19 19 20 function ShellInner() { 20 21 const isDrawerOpen = useIsDrawerOpen() ··· 41 42 </View> 42 43 <Composer winHeight={0} /> 43 44 <ModalsContainer /> 45 + <PortalOutlet /> 44 46 <Lightbox /> 45 47 {!isDesktop && isDrawerOpen && ( 46 48 <TouchableOpacity
+19
web/index.html
··· 43 43 height: calc(100% + env(safe-area-inset-top)); 44 44 } 45 45 46 + /* Remove autofill styles on Webkit */ 47 + input:-webkit-autofill, 48 + input:-webkit-autofill:hover, 49 + input:-webkit-autofill:focus, 50 + textarea:-webkit-autofill, 51 + textarea:-webkit-autofill:hover, 52 + textarea:-webkit-autofill:focus, 53 + select:-webkit-autofill, 54 + select:-webkit-autofill:hover, 55 + select:-webkit-autofill:focus { 56 + border: 0; 57 + -webkit-text-fill-color: transparent; 58 + -webkit-box-shadow: none; 59 + } 60 + /* Force left-align date/time inputs on iOS mobile */ 61 + input::-webkit-date-and-time-value { 62 + text-align: left; 63 + } 64 + 46 65 /* Color theming */ 47 66 :root { 48 67 --text: black;
+25
yarn.lock
··· 6966 6966 "@svgr/plugin-svgo" "^5.5.0" 6967 6967 loader-utils "^2.0.0" 6968 6968 6969 + "@tamagui/compose-refs@1.84.1": 6970 + version "1.84.1" 6971 + resolved "https://registry.yarnpkg.com/@tamagui/compose-refs/-/compose-refs-1.84.1.tgz#244735edc3ac2e617389297f005d5bc25872465f" 6972 + integrity sha512-oZ0rUmQABlGm/QKQITxAW9WLV3qjyq1ehgoWcZVmtc1Kc/hkFQe2J+wRQV726CmTAnuUgUXi3eoNMwBVoZksfQ== 6973 + 6974 + "@tamagui/constants@1.84.1": 6975 + version "1.84.1" 6976 + resolved "https://registry.yarnpkg.com/@tamagui/constants/-/constants-1.84.1.tgz#62e41837dbe844d14e255f3eea9c2583044d2509" 6977 + integrity sha512-QmvyCqtEIugqXutQI35GJQ1hlpSapYCdOHx9QlgsOWjAY34pu55MaY/tDrQeQ0AUmI/qx30vy7TsCJxB4QFEoQ== 6978 + 6979 + "@tamagui/focus-scope@^1.84.1": 6980 + version "1.84.1" 6981 + resolved "https://registry.yarnpkg.com/@tamagui/focus-scope/-/focus-scope-1.84.1.tgz#e9f061184048c75f87da023f54b9c5abccdd460d" 6982 + integrity sha512-0E1Wc3jmKhafETfH1dUuJYmGK1bDNA/9TySbOeTjTToxUoL3V0G2W5JSwSMCDqR1Bl+xrGlGwzXTUhouw8qSog== 6983 + dependencies: 6984 + "@tamagui/compose-refs" "1.84.1" 6985 + "@tamagui/use-event" "1.84.1" 6986 + 6987 + "@tamagui/use-event@1.84.1": 6988 + version "1.84.1" 6989 + resolved "https://registry.yarnpkg.com/@tamagui/use-event/-/use-event-1.84.1.tgz#a095a1bde9c40c4a397226c57c3fa32f6018f504" 6990 + integrity sha512-U88WCxvMz7ZSfMFMJEFbG3tJjK/Lf+PHlmtYvlx1V+YiqRBoj5+milzoM8PclENn5vZMiJW0ozYRgzI/cdE7Eg== 6991 + dependencies: 6992 + "@tamagui/constants" "1.84.1" 6993 + 6969 6994 "@tanstack/query-core@5.8.1": 6970 6995 version "5.8.1" 6971 6996 resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.8.1.tgz#5215a028370d9b2f32e83787a0ea119e2f977996"