From 75bc87f8ff3afc191ac7876aaf5ffaf6f7635880 Mon Sep 17 00:00:00 2001 From: Hunter Copp Date: Tue, 17 Feb 2026 12:39:29 -0500 Subject: [PATCH 1/2] feat: figma code connect and doc updates (#392) * feat: update carousel code connect * Update carousel pagination dot color * Revert "Update carousel pagination dot color" This reverts commit bc33f422f0842d8b9666bec2a6f06f1923274916. * Update tray figma * Update slidebutton docs * Fix lint * Drop nested child properties * Improve web tray code connect * Update text --- .../inputs/SlideButton/_mobileExamples.mdx | 137 +++++++++++--- .../inputs/SlideButton/mobileMetadata.json | 7 +- .../src/carousel/__figma__/Carousel.figma.tsx | 15 +- .../overlays/tray/__figma__/Tray.figma.tsx | 167 +++++++++--------- .../src/carousel/__figma__/Carousel.figma.tsx | 15 +- .../overlays/tray/__figma__/Tray.figma.tsx | 165 +++++++++++++++++ 6 files changed, 394 insertions(+), 112 deletions(-) create mode 100644 packages/web/src/overlays/tray/__figma__/Tray.figma.tsx diff --git a/apps/docs/docs/components/inputs/SlideButton/_mobileExamples.mdx b/apps/docs/docs/components/inputs/SlideButton/_mobileExamples.mdx index a6efccf54..64e3c8973 100644 --- a/apps/docs/docs/components/inputs/SlideButton/_mobileExamples.mdx +++ b/apps/docs/docs/components/inputs/SlideButton/_mobileExamples.mdx @@ -1,9 +1,9 @@ import useBaseUrl from '@docusaurus/useBaseUrl'; import ThemedImage from '@theme/ThemedImage'; -### SlideButton +## Basics -Use the `onChange` prop to listen and make changes to the `checked` state. +Use the `onChange` callback to update the `checked` state. This is the primary callback that controls both the visual and accessible state of the component. console.log('Completed')} uncheckedLabel="Swipe to confirm" checkedLabel="Confirming..." /> @@ -33,9 +32,9 @@ function Example() { } ``` -### Negative SlideButton +## Variants -You can use the `variant` prop to change the color of the button. +Use the `variant` prop to change the color of the button. The default variant is `primary`. Available variants are `negative` and `positive`. console.log('Completed')} uncheckedLabel="Swipe to confirm" checkedLabel="Confirming..." variant="negative" @@ -66,7 +64,7 @@ function Example() { } ``` -### Compact SlideButton +## Compact Use the `compact` prop to reduce the height, border-radius and padding of the button: @@ -78,7 +76,6 @@ function Example() { console.log('Completed')} uncheckedLabel="Swipe to confirm" checkedLabel="Confirming..." compact @@ -87,9 +84,31 @@ function Example() { } ``` -### Auto Complete on Threshold +## Disabled + +Use the `disabled` prop to prevent interaction. This works for both unchecked and checked states. + +```jsx +function Example() { + return ( + + + + + ); +} +``` + +## Auto Complete on Threshold -You can set the button to automatically complete when the slide reaches the threshold: +By default, the user must release the handle past the threshold to complete. Set `autoCompleteSlideOnThresholdMet` to automatically complete as soon as the threshold is reached, without requiring release. + +You can also adjust the threshold via `checkThreshold` (a value from 0 to 1, defaulting to 0.7). ```jsx function Example() { @@ -99,7 +118,6 @@ function Example() { console.log('Completed')} uncheckedLabel="Swipe to confirm" checkedLabel="Confirming..." autoCompleteSlideOnThresholdMet @@ -108,9 +126,41 @@ function Example() { } ``` -### Custom Nodes on SlideButton +## Callback Lifecycle + +SlideButton fires callbacks in a specific order during the slide gesture: + +1. `onSlideStart` -- when the gesture begins +2. `onChange` -- when the slide completes past the threshold (sets `checked` to `true`) +3. `onSlideComplete` -- immediately after `onChange` +4. `onSlideEnd` -- always fires last + +If the user releases before the threshold, `onSlideCancel` fires instead, followed by `onSlideEnd`. + +**Important:** Always use `onChange` to manage the `checked` state. The `checked` prop drives the component's `accessibilityLabel` (switching between `uncheckedLabel` and `checkedLabel`), so failing to update it means screen readers won't announce the state change. Use `onSlideComplete` only for supplementary side effects (e.g. analytics, haptic feedback) that don't affect accessible state. + +```jsx +function Example() { + const [checked, setChecked] = useState(false); + + return ( + console.log('Started')} + onSlideComplete={() => console.log('Completed')} + onSlideCancel={() => console.log('Cancelled')} + onSlideEnd={() => console.log('Ended')} + uncheckedLabel="Swipe to confirm" + checkedLabel="Confirming..." + /> + ); +} +``` + +## Custom Nodes -You can also use SlideButton with custom nodes. +Use `startUncheckedNode` and `endCheckedNode` to replace the default arrow icon and loading indicator on the handle. ```jsx @@ -132,19 +182,41 @@ function Example() { console.log('Completed')} uncheckedLabel="Swipe to enable notifications" checkedLabel="Enabling..." - startUncheckedNode={} - endCheckedNode={} + startUncheckedNode={} + endCheckedNode={} /> ); } ``` -### Custom Background and Handle Components +## Labels as Nodes + +The `uncheckedLabel` and `checkedLabel` props accept `ReactNode`, so you can pass custom styled text or other components. When using non-string labels, the component uses `accessibilityLabelledBy` to associate the handle with the container element, so ensure your label nodes contain meaningful text content. -You can customize the background and handle components of the SlideButton. +```jsx +function Example() { + const [checked, setChecked] = useState(false); + + return ( + Swipe to confirm} + checkedLabel={ + + Confirming... + + } + /> + ); +} +``` + +## Custom Background and Handle Components + +You can fully customize the background and handle by providing your own components via `SlideButtonBackgroundComponent` and `SlideButtonHandleComponent`. Your components receive typed props (`SlideButtonBackgroundProps` and `SlideButtonHandleProps`) including a `progress` spring value and the current `checked` state. ```jsx @@ -208,3 +280,28 @@ function Example() { ); } ``` + +## Accessibility + +SlideButton has built-in accessibility support. The component automatically derives its `accessibilityLabel` from the `checked` state -- displaying `uncheckedLabel` when unchecked and `checkedLabel` when checked. It also registers an `activate` accessibility action so screen readers can trigger the slide without performing a gesture. + +**Use `onChange` as your primary callback.** The `onChange` callback updates the `checked` prop, which controls the accessible label. Placing critical logic in `onSlideComplete` without updating `checked` via `onChange` will leave the accessible state stale, meaning screen readers won't announce the confirmation. + +When providing a custom `SlideButtonHandleComponent`, always spread the incoming props to preserve the built-in `accessibilityActions` and `onAccessibilityAction` handlers, and set `accessibilityLabel` and `accessibilityRole="button"` on the handle element. + +When using `ReactNode` labels instead of strings, the component uses `accessibilityLabelledBy` to link to the container element, so ensure your custom label nodes contain meaningful text. + +```jsx +function Example() { + const [checked, setChecked] = useState(false); + + return ( + + ); +} +``` diff --git a/apps/docs/docs/components/inputs/SlideButton/mobileMetadata.json b/apps/docs/docs/components/inputs/SlideButton/mobileMetadata.json index 31631d3ce..2c8b9fc38 100644 --- a/apps/docs/docs/components/inputs/SlideButton/mobileMetadata.json +++ b/apps/docs/docs/components/inputs/SlideButton/mobileMetadata.json @@ -21,5 +21,10 @@ "url": "/components/inputs/Pressable/" } ], - "dependencies": [] + "dependencies": [ + { + "name": "react-native-gesture-handler", + "version": "^2.16.2" + } + ] } diff --git a/packages/mobile/src/carousel/__figma__/Carousel.figma.tsx b/packages/mobile/src/carousel/__figma__/Carousel.figma.tsx index e538ae0e9..26597b66f 100644 --- a/packages/mobile/src/carousel/__figma__/Carousel.figma.tsx +++ b/packages/mobile/src/carousel/__figma__/Carousel.figma.tsx @@ -5,9 +5,20 @@ figma.connect( Carousel, 'https://www.figma.com/design/k5CtyJccNQUGMI5bI4lJ2g/%E2%9C%A8-CDS-Components?node-id=48671-10433', { + variant: { platform: 'mobile' }, imports: ["import { Carousel, CarouselItem } from '@coinbase/cds-mobile/carousel'"], - example: () => ( - + props: { + title: figma.boolean('show header', { + true: figma.string('title'), + false: undefined, + }), + hidePagination: figma.boolean('show pagination', { + true: undefined, + false: true, + }), + }, + example: ({ title, hidePagination }) => ( + {/* Item content */} {/* Item content */} {/* Item content */} diff --git a/packages/mobile/src/overlays/tray/__figma__/Tray.figma.tsx b/packages/mobile/src/overlays/tray/__figma__/Tray.figma.tsx index dd92e1c8a..3406b6f83 100644 --- a/packages/mobile/src/overlays/tray/__figma__/Tray.figma.tsx +++ b/packages/mobile/src/overlays/tray/__figma__/Tray.figma.tsx @@ -1,46 +1,47 @@ -import { useRef, useState } from 'react'; +import React, { useState } from 'react'; import { figma } from '@figma/code-connect'; import { Button } from '../../../buttons/Button'; import { Box, VStack } from '../../../layout'; -import { TextBody, TextTitle1 } from '../../../typography'; -import { Tray, TrayStickyFooter } from '../Tray'; +import { StickyFooter } from '../../../sticky-footer/StickyFooter'; +import { Text } from '../../../typography/Text'; +import { Tray } from '../Tray'; figma.connect( Tray, 'https://www.figma.com/design/k5CtyJccNQUGMI5bI4lJ2g/✨-CDS-Components?node-id=14729-33327&m=dev', { - imports: ["import { Tray } from '@coinbase/cds-mobile/overlays/tray/Tray'"], + imports: [ + "import { Tray } from '@coinbase/cds-mobile/overlays/tray/Tray'", + "import { StickyFooter } from '@coinbase/cds-mobile/sticky-footer/StickyFooter'", + ], props: { title: figma.boolean('show section header', { true: figma.textContent('SectionHeader'), false: undefined, }), - stickyFooter: figma.children('StickyFooter'), content: figma.children('.Select Option*'), }, - example: function TrayExample({ stickyFooter, content, title }) { + example: function TrayExample({ content, title }) { const [isTrayVisible, setIsTrayVisible] = useState(false); - const trayRef = useRef(null); return ( <> - + {isTrayVisible && ( ( + + + + )} + handleBarVariant="inside" onCloseComplete={() => setIsTrayVisible(false)} - onVisibilityChange={() => {}} title={title} > - {({ handleClose }) => ( - - {content} - {stickyFooter} - - )} + {content} )} @@ -59,37 +60,31 @@ figma.connect( true: figma.children('Spot Square/blockchain'), false: undefined, }), - title: figma.textContent('SectionHeader'), - stickyFooter: figma.children('StickyFooter'), + sectionTitle: figma.textContent('SectionHeader'), }, - example: function TrayExample({ pictogram, title, stickyFooter }) { + example: function TrayExample({ pictogram, sectionTitle }) { const [isTrayVisible, setIsTrayVisible] = useState(false); - const trayRef = useRef(null); return ( <> - + {isTrayVisible && ( setIsTrayVisible(false)} - onVisibilityChange={() => {}} - title={title} - > - {({ handleClose }) => ( - + title={ + {pictogram} - - Lorem ipsum dolor sit amet consectetur. Lacus vitae vulputate maecenas sed ac - cursus enim elementum euismod. Ac vulputate gravida mauris id nulla imperdiet - eget. Dictum vitae enim eget ut. Maecenas hendrerit amet integer sagittis cras. - Fermentum ultricies malesuada interdum - - {stickyFooter} - - )} + {sectionTitle} + + } + > + + + Content goes here. + + )} @@ -102,44 +97,45 @@ figma.connect( Tray, 'https://www.figma.com/design/k5CtyJccNQUGMI5bI4lJ2g/✨-CDS-Components?node-id=14729-33505&m=dev', { - imports: ["import { Tray } from '@coinbase/cds-mobile/overlays/tray/Tray'"], + imports: [ + "import { Tray } from '@coinbase/cds-mobile/overlays/tray/Tray'", + "import { StickyFooter } from '@coinbase/cds-mobile/sticky-footer/StickyFooter'", + ], props: { spotRectangle: figma.instance('spot rectangle'), title: figma.string('title'), body: figma.string('body'), - stickyFooter: figma.children('StickyFooter'), }, - example: function TrayExample({ spotRectangle, title, body, stickyFooter }) { + example: function TrayExample({ spotRectangle, title, body }) { const [isTrayVisible, setIsTrayVisible] = useState(false); - const trayRef = useRef(null); return ( <> - + {isTrayVisible && ( ( + + + + )} + handleBarVariant="inside" onCloseComplete={() => setIsTrayVisible(false)} - onVisibilityChange={() => {}} > - {({ handleClose }) => ( - - - - {spotRectangle} - - - {title} - - - {body} - - - {stickyFooter} - - )} + + + {spotRectangle} + + + {title} + + + {body} + + )} @@ -158,20 +154,17 @@ figma.connect( }, example: function TrayExample({ children }) { const [isTrayVisible, setIsTrayVisible] = useState(false); - const trayRef = useRef(null); return ( <> - + {isTrayVisible && ( setIsTrayVisible(false)} - onVisibilityChange={() => {}} + title="Title" > - {({ handleClose }) => {children}} + {children} )} @@ -184,37 +177,37 @@ figma.connect( Tray, 'https://www.figma.com/design/k5CtyJccNQUGMI5bI4lJ2g/✨-CDS-Components?node-id=14729-77780&m=dev', { - imports: ["import { Tray } from '@coinbase/cds-mobile/overlays/tray/Tray'"], + imports: [ + "import { Tray } from '@coinbase/cds-mobile/overlays/tray/Tray'", + "import { StickyFooter } from '@coinbase/cds-mobile/sticky-footer/StickyFooter'", + ], props: { content: figma.instance('content'), - stickyFooter: figma.children('StickyFooter'), title: figma.boolean('show section header', { true: figma.textContent('SectionHeader'), false: undefined, }), }, - example: function TrayExample({ content, stickyFooter, title }) { + example: function TrayExample({ content, title }) { const [isTrayVisible, setIsTrayVisible] = useState(false); - const trayRef = useRef(null); return ( <> - + {isTrayVisible && ( ( + + + + )} + handleBarVariant="inside" onCloseComplete={() => setIsTrayVisible(false)} - onVisibilityChange={() => {}} title={title} > - {({ handleClose }) => ( - - {content} - {stickyFooter} - - )} + {content} )} diff --git a/packages/web/src/carousel/__figma__/Carousel.figma.tsx b/packages/web/src/carousel/__figma__/Carousel.figma.tsx index b54847416..cb248f543 100644 --- a/packages/web/src/carousel/__figma__/Carousel.figma.tsx +++ b/packages/web/src/carousel/__figma__/Carousel.figma.tsx @@ -6,9 +6,20 @@ figma.connect( Carousel, 'https://www.figma.com/design/k5CtyJccNQUGMI5bI4lJ2g/%E2%9C%A8-CDS-Components?node-id=48671-10433', { + variant: { platform: 'desktop' }, imports: ["import { Carousel, CarouselItem } from '@coinbase/cds-web/carousel'"], - example: () => ( - + props: { + title: figma.boolean('show header', { + true: figma.string('title'), + false: undefined, + }), + hidePagination: figma.boolean('show pagination', { + true: undefined, + false: true, + }), + }, + example: ({ title, hidePagination }) => ( + {/* Item content */} {/* Item content */} {/* Item content */} diff --git a/packages/web/src/overlays/tray/__figma__/Tray.figma.tsx b/packages/web/src/overlays/tray/__figma__/Tray.figma.tsx new file mode 100644 index 000000000..59633c6c2 --- /dev/null +++ b/packages/web/src/overlays/tray/__figma__/Tray.figma.tsx @@ -0,0 +1,165 @@ +import { useId, useState } from 'react'; +import { figma } from '@figma/code-connect'; + +import { Button } from '../../../buttons'; +import { useBreakpoints } from '../../../hooks/useBreakpoints'; +import { Pictogram } from '../../../illustrations/Pictogram'; +import { Box } from '../../../layout'; +import { VStack } from '../../../layout/VStack'; +import { PageFooter } from '../../../page/PageFooter'; +import { Text } from '../../../typography/Text'; +import { Tray } from '../Tray'; + +const FIGMA_URL = + 'https://www.figma.com/design/k5CtyJccNQUGMI5bI4lJ2g/%E2%9C%A8-CDS-Components?node-id=74148-11495&m=dev'; + +figma.connect(Tray, FIGMA_URL, { + variant: { type: 'standard' }, + imports: [ + "import { Tray } from '@coinbase/cds-web/overlays/tray/Tray'", + "import { PageFooter } from '@coinbase/cds-web/page/PageFooter'", + "import { useBreakpoints } from '@coinbase/cds-web/hooks/useBreakpoints'", + ], + props: { + title: figma.textContent('SectionHeader'), + }, + example: function StandardExample({ title }) { + const [visible, setVisible] = useState(false); + const { isPhone } = useBreakpoints(); + return ( + <> + + {visible && ( + ( + + Close + + } + justifyContent={isPhone ? 'center' : 'flex-end'} + /> + )} + onCloseComplete={() => setVisible(false)} + pin={isPhone ? 'bottom' : 'right'} + showHandleBar={isPhone} + title={title} + > + + Content goes here. + + + )} + + ); + }, +}); + +figma.connect(Tray, FIGMA_URL, { + variant: { type: 'illustration' }, + imports: [ + "import { Tray } from '@coinbase/cds-web/overlays/tray/Tray'", + "import { Pictogram } from '@coinbase/cds-web/illustrations/Pictogram'", + "import { useBreakpoints } from '@coinbase/cds-web/hooks/useBreakpoints'", + ], + props: { + sectionTitle: figma.textContent('SectionHeader'), + }, + example: function IllustrationExample({ sectionTitle }) { + const [visible, setVisible] = useState(false); + const { isPhone } = useBreakpoints(); + const titleId = useId(); + return ( + <> + + {visible && ( + setVisible(false)} + pin={isPhone ? 'bottom' : 'right'} + showHandleBar={isPhone} + title={ + + + + {sectionTitle} + + + } + > + + Content goes here. + + + )} + + ); + }, +}); + +figma.connect(Tray, FIGMA_URL, { + variant: { type: 'full-bleed image' }, + imports: [ + "import { Tray } from '@coinbase/cds-web/overlays/tray/Tray'", + "import { useBreakpoints } from '@coinbase/cds-web/hooks/useBreakpoints'", + ], + props: { + sectionTitle: figma.textContent('SectionHeader'), + }, + example: function FullBleedImageExample({ sectionTitle }) { + const [visible, setVisible] = useState(false); + const { isPhone } = useBreakpoints(); + const titleId = useId(); + return ( + <> + + {visible && ( + + {sectionTitle} + + } + onCloseComplete={() => setVisible(false)} + pin={isPhone ? 'bottom' : 'right'} + showHandleBar={isPhone} + styles={{ + handleBar: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + zIndex: 1, + }, + closeButton: { + position: 'absolute', + top: 'var(--space-4)', + right: 'var(--space-4)', + zIndex: 1, + }, + header: { paddingTop: 0 }, + }} + title={ + + Full Bleed + + } + > + + Content goes here. + + + )} + + ); + }, +}); From f2a6486c6985d324b7d487d24d7480892c0e3a4e Mon Sep 17 00:00:00 2001 From: Hunter Copp Date: Tue, 17 Feb 2026 18:51:45 -0500 Subject: [PATCH 2/2] feat: expo template (#402) * feat: expo template * Fix formatting * Include metro * Add Inter --- .../installation/_mobileContent.mdx | 4 + .../installation/_webContent.mdx | 4 + .../templates/_mobileContent.mdx | 105 ++++++++++++++ .../docs/getting-started/templates/index.mdx | 13 +- .../templates/mobileMetadata.json | 3 + .../static/img/logos/frameworks/expo_dark.png | Bin 0 -> 7040 bytes .../img/logos/frameworks/expo_light.png | Bin 0 -> 7041 bytes templates/expo-app/.gitignore | 33 +++++ templates/expo-app/.nvmrc | 1 + templates/expo-app/.yarnrc.yml | 4 + templates/expo-app/App.tsx | 137 ++++++++++++++++++ templates/expo-app/README.md | 49 +++++++ templates/expo-app/app.json | 15 ++ templates/expo-app/babel.config.js | 6 + .../expo-app/components/AssetCarousel.tsx | 54 +++++++ templates/expo-app/components/AssetChart.tsx | 116 +++++++++++++++ templates/expo-app/components/AssetList.tsx | 67 +++++++++ templates/expo-app/components/CardList.tsx | 82 +++++++++++ templates/expo-app/components/Navbar.tsx | 33 +++++ .../expo-app/components/TabBarButton.tsx | 44 ++++++ templates/expo-app/metro.config.js | 23 +++ templates/expo-app/package.json | 47 ++++++ templates/expo-app/tsconfig.json | 8 + 23 files changed, 845 insertions(+), 3 deletions(-) create mode 100644 apps/docs/docs/getting-started/templates/_mobileContent.mdx create mode 100644 apps/docs/docs/getting-started/templates/mobileMetadata.json create mode 100644 apps/docs/static/img/logos/frameworks/expo_dark.png create mode 100644 apps/docs/static/img/logos/frameworks/expo_light.png create mode 100644 templates/expo-app/.gitignore create mode 100644 templates/expo-app/.nvmrc create mode 100644 templates/expo-app/.yarnrc.yml create mode 100644 templates/expo-app/App.tsx create mode 100644 templates/expo-app/README.md create mode 100644 templates/expo-app/app.json create mode 100644 templates/expo-app/babel.config.js create mode 100644 templates/expo-app/components/AssetCarousel.tsx create mode 100644 templates/expo-app/components/AssetChart.tsx create mode 100644 templates/expo-app/components/AssetList.tsx create mode 100644 templates/expo-app/components/CardList.tsx create mode 100644 templates/expo-app/components/Navbar.tsx create mode 100644 templates/expo-app/components/TabBarButton.tsx create mode 100644 templates/expo-app/metro.config.js create mode 100644 templates/expo-app/package.json create mode 100644 templates/expo-app/tsconfig.json diff --git a/apps/docs/docs/getting-started/installation/_mobileContent.mdx b/apps/docs/docs/getting-started/installation/_mobileContent.mdx index 74f6580e8..60ab85c71 100644 --- a/apps/docs/docs/getting-started/installation/_mobileContent.mdx +++ b/apps/docs/docs/getting-started/installation/_mobileContent.mdx @@ -5,6 +5,10 @@ import { MDXArticle } from '@site/src/components/page/MDXArticle'; ## Installation +:::tip Starting a new project? +Check out our [templates](/getting-started/templates) for pre-configured starter apps with CDS already set up. +::: + To install the CDS library for React Native applications, run the following command: ```bash diff --git a/apps/docs/docs/getting-started/installation/_webContent.mdx b/apps/docs/docs/getting-started/installation/_webContent.mdx index df0695e27..b6acc2e1b 100644 --- a/apps/docs/docs/getting-started/installation/_webContent.mdx +++ b/apps/docs/docs/getting-started/installation/_webContent.mdx @@ -5,6 +5,10 @@ import { MDXArticle } from '@site/src/components/page/MDXArticle'; ## Installation +:::tip Starting a new project? +Check out our [templates](/getting-started/templates) for pre-configured starter apps with CDS already set up. +::: + To install the CDS library for React web applications, run the following command: ```bash diff --git a/apps/docs/docs/getting-started/templates/_mobileContent.mdx b/apps/docs/docs/getting-started/templates/_mobileContent.mdx new file mode 100644 index 000000000..5798d8de8 --- /dev/null +++ b/apps/docs/docs/getting-started/templates/_mobileContent.mdx @@ -0,0 +1,105 @@ +import { MDXSection } from '@site/src/components/page/MDXSection'; +import { MDXArticle } from '@site/src/components/page/MDXArticle'; +import { TemplateCard } from '@site/src/components/page/TemplateCard'; +import { HStack } from '@coinbase/cds-web/layout'; +import ThemedImage from '@theme/ThemedImage'; + + + + +## Get started + +The easiest way to get started with CDS on mobile is with a template. The Expo template includes the required CDS packages, dependencies, and pre-configured settings with a working example application to help you start building. + + + + } + /> + + + + + + + + +## Installation + +To create a new project from the template, use `gitpick` to bootstrap your application: + +### Expo + +```bash +npx -y gitpick coinbase/cds/tree/master/templates/expo-app cds-expo +cd cds-expo +``` + + + + + + + +## Setup + +After creating your project, install dependencies and start developing: + +```bash +# We suggest using nvm to manage Node.js versions +nvm install +nvm use + +# Enable corepack for package manager setup +corepack enable + +# Install dependencies +yarn + +# Start development server +yarn start +``` + + + + + + + +## What's included + +All templates come pre-configured with: + +- Latest CDS packages (`@coinbase/cds-mobile`, `@coinbase/cds-icons`, etc.) +- TypeScript configuration +- Example components demonstrating common UI patterns +- Theme setup with CDS default theme +- Navigation with React Navigation + + + + + + + +## Next steps + +After setting up your template, learn how to customize and extend CDS: + +- [Theming](/getting-started/theming) - Customize colors, spacing, and typography +- [Installation](/getting-started/installation) - Manual installation and setup options + + + diff --git a/apps/docs/docs/getting-started/templates/index.mdx b/apps/docs/docs/getting-started/templates/index.mdx index c126ec318..821b28e69 100644 --- a/apps/docs/docs/getting-started/templates/index.mdx +++ b/apps/docs/docs/getting-started/templates/index.mdx @@ -1,7 +1,7 @@ --- id: templates title: Templates -platform_switcher_options: { web: true, mobile: false } +platform_switcher_options: { web: true, mobile: true } hide_title: true --- @@ -11,10 +11,17 @@ import { ContentHeader } from '@site/src/components/page/ContentHeader'; import { ContentPageContainer } from '@site/src/components/page/ContentPageContainer'; import WebContent, { toc as webContentToc } from './_webContent.mdx'; +import MobileContent, { toc as mobileContentToc } from './_mobileContent.mdx'; import webMetadata from './webMetadata.json'; +import mobileMetadata from './mobileMetadata.json'; - - } webContentToc={webContentToc} /> + + } + webContentToc={webContentToc} + mobileContent={} + mobileContentToc={mobileContentToc} + /> diff --git a/apps/docs/docs/getting-started/templates/mobileMetadata.json b/apps/docs/docs/getting-started/templates/mobileMetadata.json new file mode 100644 index 000000000..da19f1de1 --- /dev/null +++ b/apps/docs/docs/getting-started/templates/mobileMetadata.json @@ -0,0 +1,3 @@ +{ + "description": "Get started quickly with a pre-built Expo template configured with CDS components and best practices." +} diff --git a/apps/docs/static/img/logos/frameworks/expo_dark.png b/apps/docs/static/img/logos/frameworks/expo_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..52ae390f3bb6e7ebc2d35b86879e07ad9b3facd9 GIT binary patch literal 7040 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 literal 0 HcmV?d00001 diff --git a/apps/docs/static/img/logos/frameworks/expo_light.png b/apps/docs/static/img/logos/frameworks/expo_light.png new file mode 100644 index 0000000000000000000000000000000000000000..ef1c5458bc6f88226a4cf536017a17ed4cec1186 GIT binary patch literal 7041 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 { + const theme = useTheme(); + return ( + {children} + ); +}); + +export default function App() { + const [activeColorScheme, setActiveColorScheme] = useState('light'); + const [fontsLoaded] = useFonts({ + CoinbaseIcons: require('@coinbase/cds-icons/fonts/native/CoinbaseIcons.ttf'), + Inter_400Regular, + Inter_600SemiBold, + }); + + const toggleColorScheme = useCallback( + () => setActiveColorScheme((s) => (s === 'light' ? 'dark' : 'light')), + [], + ); + + if (!fontsLoaded) { + return null; + } + + return ( + + + + + + + + + + + ); +} + +function AppContent({ toggleColorScheme }: { toggleColorScheme: () => void }) { + const [activeChip, setActiveChip] = useState(chipTabs[0]); + const [activeNavTab, setActiveNavTab] = useState('home'); + const insets = useSafeAreaInsets(); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + setActiveNavTab('home')} + /> + setActiveNavTab('trade')} + /> + setActiveNavTab('explore')} + /> + setActiveNavTab('account')} + /> + + + + ); +} diff --git a/templates/expo-app/README.md b/templates/expo-app/README.md new file mode 100644 index 000000000..885febf8e --- /dev/null +++ b/templates/expo-app/README.md @@ -0,0 +1,49 @@ +# Coinbase Design System - Expo Template + +A React Native mobile application template integrated with the Coinbase Design System (CDS). + +## Installation + +Use `gitpick` to create a new project from this template: + +```sh +npx -y gitpick coinbase/cds/tree/master/templates/expo-app cds-expo +cd cds-expo +``` + +## Setup + +We suggest [nvm](https://github.com/nvm-sh/nvm/tree/master) to manage Node.js versions. If you have it installed, you can use these commands to set the correct Node.js version. Using corepack ensures you have your package manager setup. + +```sh +nvm install +nvm use +corepack enable +yarn +``` + +## Development + +- `yarn start` - Start the Expo development server +- `yarn ios` - Run on iOS simulator +- `yarn android` - Run on Android emulator +- `yarn web` - Run in web browser + +## Dev Builds + +Some CDS components require native modules that are not available in Expo Go. If you use any of the following components, you will need to create a [development build](https://docs.expo.dev/develop/development-builds/introduction/): + +- `DatePicker` +- `openWebBrowser` +- `AndroidNavigationBar` + +To create a development build: + +```sh +npx expo prebuild +npx expo run:ios # or npx expo run:android +``` + +## Documentation + +Visit [cds.coinbase.com](https://cds.coinbase.com) for the latest CDS documentation and component examples. diff --git a/templates/expo-app/app.json b/templates/expo-app/app.json new file mode 100644 index 000000000..d5a0b64d7 --- /dev/null +++ b/templates/expo-app/app.json @@ -0,0 +1,15 @@ +{ + "expo": { + "name": "cds-expo-app", + "slug": "cds-expo-app", + "version": "1.0.0", + "orientation": "portrait", + "userInterfaceStyle": "light", + "ios": { + "supportsTablet": true + }, + "android": { + "backgroundColor": "#ffffff" + } + } +} diff --git a/templates/expo-app/babel.config.js b/templates/expo-app/babel.config.js new file mode 100644 index 000000000..9d89e1311 --- /dev/null +++ b/templates/expo-app/babel.config.js @@ -0,0 +1,6 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: ['babel-preset-expo'], + }; +}; diff --git a/templates/expo-app/components/AssetCarousel.tsx b/templates/expo-app/components/AssetCarousel.tsx new file mode 100644 index 000000000..7bf31422f --- /dev/null +++ b/templates/expo-app/components/AssetCarousel.tsx @@ -0,0 +1,54 @@ +import React from 'react'; + +import { useTheme } from '@coinbase/cds-mobile'; +import { Carousel, CarouselItem } from '@coinbase/cds-mobile/carousel'; +import { MediaCard } from '@coinbase/cds-mobile/cards'; +import { RemoteImage } from '@coinbase/cds-mobile/media'; +import { Text } from '@coinbase/cds-mobile/typography'; +import { assets } from '@coinbase/cds-common/internal/data/assets'; + +const assetList = Object.values(assets); + +export function AssetCarousel() { + const theme = useTheme(); + const horizontalPadding = theme.space[2]; + const horizontalGap = theme.space[2]; + + return ( + + {assetList.map((asset) => ( + + + } + title={asset.symbol} + subtitle={asset.name} + description={ + + Explore + + } + onPress={() => {}} + /> + + ))} + + ); +} diff --git a/templates/expo-app/components/AssetChart.tsx b/templates/expo-app/components/AssetChart.tsx new file mode 100644 index 000000000..3a465f47f --- /dev/null +++ b/templates/expo-app/components/AssetChart.tsx @@ -0,0 +1,116 @@ +import React, { memo, useState, useCallback, useMemo, forwardRef } from 'react'; + +import type { TabValue } from '@coinbase/cds-common/tabs/useTabs'; +import { useTabsContext } from '@coinbase/cds-common/tabs/TabsContext'; +import { useTheme } from '@coinbase/cds-mobile'; +import { VStack } from '@coinbase/cds-mobile/layout'; +import { RemoteImage } from '@coinbase/cds-mobile/media'; +import { Text } from '@coinbase/cds-mobile/typography'; +import { SectionHeader } from '@coinbase/cds-mobile/section-header'; +import { SegmentedTab } from '@coinbase/cds-mobile/tabs/SegmentedTab'; +import { + ChartBridgeProvider, + LineChart, + PeriodSelector, + PeriodSelectorActiveIndicator, +} from '@coinbase/cds-mobile-visualization'; +import { assets } from '@coinbase/cds-common/internal/data/assets'; +import { sparklineInteractiveData } from '@coinbase/cds-common/internal/visualizations/SparklineInteractiveData'; + +const btcColor = assets.btc.color; + +const tabs = [ + { id: 'hour', label: '1H' }, + { id: 'day', label: '1D' }, + { id: 'week', label: '1W' }, + { id: 'month', label: '1M' }, + { id: 'year', label: '1Y' }, + { id: 'all', label: 'All' }, +]; + +const BTCActiveIndicator = memo((props: any) => { + return ; +}); + +const BTCTab = memo( + forwardRef(({ label, ...props }: any, ref: any) => { + const { activeTab } = useTabsContext(); + const isActive = activeTab?.id === props.id; + const theme = useTheme(); + + const wrappedLabel = + typeof label === 'string' ? ( + + {label} + + ) : ( + label + ); + + return ; + }), +); + +const priceFormatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', +}); + +function formatPrice(price: number) { + return priceFormatter.format(price); +} + +export const AssetChart = memo(function AssetChart() { + const [timePeriod, setTimePeriod] = useState(tabs[0]); + + const sparklineTimePeriodData = useMemo(() => { + return sparklineInteractiveData[timePeriod.id as keyof typeof sparklineInteractiveData]; + }, [timePeriod]); + + const sparklineValues = useMemo(() => { + return sparklineTimePeriodData.map((d) => d.value); + }, [sparklineTimePeriodData]); + + const currentPrice = + sparklineInteractiveData.hour[sparklineInteractiveData.hour.length - 1].value; + + const onPeriodChange = useCallback((period: TabValue | null) => { + setTimePeriod(period || tabs[0]); + }, []); + + return ( + + + {formatPrice(currentPrice)}} + end={ + + + + } + title={Bitcoin} + /> + + + + + ); +}); diff --git a/templates/expo-app/components/AssetList.tsx b/templates/expo-app/components/AssetList.tsx new file mode 100644 index 000000000..f9f5a6616 --- /dev/null +++ b/templates/expo-app/components/AssetList.tsx @@ -0,0 +1,67 @@ +import React from 'react'; + +import { VStack } from '@coinbase/cds-mobile/layout'; +import { Text } from '@coinbase/cds-mobile/typography'; +import { ListCell } from '@coinbase/cds-mobile/cells'; +import { Avatar } from '@coinbase/cds-mobile/media'; +import { assets as cdsAssets } from '@coinbase/cds-common/internal/data/assets'; + +const assetData = [ + { + key: 'btc', + name: 'Bitcoin', + symbol: 'BTC', + price: '$67,432.18', + change: '+2.41%', + }, + { + key: 'eth', + name: 'Ethereum', + symbol: 'ETH', + price: '$3,521.90', + change: '+1.83%', + }, + { + key: 'ada', + name: 'Cardano', + symbol: 'ADA', + price: '$0.6231', + change: '-0.82%', + }, +] as const; + +type AssetKey = keyof typeof cdsAssets; + +export function AssetList() { + return ( + + + Your assets + + {assetData.map((asset) => { + const cdsAsset = cdsAssets[asset.key as AssetKey]; + return ( + + {asset.change} + + } + media={ + + } + accessory="arrow" + onPress={() => {}} + /> + ); + })} + + ); +} diff --git a/templates/expo-app/components/CardList.tsx b/templates/expo-app/components/CardList.tsx new file mode 100644 index 000000000..01652763f --- /dev/null +++ b/templates/expo-app/components/CardList.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { Dimensions } from 'react-native'; + +import { useTheme } from '@coinbase/cds-mobile'; +import { Carousel, CarouselItem } from '@coinbase/cds-mobile/carousel'; +import { MessagingCard } from '@coinbase/cds-mobile/cards'; +import { Pictogram } from '@coinbase/cds-mobile/illustrations'; +import { Button } from '@coinbase/cds-mobile/buttons'; + +export function CardList() { + const theme = useTheme(); + const windowWidth = Dimensions.get('window').width; + const horizontalPadding = theme.space[2]; + const horizontalGap = theme.space[2]; + const carouselWidth = windowWidth - horizontalPadding * 2; + const itemWidth = (carouselWidth - horizontalGap) / 1.1; + + return ( + + + + } + mediaPlacement="end" + action="Get started" + onActionButtonPress={() => {}} + onDismissButtonPress={() => {}} + /> + + + + } + mediaPlacement="end" + action={ + + } + onDismissButtonPress={() => {}} + /> + + + + + } + mediaPlacement="end" + action="Start learning" + onActionButtonPress={() => {}} + onDismissButtonPress={() => {}} + /> + + + ); +} diff --git a/templates/expo-app/components/Navbar.tsx b/templates/expo-app/components/Navbar.tsx new file mode 100644 index 000000000..c66754bf6 --- /dev/null +++ b/templates/expo-app/components/Navbar.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { useTheme } from '@coinbase/cds-mobile'; +import { HStack, Box } from '@coinbase/cds-mobile/layout'; +import { Avatar } from '@coinbase/cds-mobile/media'; +import { TopNavBar, NavBarIconButton, NavigationTitle } from '@coinbase/cds-mobile/navigation'; + +export function Navbar({ toggleColorScheme }: { toggleColorScheme: () => void }) { + const theme = useTheme(); + const insets = useSafeAreaInsets(); + const isDark = theme.activeColorScheme === 'dark'; + + return ( + + } + end={ + + + + + } + > + Home + + + ); +} diff --git a/templates/expo-app/components/TabBarButton.tsx b/templates/expo-app/components/TabBarButton.tsx new file mode 100644 index 000000000..71aeaec06 --- /dev/null +++ b/templates/expo-app/components/TabBarButton.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Pressable, type StyleProp, type ViewStyle } from 'react-native'; + +import { useTheme } from '@coinbase/cds-mobile'; +import { VStack } from '@coinbase/cds-mobile/layout'; +import { Icon } from '@coinbase/cds-mobile/icons'; +import { Text } from '@coinbase/cds-mobile/typography'; +import type { IconName } from '@coinbase/cds-common/types'; + +export type TabBarButtonProps = { + icon: IconName; + label: string; + active?: boolean; + onPress?: () => void; + style?: StyleProp; +}; + +export function TabBarButton({ icon, label, active = false, onPress, style }: TabBarButtonProps) { + const theme = useTheme(); + + return ( + + + + + {label} + + + + ); +} diff --git a/templates/expo-app/metro.config.js b/templates/expo-app/metro.config.js new file mode 100644 index 000000000..a49e94d80 --- /dev/null +++ b/templates/expo-app/metro.config.js @@ -0,0 +1,23 @@ +const path = require('path'); +const { getDefaultConfig } = require('expo/metro-config'); + +const config = getDefaultConfig(__dirname); + +config.resolver.unstable_enablePackageExports = true; + +// Force @react-spring/native to use its CJS entry point. +// The ESM (.modern.mjs) entry uses a __require("react-native") polyfill +// that Metro cannot resolve when package exports are enabled. +const originalResolveRequest = config.resolver.resolveRequest; +config.resolver.resolveRequest = (context, moduleName, platform) => { + if (moduleName === '@react-spring/native') { + return { + type: 'sourceFile', + filePath: path.resolve(__dirname, 'node_modules/@react-spring/native/dist/cjs/index.js'), + }; + } + const resolve = originalResolveRequest ?? context.resolveRequest; + return resolve(context, moduleName, platform); +}; + +module.exports = config; diff --git a/templates/expo-app/package.json b/templates/expo-app/package.json new file mode 100644 index 000000000..3a4d62414 --- /dev/null +++ b/templates/expo-app/package.json @@ -0,0 +1,47 @@ +{ + "name": "cds-expo-app", + "main": "expo/AppEntry.js", + "scripts": { + "start": "expo start", + "android": "expo start --android", + "ios": "expo start --ios", + "web": "expo start --web" + }, + "dependencies": { + "@expo-google-fonts/inter": "^0.2.3", + "@coinbase/cds-common": "^8", + "@coinbase/cds-icons": "^5", + "@coinbase/cds-illustrations": "^4", + "@coinbase/cds-mobile": "^8", + "@coinbase/cds-mobile-visualization": "beta", + "@dotlottie/react-player": "1.6.1", + "@lottiefiles/dotlottie-react": "0.6.5", + "@lottiefiles/react-lottie-player": "3.5.3", + "@react-navigation/native": "6.1.17", + "@react-navigation/native-stack": "6.9.26", + "@react-navigation/stack": "6.4.1", + "@shopify/react-native-skia": "1.2.3", + "expo": "~51.0.28", + "expo-status-bar": "~1.12.1", + "lottie-react-native": "6.7.0", + "metro": "0.80.12", + "react": "18.2.0", + "react-native": "0.74.5", + "react-native-date-picker": "5.0.13", + "react-native-gesture-handler": "~2.16.1", + "react-native-inappbrowser-reborn": "3.7.0", + "react-native-linear-gradient": "2.8.3", + "react-native-navigation-bar-color": "2.0.2", + "react-native-reanimated": "~3.10.1", + "react-native-safe-area-context": "4.10.5", + "react-native-screens": "3.31.1", + "react-native-svg": "15.2.0" + }, + "devDependencies": { + "@babel/core": "^7.20.0", + "@types/react": "~18.2.0", + "typescript": "~5.3.0" + }, + "private": true, + "packageManager": "yarn@4.9.2" +} diff --git a/templates/expo-app/tsconfig.json b/templates/expo-app/tsconfig.json new file mode 100644 index 000000000..ed8ca65f9 --- /dev/null +++ b/templates/expo-app/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "expo/tsconfig.base.json", + "compilerOptions": { + "strict": true, + "jsx": "react-jsx", + "moduleResolution": "bundler" + } +}