about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.js15
-rw-r--r--eslint/__tests__/avoid-unwrapped-text.test.js339
-rw-r--r--eslint/avoid-unwrapped-text.js194
-rw-r--r--src/components/Button.tsx21
-rw-r--r--src/components/Lists.tsx6
-rw-r--r--src/components/moderation/LabelsOnMeDialog.tsx4
-rw-r--r--src/screens/Login/ChooseAccountForm.tsx4
-rw-r--r--src/screens/Login/LoginForm.tsx4
-rw-r--r--src/screens/Onboarding/Layout.tsx4
-rw-r--r--src/view/com/auth/server-input/index.tsx2
-rw-r--r--src/view/screens/Settings/ExportCarDialog.tsx4
-rw-r--r--src/view/screens/Storybook/Buttons.tsx16
-rw-r--r--src/view/screens/Storybook/Dialogs.tsx14
-rw-r--r--src/view/screens/Storybook/Forms.tsx4
-rw-r--r--src/view/screens/Storybook/index.tsx29
15 files changed, 587 insertions, 73 deletions
diff --git a/.eslintrc.js b/.eslintrc.js
index df0c76230..a999fd24b 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -23,17 +23,12 @@ module.exports = {
     'bsky-internal/avoid-unwrapped-text': [
       'error',
       {
-        impliedTextComponents: [
-          'Button', // TODO: Not always safe.
-          'H1',
-          'H2',
-          'H3',
-          'H4',
-          'H5',
-          'H6',
-          'P',
-        ],
+        impliedTextComponents: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P'],
         impliedTextProps: [],
+        suggestedTextWrappers: {
+          Button: 'ButtonText',
+          'ToggleButton.Button': 'ToggleButton.ButtonText',
+        },
       },
     ],
     'simple-import-sort/imports': [
diff --git a/eslint/__tests__/avoid-unwrapped-text.test.js b/eslint/__tests__/avoid-unwrapped-text.test.js
index 7c667b4a8..a6762b8fd 100644
--- a/eslint/__tests__/avoid-unwrapped-text.test.js
+++ b/eslint/__tests__/avoid-unwrapped-text.test.js
@@ -199,7 +199,7 @@ describe('avoid-unwrapped-text', () => {
 
       {
         code: `
-<View prop={
+<View propText={
   <Trans><Text>foo</Text></Trans>
 }>
   <Bar />
@@ -281,6 +281,170 @@ function MyText({ foo }) {
 }
         `,
       },
+
+      {
+        code: `
+<View>
+  <Text>{'foo'}</Text>
+</View>
+       `,
+      },
+
+      {
+        code: `
+<View>
+  <Text>{foo + 'foo'}</Text>
+</View>
+       `,
+      },
+
+      {
+        code: `
+<View>
+  <Text><Trans>{'foo'}</Trans></Text>
+</View>
+       `,
+      },
+
+      {
+        code: `
+<View>
+  {foo['bar'] && <Bar />}
+</View>
+       `,
+      },
+
+      {
+        code: `
+<View>
+  {(foo === 'bar') && <Bar />}
+</View>
+       `,
+      },
+
+      {
+        code: `
+<View>
+  {(foo !== 'bar') && <Bar />}
+</View>
+       `,
+      },
+
+      {
+        code: `
+<View>
+  <Text>{\`foo\`}</Text>
+</View>
+       `,
+      },
+
+      {
+        code: `
+<View>
+  <Text><Trans>{\`foo\`}</Trans></Text>
+</View>
+       `,
+      },
+
+      {
+        code: `
+<View>
+  <Text>{_(msg\`foo\`)}</Text>
+</View>
+       `,
+      },
+
+      {
+        code: `
+<View>
+  <Text><Trans>{_(msg\`foo\`)}</Trans></Text>
+</View>
+       `,
+      },
+
+      {
+        code: `
+<Foo>
+  <View prop={stuff('foo')}>
+    <Bar />
+  </View>
+</Foo>
+       `,
+      },
+
+      {
+        code: `
+<Foo>
+  <View onClick={() => stuff('foo')}>
+    <Bar />
+  </View>
+</Foo>
+       `,
+      },
+
+      {
+        code: `
+<View>
+  {renderItem('foo')}
+</View>
+       `,
+      },
+
+      {
+        code: `
+<View>
+  {foo === 'foo' && <Bar />}
+</View>
+       `,
+      },
+
+      {
+        code: `
+<View>
+  {foo['foo'] && <Bar />}
+</View>
+       `,
+      },
+
+      {
+        code: `
+<View>
+  {check('foo') && <Bar />}
+</View>
+       `,
+      },
+
+      {
+        code: `
+<View>
+  {foo.bar && <Bar />}
+</View>
+        `,
+      },
+
+      {
+        code: `
+<Text>
+  <Trans>{renderItem('foo')}</Trans>
+</Text>
+       `,
+      },
+
+      {
+        code: `
+<View>
+  {null}
+</View>
+       `,
+      },
+
+      {
+        code: `
+<Text>
+  <Trans>{null}</Trans>
+</Text>
+       `,
+      },
     ],
 
     invalid: [
@@ -455,6 +619,179 @@ function MyText({ foo }) {
         `,
         errors: 1,
       },
+
+      {
+        code: `
+<View>
+  {'foo'}
+</View>
+       `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  {foo && 'foo'}
+</View>
+       `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  <Trans>{'foo'}</Trans>
+</View>
+       `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  {foo && <Trans>{'foo'}</Trans>}
+</View>
+       `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  {10}
+</View>
+       `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  <Trans>{10}</Trans>
+</View>
+       `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  <Trans>{foo + 10}</Trans>
+</View>
+             `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  {\`foo\`}
+</View>
+       `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  <Trans>{\`foo\`}</Trans>
+</View>
+       `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  <Trans>{foo + \`foo\`}</Trans>
+</View>
+       `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  {_(msg\`foo\`)}
+</View>
+       `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  {foo + _(msg\`foo\`)}
+</View>
+       `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  <Trans>{_(msg\`foo\`)}</Trans>
+</View>
+       `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  <Trans>{foo + _(msg\`foo\`)}</Trans>
+</View>
+       `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  <Trans>foo</Trans>
+</View>
+        `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  <Trans><Trans>foo</Trans></Trans>
+</View>
+        `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  <Trans>{foo}</Trans>
+</View>
+        `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View>
+  <Trans>{'foo'}</Trans>
+</View>
+       `,
+        errors: 1,
+      },
+
+      {
+        code: `
+<View prop={
+  <Trans><Text>foo</Text></Trans>
+}>
+  <Bar />
+</View>
+        `,
+        errors: 1,
+      },
     ],
   }
 
diff --git a/eslint/avoid-unwrapped-text.js b/eslint/avoid-unwrapped-text.js
index 79d099f00..eef31f795 100644
--- a/eslint/avoid-unwrapped-text.js
+++ b/eslint/avoid-unwrapped-text.js
@@ -33,6 +33,7 @@ exports.create = function create(context) {
   const options = context.options[0] || {}
   const impliedTextProps = options.impliedTextProps ?? []
   const impliedTextComponents = options.impliedTextComponents ?? []
+  const suggestedTextWrappers = options.suggestedTextWrappers ?? {}
   const textProps = [...impliedTextProps]
   const textComponents = ['Text', ...impliedTextComponents]
 
@@ -54,13 +55,13 @@ exports.create = function create(context) {
             return
           }
           if (tagName === 'Trans') {
-            // Skip over it and check above.
+            // Exit and rely on the traversal for <Trans> JSXElement (code below).
             // TODO: Maybe validate that it's present.
-            parent = parent.parent
-            continue
+            return
           }
-          let message = 'Wrap this string in <Text>.'
-          if (tagName !== 'View') {
+          const suggestedWrapper = suggestedTextWrappers[tagName]
+          let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
+          if (tagName !== 'View' && !suggestedWrapper) {
             message +=
               ' If <' +
               tagName +
@@ -112,6 +113,189 @@ exports.create = function create(context) {
         continue
       }
     },
+    Literal(node) {
+      if (typeof node.value !== 'string' && typeof node.value !== 'number') {
+        return
+      }
+      let parent = node.parent
+      while (parent) {
+        if (parent.type === 'JSXElement') {
+          const tagName = getTagName(parent)
+          if (isTextComponent(tagName)) {
+            // We're good.
+            return
+          }
+          if (tagName === 'Trans') {
+            // Exit and rely on the traversal for <Trans> JSXElement (code below).
+            // TODO: Maybe validate that it's present.
+            return
+          }
+          const suggestedWrapper = suggestedTextWrappers[tagName]
+          let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
+          if (tagName !== 'View' && !suggestedWrapper) {
+            message +=
+              ' If <' +
+              tagName +
+              '> is guaranteed to render <Text>, ' +
+              'rename it to <' +
+              tagName +
+              'Text> or add it to impliedTextComponents.'
+          }
+          context.report({
+            node,
+            message,
+          })
+          return
+        }
+
+        if (parent.type === 'BinaryExpression' && parent.operator === '+') {
+          parent = parent.parent
+          continue
+        }
+
+        if (
+          parent.type === 'JSXExpressionContainer' ||
+          parent.type === 'LogicalExpression'
+        ) {
+          parent = parent.parent
+          continue
+        }
+
+        // Be conservative for other types.
+        return
+      }
+    },
+    TemplateLiteral(node) {
+      let parent = node.parent
+      while (parent) {
+        if (parent.type === 'JSXElement') {
+          const tagName = getTagName(parent)
+          if (isTextComponent(tagName)) {
+            // We're good.
+            return
+          }
+          if (tagName === 'Trans') {
+            // Exit and rely on the traversal for <Trans> JSXElement (code below).
+            // TODO: Maybe validate that it's present.
+            return
+          }
+          const suggestedWrapper = suggestedTextWrappers[tagName]
+          let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
+          if (tagName !== 'View' && !suggestedWrapper) {
+            message +=
+              ' If <' +
+              tagName +
+              '> is guaranteed to render <Text>, ' +
+              'rename it to <' +
+              tagName +
+              'Text> or add it to impliedTextComponents.'
+          }
+          context.report({
+            node,
+            message,
+          })
+          return
+        }
+
+        if (
+          parent.type === 'CallExpression' &&
+          parent.callee.type === 'Identifier' &&
+          parent.callee.name === '_'
+        ) {
+          // This is a user-facing string, keep going up.
+          parent = parent.parent
+          continue
+        }
+
+        if (parent.type === 'BinaryExpression' && parent.operator === '+') {
+          parent = parent.parent
+          continue
+        }
+
+        if (
+          parent.type === 'JSXExpressionContainer' ||
+          parent.type === 'LogicalExpression' ||
+          parent.type === 'TaggedTemplateExpression'
+        ) {
+          parent = parent.parent
+          continue
+        }
+
+        // Be conservative for other types.
+        return
+      }
+    },
+    JSXElement(node) {
+      if (getTagName(node) !== 'Trans') {
+        return
+      }
+      let parent = node.parent
+      while (parent) {
+        if (parent.type === 'JSXElement') {
+          const tagName = getTagName(parent)
+          if (isTextComponent(tagName)) {
+            // We're good.
+            return
+          }
+          if (tagName === 'Trans') {
+            // Exit and rely on the traversal for this JSXElement.
+            // TODO: Should nested <Trans> even be allowed?
+            return
+          }
+          const suggestedWrapper = suggestedTextWrappers[tagName]
+          let message = `Wrap this <Trans> in <${suggestedWrapper ?? 'Text'}>.`
+          if (tagName !== 'View' && !suggestedWrapper) {
+            message +=
+              ' If <' +
+              tagName +
+              '> is guaranteed to render <Text>, ' +
+              'rename it to <' +
+              tagName +
+              'Text> or add it to impliedTextComponents.'
+          }
+          context.report({
+            node,
+            message,
+          })
+          return
+        }
+
+        if (
+          parent.type === 'JSXAttribute' &&
+          parent.name.type === 'JSXIdentifier' &&
+          parent.parent.type === 'JSXOpeningElement' &&
+          parent.parent.parent.type === 'JSXElement'
+        ) {
+          const tagName = getTagName(parent.parent.parent)
+          const propName = parent.name.name
+          if (
+            textProps.includes(tagName + ' ' + propName) ||
+            propName === 'text' ||
+            propName.endsWith('Text')
+          ) {
+            // We're good.
+            return
+          }
+          const message =
+            'Wrap this <Trans> in <Text>.' +
+            ' If `' +
+            propName +
+            '` is guaranteed to be wrapped in <Text>, ' +
+            'rename it to `' +
+            propName +
+            'Text' +
+            '` or add it to impliedTextProps.'
+          context.report({
+            node,
+            message,
+          })
+          return
+        }
+
+        parent = parent.parent
+        continue
+      }
+    },
     ReturnStatement(node) {
       let fnScope = context.getScope()
       while (fnScope && fnScope.type !== 'function') {
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 12b3fe4cb..33d777971 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -12,7 +12,6 @@ import {
   ViewStyle,
 } from 'react-native'
 import {LinearGradient} from 'expo-linear-gradient'
-import {Trans} from '@lingui/macro'
 
 import {android, atoms as a, flatten, tokens, useTheme} from '#/alf'
 import {Props as SVGIconProps} from '#/components/icons/common'
@@ -59,6 +58,10 @@ export type ButtonState = {
 
 export type ButtonContext = VariantProps & ButtonState
 
+type NonTextElements =
+  | React.ReactElement
+  | Iterable<React.ReactElement | null | undefined | boolean>
+
 export type ButtonProps = Pick<
   PressableProps,
   'disabled' | 'onPress' | 'testID'
@@ -68,11 +71,9 @@ export type ButtonProps = Pick<
     testID?: string
     label: string
     style?: StyleProp<ViewStyle>
-    children:
-      | React.ReactNode
-      | string
-      | ((context: ButtonContext) => React.ReactNode | string)
+    children: NonTextElements | ((context: ButtonContext) => NonTextElements)
   }
+
 export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
 
 const Context = React.createContext<VariantProps & ButtonState>({
@@ -404,15 +405,7 @@ export function Button({
         </View>
       )}
       <Context.Provider value={context}>
-        {/* @ts-ignore */}
-        {typeof children === 'string' || children?.type === Trans ? (
-          /* @ts-ignore */
-          <ButtonText>{children}</ButtonText>
-        ) : typeof children === 'function' ? (
-          children(context)
-        ) : (
-          children
-        )}
+        {typeof children === 'function' ? children(context) : children}
       </Context.Provider>
     </Pressable>
   )
diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx
index 605626fef..89913b12b 100644
--- a/src/components/Lists.tsx
+++ b/src/components/Lists.tsx
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
 import {cleanError} from 'lib/strings/errors'
 import {CenteredView} from 'view/com/util/Views'
 import {atoms as a, useBreakpoints, useTheme} from '#/alf'
-import {Button} from '#/components/Button'
+import {Button, ButtonText} from '#/components/Button'
 import {Error} from '#/components/Error'
 import {Loader} from '#/components/Loader'
 import {Text} from '#/components/Typography'
@@ -87,7 +87,9 @@ function ListFooterMaybeError({
             a.py_sm,
           ]}
           onPress={onRetry}>
-          <Trans>Retry</Trans>
+          <ButtonText>
+            <Trans>Retry</Trans>
+          </ButtonText>
         </Button>
       </View>
     </View>
diff --git a/src/components/moderation/LabelsOnMeDialog.tsx b/src/components/moderation/LabelsOnMeDialog.tsx
index 95e3d242b..5cf86644c 100644
--- a/src/components/moderation/LabelsOnMeDialog.tsx
+++ b/src/components/moderation/LabelsOnMeDialog.tsx
@@ -244,7 +244,7 @@ function AppealForm({
           size="medium"
           onPress={onPressBack}
           label={_(msg`Back`)}>
-          {_(msg`Back`)}
+          <ButtonText>{_(msg`Back`)}</ButtonText>
         </Button>
         <Button
           testID="submitBtn"
@@ -253,7 +253,7 @@ function AppealForm({
           size="medium"
           onPress={onSubmit}
           label={_(msg`Submit`)}>
-          {_(msg`Submit`)}
+          <ButtonText>{_(msg`Submit`)}</ButtonText>
         </Button>
       </View>
     </>
diff --git a/src/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx
index 01eca1876..134411903 100644
--- a/src/screens/Login/ChooseAccountForm.tsx
+++ b/src/screens/Login/ChooseAccountForm.tsx
@@ -10,7 +10,7 @@ import {useLoggedOutViewControls} from '#/state/shell/logged-out'
 import * as Toast from '#/view/com/util/Toast'
 import {atoms as a} from '#/alf'
 import {AccountList} from '#/components/AccountList'
-import {Button} from '#/components/Button'
+import {Button, ButtonText} from '#/components/Button'
 import * as TextField from '#/components/forms/TextField'
 import {FormContainer} from './FormContainer'
 
@@ -75,7 +75,7 @@ export const ChooseAccountForm = ({
           color="secondary"
           size="medium"
           onPress={onPressBack}>
-          {_(msg`Back`)}
+          <ButtonText>{_(msg`Back`)}</ButtonText>
         </Button>
         <View style={[a.flex_1]} />
       </View>
diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx
index 6b1340b95..debb39bed 100644
--- a/src/screens/Login/LoginForm.tsx
+++ b/src/screens/Login/LoginForm.tsx
@@ -237,7 +237,9 @@ export const LoginForm = ({
             color="secondary"
             size="medium"
             onPress={onPressRetryConnect}>
-            {_(msg`Retry`)}
+            <ButtonText>
+              <Trans>Retry</Trans>
+            </ButtonText>
           </Button>
         ) : !serviceDescription ? (
           <>
diff --git a/src/screens/Onboarding/Layout.tsx b/src/screens/Onboarding/Layout.tsx
index cfaf20ffe..d48234cca 100644
--- a/src/screens/Onboarding/Layout.tsx
+++ b/src/screens/Onboarding/Layout.tsx
@@ -17,7 +17,7 @@ import {
   useTheme,
   web,
 } from '#/alf'
-import {Button, ButtonIcon} from '#/components/Button'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
 import {createPortalGroup} from '#/components/Portal'
 import {leading, P, Text} from '#/components/Typography'
@@ -73,7 +73,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
             onPress={() => onboardDispatch({type: 'skip'})}
             // DEV ONLY
             label="Clear onboarding state">
-            Clear
+            <ButtonText>Clear</ButtonText>
           </Button>
         </View>
       )}
diff --git a/src/view/com/auth/server-input/index.tsx b/src/view/com/auth/server-input/index.tsx
index 2380fffe2..0d64650dd 100644
--- a/src/view/com/auth/server-input/index.tsx
+++ b/src/view/com/auth/server-input/index.tsx
@@ -167,7 +167,7 @@ export function ServerInputDialog({
               size="small"
               onPress={() => control.close()}
               label={_(msg`Done`)}>
-              {_(msg`Done`)}
+              <ButtonText>{_(msg`Done`)}</ButtonText>
             </Button>
           </View>
         </View>
diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/view/screens/Settings/ExportCarDialog.tsx
index 3ec37e85e..e901fb090 100644
--- a/src/view/screens/Settings/ExportCarDialog.tsx
+++ b/src/view/screens/Settings/ExportCarDialog.tsx
@@ -92,7 +92,9 @@ export function ExportCarDialog({
               size={gtMobile ? 'small' : 'large'}
               onPress={() => control.close()}
               label={_(msg`Done`)}>
-              {_(msg`Done`)}
+              <ButtonText>
+                <Trans>Done</Trans>
+              </ButtonText>
             </Button>
           </View>
 
diff --git a/src/view/screens/Storybook/Buttons.tsx b/src/view/screens/Storybook/Buttons.tsx
index ad2fff3f4..cae8ec314 100644
--- a/src/view/screens/Storybook/Buttons.tsx
+++ b/src/view/screens/Storybook/Buttons.tsx
@@ -4,15 +4,15 @@ import {View} from 'react-native'
 import {atoms as a} from '#/alf'
 import {
   Button,
-  ButtonVariant,
   ButtonColor,
   ButtonIcon,
   ButtonText,
+  ButtonVariant,
 } from '#/components/Button'
-import {H1} from '#/components/Typography'
 import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight'
 import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
 import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
+import {H1} from '#/components/Typography'
 
 export function Buttons() {
   return (
@@ -29,7 +29,7 @@ export function Buttons() {
                   color={color as ButtonColor}
                   size="large"
                   label="Click here">
-                  Button
+                  <ButtonText>Button</ButtonText>
                 </Button>
                 <Button
                   disabled
@@ -37,7 +37,7 @@ export function Buttons() {
                   color={color as ButtonColor}
                   size="large"
                   label="Click here">
-                  Button
+                  <ButtonText>Button</ButtonText>
                 </Button>
               </React.Fragment>
             ))}
@@ -54,7 +54,7 @@ export function Buttons() {
                     color={name as ButtonColor}
                     size="large"
                     label="Click here">
-                    Button
+                    <ButtonText>Button</ButtonText>
                   </Button>
                   <Button
                     disabled
@@ -62,7 +62,7 @@ export function Buttons() {
                     color={name as ButtonColor}
                     size="large"
                     label="Click here">
-                    Button
+                    <ButtonText>Button</ButtonText>
                   </Button>
                 </React.Fragment>
               ),
@@ -77,7 +77,7 @@ export function Buttons() {
                     color={name as ButtonColor}
                     size="large"
                     label="Click here">
-                    Button
+                    <ButtonText>Button</ButtonText>
                   </Button>
                   <Button
                     disabled
@@ -85,7 +85,7 @@ export function Buttons() {
                     color={name as ButtonColor}
                     size="large"
                     label="Click here">
-                    Button
+                    <ButtonText>Button</ButtonText>
                   </Button>
                 </React.Fragment>
               ),
diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx
index 5c5e480fe..4722784ca 100644
--- a/src/view/screens/Storybook/Dialogs.tsx
+++ b/src/view/screens/Storybook/Dialogs.tsx
@@ -3,7 +3,7 @@ import {View} from 'react-native'
 
 import {useDialogStateControlContext} from '#/state/dialogs'
 import {atoms as a} from '#/alf'
-import {Button} from '#/components/Button'
+import {Button, ButtonText} from '#/components/Button'
 import * as Dialog from '#/components/Dialog'
 import * as Prompt from '#/components/Prompt'
 import {H3, P} from '#/components/Typography'
@@ -26,7 +26,7 @@ export function Dialogs() {
           basic.open()
         }}
         label="Open basic dialog">
-        Open all dialogs
+        <ButtonText>Open all dialogs</ButtonText>
       </Button>
 
       <Button
@@ -37,7 +37,7 @@ export function Dialogs() {
           scrollable.open()
         }}
         label="Open basic dialog">
-        Open scrollable dialog
+        <ButtonText>Open scrollable dialog</ButtonText>
       </Button>
 
       <Button
@@ -48,7 +48,7 @@ export function Dialogs() {
           basic.open()
         }}
         label="Open basic dialog">
-        Open basic dialog
+        <ButtonText>Open basic dialog</ButtonText>
       </Button>
 
       <Button
@@ -57,7 +57,7 @@ export function Dialogs() {
         size="small"
         onPress={() => prompt.open()}
         label="Open prompt">
-        Open prompt
+        <ButtonText>Open prompt</ButtonText>
       </Button>
 
       <Prompt.Outer control={prompt}>
@@ -102,7 +102,7 @@ export function Dialogs() {
               size="small"
               onPress={closeAllDialogs}
               label="Close all dialogs">
-              Close all dialogs
+              <ButtonText>Close all dialogs</ButtonText>
             </Button>
             <View style={{height: 1000}} />
             <View style={[a.flex_row, a.justify_end]}>
@@ -116,7 +116,7 @@ export function Dialogs() {
                   })
                 }
                 label="Open basic dialog">
-                Close dialog
+                <ButtonText>Close dialog</ButtonText>
               </Button>
             </View>
           </View>
diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx
index b771ad5e0..1e4efdcc7 100644
--- a/src/view/screens/Storybook/Forms.tsx
+++ b/src/view/screens/Storybook/Forms.tsx
@@ -2,7 +2,7 @@ import React from 'react'
 import {View} from 'react-native'
 
 import {atoms as a} from '#/alf'
-import {Button} from '#/components/Button'
+import {Button, ButtonText} from '#/components/Button'
 import {DateField, LabelText} from '#/components/forms/DateField'
 import * as TextField from '#/components/forms/TextField'
 import * as Toggle from '#/components/forms/Toggle'
@@ -191,7 +191,7 @@ export function Forms() {
           setToggleGroupBValues(['a', 'b'])
           setToggleGroupCValues(['a'])
         }}>
-        Reset all toggles
+        <ButtonText>Reset all toggles</ButtonText>
       </Button>
 
       <View style={[a.gap_md, a.align_start, a.w_full]}>
diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx
index 3a2e2f369..35a666601 100644
--- a/src/view/screens/Storybook/index.tsx
+++ b/src/view/screens/Storybook/index.tsx
@@ -1,22 +1,21 @@
 import React from 'react'
 import {View} from 'react-native'
-import {CenteredView, ScrollView} from '#/view/com/util/Views'
 
-import {atoms as a, useTheme, ThemeProvider} from '#/alf'
 import {useSetThemePrefs} from '#/state/shell'
-import {Button} from '#/components/Button'
-
-import {Theming} from './Theming'
-import {Typography} from './Typography'
-import {Spacing} from './Spacing'
+import {CenteredView, ScrollView} from '#/view/com/util/Views'
+import {atoms as a, ThemeProvider, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import {Breakpoints} from './Breakpoints'
 import {Buttons} from './Buttons'
-import {Links} from './Links'
-import {Forms} from './Forms'
 import {Dialogs} from './Dialogs'
-import {Breakpoints} from './Breakpoints'
-import {Shadows} from './Shadows'
+import {Forms} from './Forms'
 import {Icons} from './Icons'
+import {Links} from './Links'
 import {Menus} from './Menus'
+import {Shadows} from './Shadows'
+import {Spacing} from './Spacing'
+import {Theming} from './Theming'
+import {Typography} from './Typography'
 
 export function Storybook() {
   const t = useTheme()
@@ -33,7 +32,7 @@ export function Storybook() {
               size="small"
               label='Set theme to "system"'
               onPress={() => setColorMode('system')}>
-              System
+              <ButtonText>System</ButtonText>
             </Button>
             <Button
               variant="solid"
@@ -41,7 +40,7 @@ export function Storybook() {
               size="small"
               label='Set theme to "light"'
               onPress={() => setColorMode('light')}>
-              Light
+              <ButtonText>Light</ButtonText>
             </Button>
             <Button
               variant="solid"
@@ -52,7 +51,7 @@ export function Storybook() {
                 setColorMode('dark')
                 setDarkTheme('dim')
               }}>
-              Dim
+              <ButtonText>Dim</ButtonText>
             </Button>
             <Button
               variant="solid"
@@ -63,7 +62,7 @@ export function Storybook() {
                 setColorMode('dark')
                 setDarkTheme('dark')
               }}>
-              Dark
+              <ButtonText>Dark</ButtonText>
             </Button>
           </View>