diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2f85a362c..70cd7855a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -57,10 +57,9 @@ jobs: with: node-version: ${{ matrix.node-version }} - run: pnpm install - - run: pnpm build - run: pnpm test - cypress: + storybook-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -68,31 +67,35 @@ jobs: - uses: actions/setup-node@v5 with: node-version: lts/* - - uses: cypress-io/github-action@v6 + - run: pnpm install + - name: Store Playwright's version + id: playwright-version + run: | + PLAYWRIGHT_VERSION=$(pnpm --filter sanity-ui-storybook exec playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) + echo "Playwright's version: $PLAYWRIGHT_VERSION" + echo "version=${PLAYWRIGHT_VERSION}" >> "$GITHUB_OUTPUT" + - name: Cache Playwright browsers for Playwright's version + id: cache-playwright-browsers + uses: actions/cache@v4 with: - build: pnpm workshop:build - start: pnpm workshop:start - wait-on: http://localhost:1337 - env: - NODE_OPTIONS: "--no-experimental-require-module --no-experimental-detect-module" + path: ~/.cache/ms-playwright + key: ${{ steps.playwright-version.outputs.version }}-playwright-browsers-chromium + - name: Install Playwright browsers + if: steps.cache-playwright-browsers.outputs.cache-hit != 'true' + run: pnpm --filter sanity-ui-storybook exec playwright install --with-deps chromium + - run: pnpm test:browser - uses: actions/upload-artifact@v4 if: failure() with: - name: cypress-screenshots - path: cypress/screenshots - if-no-files-found: ignore - - uses: actions/upload-artifact@v4 - if: always() - with: - name: cypress-videos - path: cypress/videos + name: vitest-failure-screenshots + path: apps/storybook/**/__screenshots__/ if-no-files-found: ignore release: permissions: id-token: write # to enable use of OIDC for npm provenance name: "Semantic release" - needs: [lint, test, cypress] + needs: [lint, test, storybook-test] runs-on: ubuntu-latest steps: - uses: actions/create-github-app-token@v2 @@ -114,8 +117,8 @@ jobs: with: node-version: lts/* - run: pnpm install - # Branches that will release new versions are defined in .releaserc.json - - run: pnpm exec semantic-release + # Branches that will release new versions are defined in packages/ui/package.json + - run: pnpm --filter @sanity/ui exec semantic-release env: NPM_CONFIG_PROVENANCE: true GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml index 46826918a..0878ca74f 100644 --- a/.github/workflows/pkg-pr-new.yml +++ b/.github/workflows/pkg-pr-new.yml @@ -33,7 +33,7 @@ jobs: - id: pre-flight run: | - VERSION="$(jq -r .version package.json)-pkg.pr.new@$(git rev-parse --short HEAD)" + VERSION="$(jq -r .version packages/ui/package.json)-pkg.pr.new@$(git rev-parse --short HEAD)" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - run: pnpm build @@ -42,4 +42,4 @@ jobs: - run: | echo "Publishing to pkg.pr.new" - pnpm pnpx pkg-pr-new publish + pnpm pnpx pkg-pr-new publish ./packages/ui diff --git a/.gitignore b/.gitignore index ca9e94eae..15a265de0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,12 @@ .DS_Store .env.local -.workshop dist etc node_modules storybook-static .eslintcache + +# Vitest browser mode failure screenshots and attachments +__screenshots__ +.vitest-attachments diff --git a/.npmrc b/.npmrc index 8826b8a20..f60f201d6 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1,2 @@ strict-peer-dependencies = false link-workspace-packages = deep -# https://github.com/cypress-io/github-action/blob/43c624e40c65c7c3a255a901d0a0a0a2040b7140/examples/basic-pnpm/.npmrc#L2C1-L2C25 -side-effects-cache=false diff --git a/.prettierignore b/.prettierignore index f036e9100..0d6ca4f2f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,3 @@ -.workshop dist pnpm-lock.yaml +CHANGELOG.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..236b68418 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# AGENTS.md + +## Cursor Cloud specific instructions + +This is the `v2` maintenance branch of the `@sanity/ui` React component +library, structured as a pnpm monorepo: the published package lives in +`packages/ui`, the Figma plugin in `packages/figma`, and the Storybook app in +`apps/storybook` (`pnpm-workspace.yaml`). The root `package.json` is a private +workspace root whose scripts orchestrate via pnpm filters. Package manager is +pnpm (`packageManager` pin in `package.json`). + +Standard scripts live in the root `package.json` (`lint`, `ts:check`, `test`, +`build`, `dev`) and forward to the workspace packages. Notes that are not +obvious from the scripts: + +- `pnpm test` runs the unit tests with vitest (`packages/ui/vitest.config.ts`). + `@sanity/ui` is aliased to the `packages/ui/exports/` source, so unit tests + run directly against source and do not require a `pnpm build` first. +- `pnpm dev` starts Storybook (`apps/storybook`) on http://localhost:6006. It + resolves `@sanity/ui` to the `packages/ui` source, so it hot-reloads source + edits directly (no rebuild needed). +- `pnpm test:browser` runs the Storybook tests (`apps/storybook`): vitest + renders every story in headless Chromium via `@storybook/addon-vitest` and + executes story `play` interactions, plus the browser tests in + `apps/storybook/tests/` (see `apps/storybook/vitest.config.ts`). The + Playwright-provided browser must be installed once via + `pnpm --filter sanity-ui-storybook exec playwright install chromium`. + Stories opt out of being tested with the `!test` tag. +- Releases are automated with semantic-release: commit messages must follow + Conventional Commits (enforced via commitlint), and pushes to the `v2` + branch publish `@sanity/ui` to npm from `packages/ui`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be582d777..97d1fb396 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,50 @@ # Contributing guidelines +This repository is a pnpm monorepo. The published `@sanity/ui` package lives in +[`packages/ui`](packages/ui), the Figma plugin lives in +[`packages/figma`](packages/figma), and the Storybook app lives in +[`apps/storybook`](apps/storybook). + +## Getting started + +```sh +pnpm install +pnpm build +pnpm test +``` + +Run `pnpm dev` to start Storybook (http://localhost:6006). Storybook resolves +`@sanity/ui` from the package source, so edits to `packages/ui/src` hot-reload +without a rebuild. + +## Testing + +Unit tests are written with [vitest](https://vitest.dev) and live next to the +source in `packages/ui/src`. Run them with `pnpm test` (or +`pnpm test:watch` in `packages/ui` for watch mode). They run against the +package source, so no build is required. + +Browser tests live in the Storybook app (`apps/storybook`) and use +[Storybook's Vitest addon](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon): +every story is rendered as a smoke test in headless Chromium, and interaction +tests are written as story [`play` functions](https://storybook.js.org/docs/writing-stories/play-function). +Tests that need direct control over the browser (e.g. resizing the viewport) +live in `apps/storybook/tests/`. + +Install the Playwright-provided browser once with +`pnpm --filter sanity-ui-storybook exec playwright install chromium`, then run +`pnpm test:browser`. While developing, `pnpm dev` exposes the same tests +interactively through the testing panel in the Storybook UI. + +## Releasing + +Releases are automated with +[semantic-release](https://github.com/semantic-release/semantic-release): +commits that land on the `v2` branch are analyzed (commit messages must follow +[Conventional Commits](https://www.conventionalcommits.org)), and new versions +of `@sanity/ui` are published to npm from `packages/ui` by the +[CI & Release workflow](https://github.com/sanity-io/ui/actions/workflows/main.yml). + ## Working with prereleases ### Going from `alpha => beta` diff --git a/README.md b/README.md deleted file mode 100644 index c91e5f5e0..000000000 --- a/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# @sanity/ui - -The Sanity UI components. - -```sh -npm install @sanity/ui - -# Install peer dependencies -npm install react react-dom styled-components -``` - -[![npm version](https://img.shields.io/npm/v/%40sanity%2Fui/release-v2?style=flat-square)](https://www.npmjs.com/package/@sanity/ui/v/release-v2) - -## Usage - -```jsx -import {Button, ThemeProvider} from '@sanity/ui' -import {buildTheme} from '@sanity/ui/theme' -import {createRoot} from 'react-dom/client' - -const root = createRoot(document.getElementById('root')) -const theme = buildTheme() - -root.render( - - + + + + + ), +} + +export const WithinMenuItem: Story = { + parameters: {controls: {include: []}}, + render: () => ( + + + + + + {[1, 2, 3].map((index) => ( + + + + Menu item {index} + + + + + + + + + + + ))} + + + + + + ), +} diff --git a/stories/primitives/AvatarCounter.stories.tsx b/apps/storybook/stories/primitives/AvatarCounter.stories.tsx similarity index 80% rename from stories/primitives/AvatarCounter.stories.tsx rename to apps/storybook/stories/primitives/AvatarCounter.stories.tsx index 950463597..feba5ccf5 100644 --- a/stories/primitives/AvatarCounter.stories.tsx +++ b/apps/storybook/stories/primitives/AvatarCounter.stories.tsx @@ -1,6 +1,6 @@ -import type {Meta, StoryFn, StoryObj} from '@storybook/react' +import {AvatarCounter, AvatarStack} from '@sanity/ui' +import type {Meta, StoryFn, StoryObj} from '@storybook/react-vite' -import {AvatarCounter, AvatarStack} from '../../src/core/primitives' import {getAvatarSizeControls} from '../controls' const meta: Meta = { diff --git a/stories/primitives/AvatarStack.stories.tsx b/apps/storybook/stories/primitives/AvatarStack.stories.tsx similarity index 84% rename from stories/primitives/AvatarStack.stories.tsx rename to apps/storybook/stories/primitives/AvatarStack.stories.tsx index b6cd42eef..cb792b877 100644 --- a/stories/primitives/AvatarStack.stories.tsx +++ b/apps/storybook/stories/primitives/AvatarStack.stories.tsx @@ -1,6 +1,6 @@ -import type {Meta, StoryObj} from '@storybook/react' +import {Avatar, AvatarCounter, AvatarStack} from '@sanity/ui' +import type {Meta, StoryObj} from '@storybook/react-vite' -import {Avatar, AvatarCounter, AvatarStack} from '../../src/core/primitives' import {AVATAR_SRC} from '../constants' import {getAvatarSizeControls} from '../controls' diff --git a/stories/primitives/Badge.stories.tsx b/apps/storybook/stories/primitives/Badge.stories.tsx similarity index 93% rename from stories/primitives/Badge.stories.tsx rename to apps/storybook/stories/primitives/Badge.stories.tsx index 9020bbaea..efb42029a 100644 --- a/stories/primitives/Badge.stories.tsx +++ b/apps/storybook/stories/primitives/Badge.stories.tsx @@ -1,6 +1,6 @@ -import type {Meta, StoryObj} from '@storybook/react' +import {Badge, Card, Flex, Stack} from '@sanity/ui' +import type {Meta, StoryObj} from '@storybook/react-vite' -import {Badge, Card, Flex, Stack} from '../../src/core/primitives' import {BADGE_TONES, CARD_TONES, RADII} from '../constants' import {getFontSizeControls, getRadiusControls, getSpaceControls} from '../controls' import {rowBuilder} from '../helpers/rowBuilder' diff --git a/apps/storybook/stories/primitives/Box.stories.tsx b/apps/storybook/stories/primitives/Box.stories.tsx new file mode 100644 index 000000000..f62c30a6f --- /dev/null +++ b/apps/storybook/stories/primitives/Box.stories.tsx @@ -0,0 +1,109 @@ +import {Box, Flex, Grid, Text} from '@sanity/ui' +import type {Meta, StoryObj} from '@storybook/react-vite' +import {forwardRef} from 'react' + +import {getSpaceControls} from '../controls' + +const meta: Meta = { + args: { + children: Box with a custom outline, + padding: 4, + style: {border: '1px solid red'}, + }, + argTypes: { + padding: getSpaceControls(), + paddingBottom: getSpaceControls(), + paddingLeft: getSpaceControls(), + paddingRight: getSpaceControls(), + paddingTop: getSpaceControls(), + }, + component: Box, + tags: ['autodocs'], +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: (props) => { + return + }, +} + +export const AsGridItem: Story = { + args: { + column: 2, + children: Cell B, + }, + argTypes: { + column: {control: {type: 'number', min: 1, max: 12}}, + columnStart: {control: {type: 'number', min: 1, max: 12}}, + columnEnd: {control: {type: 'number', min: 1, max: 12}}, + row: {control: {type: 'number', min: 1, max: 12}}, + rowStart: {control: {type: 'number', min: 1, max: 12}}, + rowEnd: {control: {type: 'number', min: 1, max: 12}}, + }, + render: (props) => { + return ( + + + Cell A + + + + Cell C + + + ) + }, +} + +export const Responsive: Story = { + parameters: {controls: {include: []}}, + render: () => ( + + + + This is a box with responsive props + + + + ), +} + +// A custom component with its own props. When passed to `Box`'s `as` prop, these props are +// inferred on `Box` itself: `href` is required and `target` is optional, and unknown props +// are rejected by the type checker. +const Link = forwardRef(function Link( + props: {href: string; target?: string} & Omit< + React.HTMLProps, + 'as' | 'href' | 'target' + >, + ref: React.ForwardedRef, +): React.JSX.Element { + const {children, ...restProps} = props + + return ( + + {children} + + ) +}) + +export const AsComponent: Story = { + parameters: {controls: {include: []}}, + render: () => ( + + + As component + + + ), +} diff --git a/stories/primitives/Button.stories.tsx b/apps/storybook/stories/primitives/Button.stories.tsx similarity index 53% rename from stories/primitives/Button.stories.tsx rename to apps/storybook/stories/primitives/Button.stories.tsx index dc69ff23a..4a77d4f55 100644 --- a/stories/primitives/Button.stories.tsx +++ b/apps/storybook/stories/primitives/Button.stories.tsx @@ -1,7 +1,20 @@ -import {CloseIcon, SearchIcon} from '@sanity/icons' -import type {Meta, StoryObj} from '@storybook/react' +import {AddIcon, CloseIcon, RocketIcon, SearchIcon, SquareIcon, UploadIcon} from '@sanity/icons' +import { + Box, + Button, + ButtonProps, + Card, + Container, + Flex, + Grid, + Inline, + Stack, + Text, +} from '@sanity/ui' +import type {Meta, StoryObj} from '@storybook/react-vite' +import {useCallback, useRef} from 'react' +import {styled} from 'styled-components' -import {Button, Flex, Grid, Stack, Text} from '../../src/core/primitives' import {BUTTON_MODES, BUTTON_TONES, RADII} from '../constants' import { getButtonWidthControls, @@ -51,7 +64,7 @@ export const WithIcons: Story = { }, parameters: { controls: { - include: ['icon', 'iconRight', 'mode', 'space', 'tone'], + include: ['icon', 'iconRight', 'mode', 'gap', 'space', 'tone'], }, }, render: (props) => + + ), +} + +function UploadButtonStory() { + const inputRef = useRef(null) + + const handleKeyDown = useCallback((event: React.KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + inputRef.current?.click() + } + }, []) + + return ( + + - - - - - ) -} diff --git a/src/core/primitives/avatar/__workshop__/withinMenuItem.tsx b/src/core/primitives/avatar/__workshop__/withinMenuItem.tsx deleted file mode 100644 index e766bf0c1..000000000 --- a/src/core/primitives/avatar/__workshop__/withinMenuItem.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import {useBoolean} from '@sanity/ui-workshop' - -import {Menu, MenuItem} from '../../../components' -import {Layer} from '../../../utils' -import {Box} from '../../box' -import {Card} from '../../card' -import {Container} from '../../container' -import {Flex} from '../../flex' -import {Text} from '../../text' -import {Avatar} from '../avatar' -import {AvatarStack} from '../avatarStack' - -export default function WithinMenuItemStory() { - const disabled = useBoolean('Disabled', false, 'Props') - - return ( - - - - - - - - - Menu item 1 - - - - - - - - - - - - - - Menu item 1 - - - - - - - - - - - - - - Menu item 1 - - - - - - - - - - - - - - - - ) -} diff --git a/src/core/primitives/badge/__workshop__/index.ts b/src/core/primitives/badge/__workshop__/index.ts deleted file mode 100644 index cb4101b9b..000000000 --- a/src/core/primitives/badge/__workshop__/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {defineScope} from '@sanity/ui-workshop' -import {lazy} from 'react' - -export default defineScope({ - name: 'primitives/badge', - title: 'Badge', - stories: [ - { - name: 'props', - title: 'Props', - component: lazy(() => import('./props')), - }, - { - name: 'tones', - title: 'Tones', - component: lazy(() => import('./tones')), - }, - ], -}) diff --git a/src/core/primitives/badge/__workshop__/props.tsx b/src/core/primitives/badge/__workshop__/props.tsx deleted file mode 100644 index 8452073cd..000000000 --- a/src/core/primitives/badge/__workshop__/props.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import {Badge, Flex} from '@sanity/ui' -import {useAction, useSelect, useText} from '@sanity/ui-workshop' - -import { - WORKSHOP_BADGE_MODE_OPTIONS, - WORKSHOP_BADGE_TONE_OPTIONS, - WORKSHOP_SPACE_OPTIONS, -} from '../../../__workshop__/constants' - -export default function PropsStory() { - const mode = useSelect('Mode (deprecated)', WORKSHOP_BADGE_MODE_OPTIONS, 'default', 'Props') - const paddingX = useSelect('Padding X', WORKSHOP_SPACE_OPTIONS, 1, 'Props') - const paddingY = useSelect('Padding Y', WORKSHOP_SPACE_OPTIONS, 1, 'Props') - const tone = useSelect('Tone', WORKSHOP_BADGE_TONE_OPTIONS, 'default', 'Props') - const textProp = useText('Text', 'Label', 'Props') - - return ( - - - {textProp} - - - ) -} diff --git a/src/core/primitives/badge/__workshop__/tones.tsx b/src/core/primitives/badge/__workshop__/tones.tsx deleted file mode 100644 index a55a4efa4..000000000 --- a/src/core/primitives/badge/__workshop__/tones.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import {Badge, Flex, Inline} from '@sanity/ui' -import {THEME_COLOR_STATE_TONES} from '@sanity/ui/theme' -import {useSelect} from '@sanity/ui-workshop' - -import {WORKSHOP_BADGE_MODE_OPTIONS} from '../../../__workshop__/constants' - -export default function Tones() { - const mode = useSelect('Mode (deprecated)', WORKSHOP_BADGE_MODE_OPTIONS, 'default', 'Props') - - return ( - - - {THEME_COLOR_STATE_TONES.map((tone) => ( - - {tone} - - ))} - - - ) -} diff --git a/src/core/primitives/box/__workshop__/index.ts b/src/core/primitives/box/__workshop__/index.ts deleted file mode 100644 index 88de67629..000000000 --- a/src/core/primitives/box/__workshop__/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {defineScope} from '@sanity/ui-workshop' -import {lazy} from 'react' - -export default defineScope({ - name: 'primitives/box', - title: 'Box', - stories: [ - { - name: 'props', - title: 'Props', - component: lazy(() => import('./props')), - }, - { - name: 'responsive', - title: 'Responsive', - component: lazy(() => import('./responsive')), - }, - ], -}) diff --git a/src/core/primitives/box/__workshop__/props.tsx b/src/core/primitives/box/__workshop__/props.tsx deleted file mode 100644 index 968bbc625..000000000 --- a/src/core/primitives/box/__workshop__/props.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import {Box, Card, Text} from '@sanity/ui' -import {useAction, useSelect} from '@sanity/ui-workshop' - -import {WORKSHOP_SPACE_OPTIONS} from '../../../__workshop__/constants' - -export default function PropsStory() { - const padding = useSelect('Padding', WORKSHOP_SPACE_OPTIONS, 0, 'Props') - - return ( - - - - - Box with padding={padding} - - - - - ) -} diff --git a/src/core/primitives/box/__workshop__/responsive.tsx b/src/core/primitives/box/__workshop__/responsive.tsx deleted file mode 100644 index 962475901..000000000 --- a/src/core/primitives/box/__workshop__/responsive.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import {Box, Text} from '@sanity/ui' - -export default function ResponsiveStory() { - return ( - - - - This is a box with responsive props - - - - ) -} diff --git a/src/core/primitives/button/__workshop__/custom.tsx b/src/core/primitives/button/__workshop__/custom.tsx deleted file mode 100644 index 5b0c10e06..000000000 --- a/src/core/primitives/button/__workshop__/custom.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* eslint-disable jsx-a11y/anchor-is-valid */ - -import {Button, Flex, Grid, Stack, Text} from '@sanity/ui' - -import {WORKSHOP_BUTTON_TONE_OPTIONS} from '../../../__workshop__/constants' - -export default function CustomStory() { - const tones = Object.entries(WORKSHOP_BUTTON_TONE_OPTIONS) - const len = tones.length - - return ( - - - - {tones.map(([title, tone]) => ( - - ))} - - - {tones.map(([title, tone]) => ( - - ))} - - - - ) -} diff --git a/src/core/primitives/button/__workshop__/customIcons.tsx b/src/core/primitives/button/__workshop__/customIcons.tsx deleted file mode 100644 index 29105c8d4..000000000 --- a/src/core/primitives/button/__workshop__/customIcons.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import {RocketIcon} from '@sanity/icons' -import {Button, Flex, Inline} from '@sanity/ui' - -export default function CustomIconsStory() { - return ( - - - - - ) -} diff --git a/src/core/primitives/button/__workshop__/props.tsx b/src/core/primitives/button/__workshop__/props.tsx deleted file mode 100644 index 8ff5d0499..000000000 --- a/src/core/primitives/button/__workshop__/props.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import {icons} from '@sanity/icons' -import {Button, Flex} from '@sanity/ui' -import {useAction, useBoolean, useSelect, useText} from '@sanity/ui-workshop' - -import { - WORKSHOP_BUTTON_MODE_OPTIONS, - WORKSHOP_BUTTON_TEXT_ALIGN_OPTIONS, - WORKSHOP_BUTTON_TONE_OPTIONS, - WORKSHOP_FLEX_JUSTIFY_OPTIONS, - WORKSHOP_ICON_SYMBOL_OPTIONS, - WORKSHOP_SPACE_OPTIONS, - WORKSHOP_TEXT_SIZE_OPTIONS, - WORKSHOP_TEXT_WEIGHT_OPTIONS, -} from '../../../__workshop__/constants' - -export default function ButtonStory() { - const disabled = useBoolean('Disabled', false, 'Props') - const fontSize = useSelect('Font size', WORKSHOP_TEXT_SIZE_OPTIONS, 2, 'Props') - const icon = useSelect('Icon', WORKSHOP_ICON_SYMBOL_OPTIONS, 'add-circle', 'Props') - const iconRight = useSelect('Icon (right)', WORKSHOP_ICON_SYMBOL_OPTIONS, '', 'Props') - const justify = useSelect('Justify', WORKSHOP_FLEX_JUSTIFY_OPTIONS, 'center', 'Props') - const mode = useSelect('Mode', WORKSHOP_BUTTON_MODE_OPTIONS, 'default', 'Props') - const paddingX = useSelect('Padding X', WORKSHOP_SPACE_OPTIONS, 3, 'Props') - const paddingY = useSelect('Padding Y', WORKSHOP_SPACE_OPTIONS, 3, 'Props') - const selected = useBoolean('Selected', false, 'Props') - const space = useSelect('Space', WORKSHOP_SPACE_OPTIONS, 3, 'Props') - const tone = useSelect('Tone', WORKSHOP_BUTTON_TONE_OPTIONS, 'default', 'Props') - const textAlign = - useSelect('Text align', WORKSHOP_BUTTON_TEXT_ALIGN_OPTIONS, undefined, 'Props') || undefined - const textProp = useText('Text', 'Label', 'Props') - const textWeight = - useSelect('Text weight', WORKSHOP_TEXT_WEIGHT_OPTIONS, '', 'Props') || undefined - - return ( - -