From 877e0b8cd922e24f8ef6677b9e1c0609d0345b89 Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Tue, 16 Jul 2024 17:52:32 -0700 Subject: [PATCH] Start building out Storybook for the Penumbra UI library (#1452) * Git-ignore the built Storybook library * Deprecate old component stories * Set up theme and manager options * Change how deprecation is done * Tweak settings * Set the brand title * Create an Icon component and stories * Set default color to rust * More settings tweaks * Change default color to make it more visible * Rename and simplify * Only add deprecated CSS to deprecated components * Add a Normalize component * ci: firebase config for storybook ui Adds a deploy workflow for the new storybook ui static site, deploying to: * preview.ui.penumbra.zone, on merge to main * ui.penumbra.zone, on tag push Additionally, PRs will have an ephemeral URL for preview posted to them via a bot as a comment, so unmerged work can be viewed in an online context. * Swap root order * Change default icon color * Start building out theme * Remove Normalize component for now * Fix formatting * Ignore storybook-static from Prettier * Move ignore to root --------- Co-authored-by: Conor Schaefer --- .github/workflows/deploy-ui-preview-main.yml | 32 ++++++++++++ .github/workflows/deploy-ui-preview-pr.yml | 34 +++++++++++++ .github/workflows/deploy-ui-release.yml | 34 +++++++++++++ .gitignore | 5 +- packages/ui/.firebaserc | 19 +++++++ packages/ui/.storybook/main.js | 13 ++++- packages/ui/.storybook/manager.js | 10 ++++ packages/ui/.storybook/penumbraTheme.js | 15 ++++++ packages/ui/.storybook/preview.js | 16 ------ packages/ui/.storybook/preview.jsx | 43 ++++++++++++++++ packages/ui/.storybook/public/logo.svg | 3 ++ packages/ui/components/readme.mdx | 10 ++++ packages/ui/firebase.json | 14 ++++++ packages/ui/package.json | 4 ++ packages/ui/src/Icon/index.stories.ts | 25 ++++++++++ packages/ui/src/Icon/index.tsx | 52 ++++++++++++++++++++ packages/ui/src/styled-components.d.ts | 26 ++++++++++ packages/ui/src/utils/theme.ts | 24 +++++++++ pnpm-lock.yaml | 9 ++++ 19 files changed, 370 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/deploy-ui-preview-main.yml create mode 100644 .github/workflows/deploy-ui-preview-pr.yml create mode 100644 .github/workflows/deploy-ui-release.yml create mode 100644 packages/ui/.firebaserc create mode 100644 packages/ui/.storybook/manager.js create mode 100644 packages/ui/.storybook/penumbraTheme.js delete mode 100644 packages/ui/.storybook/preview.js create mode 100644 packages/ui/.storybook/preview.jsx create mode 100644 packages/ui/.storybook/public/logo.svg create mode 100644 packages/ui/components/readme.mdx create mode 100644 packages/ui/firebase.json create mode 100644 packages/ui/src/Icon/index.stories.ts create mode 100644 packages/ui/src/Icon/index.tsx create mode 100644 packages/ui/src/styled-components.d.ts create mode 100644 packages/ui/src/utils/theme.ts diff --git a/.github/workflows/deploy-ui-preview-main.yml b/.github/workflows/deploy-ui-preview-main.yml new file mode 100644 index 0000000000..c29d4c0221 --- /dev/null +++ b/.github/workflows/deploy-ui-preview-main.yml @@ -0,0 +1,32 @@ +# Deploys the static website for the UI storybook to "preview" environment, +# on every merge into main branch. +name: Deploy UI to preview +on: + workflow_dispatch: + push: + branches: + - main +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install + working-directory: packages/ui + + - name: Build static site + run: pnpm build-storybook + working-directory: packages/ui + + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_PENUMBRA_UI }} + channelId: live + target: preview + entryPoint: packages/ui + projectId: penumbra-ui diff --git a/.github/workflows/deploy-ui-preview-pr.yml b/.github/workflows/deploy-ui-preview-pr.yml new file mode 100644 index 0000000000..4d9dff183f --- /dev/null +++ b/.github/workflows/deploy-ui-preview-pr.yml @@ -0,0 +1,34 @@ +# Deploys the static website for the UI storybook to a temporary environment, +# with an ephemeral URL posted to the PR for sharing/review. +name: Deploy UI to temporary URL +on: + workflow_dispatch: + pull_request: +permissions: + checks: write + contents: read + pull-requests: write +jobs: + build_and_preview: + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install + working-directory: packages/ui + + - name: Build static site + run: pnpm build-storybook + working-directory: packages/ui + + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_PENUMBRA_UI }} + target: preview + entryPoint: packages/ui + projectId: penumbra-ui diff --git a/.github/workflows/deploy-ui-release.yml b/.github/workflows/deploy-ui-release.yml new file mode 100644 index 0000000000..e2beb38011 --- /dev/null +++ b/.github/workflows/deploy-ui-release.yml @@ -0,0 +1,34 @@ +# Deploys the static website for the UI storybook to final prod website, +# on every tag push into main branch. +name: Deploy UI to stable channel +on: + # Support ad-hoc runs + workflow_dispatch: + # Run automatically on tag push + push: + tags: + - '**[0-9]+.[0-9]+.[0-9]+*' +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install + working-directory: packages/ui + + - name: Build static site + run: pnpm build-storybook + working-directory: packages/ui + + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_PENUMBRA_UI }} + channelId: live + target: stable + entryPoint: packages/ui + projectId: penumbra-ui diff --git a/.gitignore b/.gitignore index 86257dc813..4faea8352a 100644 --- a/.gitignore +++ b/.gitignore @@ -61,4 +61,7 @@ packages/*/penumbra-zone-*.tgz packages/*/repo-*-*.tgz packages/*/package -tsconfig.tsbuildinfo \ No newline at end of file +tsconfig.tsbuildinfo + +# Storybook builds +storybook-static diff --git a/packages/ui/.firebaserc b/packages/ui/.firebaserc new file mode 100644 index 0000000000..2b58d9aa15 --- /dev/null +++ b/packages/ui/.firebaserc @@ -0,0 +1,19 @@ +{ + "projects": { + "default": "penumbra-ui" + }, + "targets": { + "penumbra-ui": { + "hosting": { + "preview": [ + "penumbra-ui-preview" + ], + "stable": [ + "penumbra-ui" + ] + } + } + }, + "etags": {}, + "dataconnectEmulatorConfig": {} +} \ No newline at end of file diff --git a/packages/ui/.storybook/main.js b/packages/ui/.storybook/main.js index f6b4950b43..c267f58f80 100644 --- a/packages/ui/.storybook/main.js +++ b/packages/ui/.storybook/main.js @@ -10,7 +10,18 @@ function getAbsolutePath(value) { /** @type { import('@storybook/react-vite').StorybookConfig } */ const config = { - stories: ['../stories/**/*.mdx', '../components/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + stories: [ + { + directory: '../src', + files: '**/@(*.stories.@(js|jsx|mjs|ts|tsx)|*.mdx)', + titlePrefix: 'UI library', + }, + { + directory: '../components', + files: '**/@(*.stories.@(js|jsx|mjs|ts|tsx)|*.mdx)', + titlePrefix: 'Deprecated', + }, + ], addons: [ getAbsolutePath('@storybook/addon-links'), getAbsolutePath('@storybook/addon-essentials'), diff --git a/packages/ui/.storybook/manager.js b/packages/ui/.storybook/manager.js new file mode 100644 index 0000000000..50803930bf --- /dev/null +++ b/packages/ui/.storybook/manager.js @@ -0,0 +1,10 @@ +import { addons } from '@storybook/manager-api'; +import penumbraTheme from './penumbraTheme'; + +addons.setConfig({ + showToolbar: true, + theme: penumbraTheme, + sidebar: { + collapsedRoots: ['Deprecated'], + }, +}); diff --git a/packages/ui/.storybook/penumbraTheme.js b/packages/ui/.storybook/penumbraTheme.js new file mode 100644 index 0000000000..e31fc3ef57 --- /dev/null +++ b/packages/ui/.storybook/penumbraTheme.js @@ -0,0 +1,15 @@ +import { create } from '@storybook/theming/create'; +import logo from './public/logo.svg'; + +const penumbraTheme = create({ + base: 'dark', + brandImage: logo, + brandTitle: 'Penumbra UI library', + colorPrimary: '#8d5728', + colorSecondary: '#629994', + fontBase: 'Poppins', + fontCode: '"Iosevka Term",monospace', + textMutedColor: '#e3e3e3', +}); + +export default penumbraTheme; diff --git a/packages/ui/.storybook/preview.js b/packages/ui/.storybook/preview.js deleted file mode 100644 index 79924a5324..0000000000 --- a/packages/ui/.storybook/preview.js +++ /dev/null @@ -1,16 +0,0 @@ -import '../styles/globals.css'; - -/** @type { import('@storybook/react').Preview } */ -const preview = { - parameters: { - actions: { argTypesRegex: '^on[A-Z].*' }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/i, - }, - }, - }, -}; - -export default preview; diff --git a/packages/ui/.storybook/preview.jsx b/packages/ui/.storybook/preview.jsx new file mode 100644 index 0000000000..1f6aaf5c7c --- /dev/null +++ b/packages/ui/.storybook/preview.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import globalsCssUrl from '../styles/globals.css?url'; +import penumbraTheme from './penumbraTheme'; +import { ThemeProvider } from 'styled-components'; +import { theme } from '../src/utils/theme'; + +/** @type { import('@storybook/react').Preview } */ +const preview = { + decorators: [ + (Story, { title }) => { + const isDeprecatedComponent = title.startsWith('Deprecated/'); + + if (isDeprecatedComponent) { + return ( + <> + + + + ); + } + + return ( + + + + ); + }, + ], + parameters: { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + docs: { + theme: penumbraTheme, + }, + }, +}; + +export default preview; diff --git a/packages/ui/.storybook/public/logo.svg b/packages/ui/.storybook/public/logo.svg new file mode 100644 index 0000000000..0293e288a9 --- /dev/null +++ b/packages/ui/.storybook/public/logo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/ui/components/readme.mdx b/packages/ui/components/readme.mdx new file mode 100644 index 0000000000..9f92a02883 --- /dev/null +++ b/packages/ui/components/readme.mdx @@ -0,0 +1,10 @@ +import { Meta } from '@storybook/blocks'; +import * as ToasterStories from './ui/toaster/toaster.stories'; + + + +# Deprecated Penumbra UI components + +The `components/ui` directory contains deprecated Penumbra UI components. These will eventually all be replaced by components in the `src` directory; but until then, they're still here for reference and use when needed. + +Note that there are not Storybook stories for all deprecated components. To find deprecated components not listed here, please see the `components/ui` directory of the `@penumbra-zone/ui` package. diff --git a/packages/ui/firebase.json b/packages/ui/firebase.json new file mode 100644 index 0000000000..e1ec9099b2 --- /dev/null +++ b/packages/ui/firebase.json @@ -0,0 +1,14 @@ +{ + "hosting": [ + { + "target": "preview", + "public": "storybook-static", + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"] + }, + { + "target": "stable", + "public": "storybook-static", + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"] + } + ] +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 15b9142709..bb2359e1f0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -5,6 +5,7 @@ "license": "(MIT OR Apache-2.0)", "type": "module", "scripts": { + "build-storybook": "storybook build", "lint": "eslint components lib", "lint:fix": "eslint components lib --fix", "lint:strict": "tsc --noEmit && eslint components lib --max-warnings 0", @@ -67,6 +68,7 @@ "react-loader-spinner": "^6.1.6", "react-router-dom": "^6.23.1", "sonner": "1.4.3", + "styled-components": "^6.1.11", "tailwind-merge": "^2.3.0", "tinycolor2": "^1.6.0" }, @@ -78,9 +80,11 @@ "@storybook/addon-links": "^8.1.1", "@storybook/addon-postcss": "^2.0.0", "@storybook/blocks": "^8.1.1", + "@storybook/manager-api": "^8.1.11", "@storybook/preview-api": "^8.1.1", "@storybook/react": "^8.1.1", "@storybook/react-vite": "8.1.1", + "@storybook/theming": "^8.1.11", "@types/humanize-duration": "^3.27.4", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", diff --git a/packages/ui/src/Icon/index.stories.ts b/packages/ui/src/Icon/index.stories.ts new file mode 100644 index 0000000000..f8f114b316 --- /dev/null +++ b/packages/ui/src/Icon/index.stories.ts @@ -0,0 +1,25 @@ +import { ArrowRightLeft, Send, Wallet } from 'lucide-react'; +import { Meta, StoryObj } from '@storybook/react'; + +import { Icon } from '.'; + +const meta: Meta = { + component: Icon, + tags: ['autodocs'], + argTypes: { + IconComponent: { + options: ['ArrowRightLeft', 'Send', 'Wallet'], + mapping: { ArrowRightLeft, Send, Wallet }, + }, + }, +}; + +export default meta; + +export const Basic: StoryObj = { + args: { + IconComponent: ArrowRightLeft, + size: 'sm', + color: 'white', + }, +}; diff --git a/packages/ui/src/Icon/index.tsx b/packages/ui/src/Icon/index.tsx new file mode 100644 index 0000000000..eebab87627 --- /dev/null +++ b/packages/ui/src/Icon/index.tsx @@ -0,0 +1,52 @@ +import { LucideIcon } from 'lucide-react'; +import { ComponentProps } from 'react'; + +export type IconSize = 'sm' | 'md' | 'lg'; + +export interface IconProps { + /** + * The icon import from `lucide-react` to render. + * + * ```tsx + * import { ChevronRight } from 'lucide-react'; + * + * ``` + */ + IconComponent: LucideIcon; + /** + * - `sm`: 16px square + * - `md`: 24px square + * - `lg`: 48px square + */ + size: IconSize; + /** + * The CSS color to render the icon with. If left undefined, will default to + * the parent's text color (`currentColor` in SVG terms). + */ + color?: string; +} + +const PROPS_BY_SIZE: Record> = { + sm: { + size: 16, + strokeWidth: 1, + }, + md: { + size: 24, + strokeWidth: 1.5, + }, + lg: { + size: 48, + strokeWidth: 2, + }, +}; + +/** + * Renders the Lucide icon passed in via the `IconComponent` prop. Use this + * component rather than rendering Lucide icon components directly, since this + * component standardizes the stroke width and sizes throughout the Penumbra + * ecosystem. + */ +export const Icon = ({ IconComponent, size = 'sm', color }: IconProps) => ( + +); diff --git a/packages/ui/src/styled-components.d.ts b/packages/ui/src/styled-components.d.ts new file mode 100644 index 0000000000..9e2b2f4f97 --- /dev/null +++ b/packages/ui/src/styled-components.d.ts @@ -0,0 +1,26 @@ +import 'styled-components'; + +declare module 'styled-components' { + export interface DefaultTheme { + fonts: { + default: string; + mono: string; + heading: string; + }; + fontSizes: { + text9xl: string; + text8xl: string; + text7xl: string; + text6xl: string; + text5xl: string; + text4xl: string; + text3xl: string; + text2xl: string; + textXl: string; + textLg: string; + textBase: string; + textSm: string; + textXs: string; + }; + } +} diff --git a/packages/ui/src/utils/theme.ts b/packages/ui/src/utils/theme.ts new file mode 100644 index 0000000000..e16ec68325 --- /dev/null +++ b/packages/ui/src/utils/theme.ts @@ -0,0 +1,24 @@ +import { DefaultTheme } from 'styled-components'; + +export const theme: DefaultTheme = { + fonts: { + default: 'Poppins', + mono: 'Iosevka Term, monospace', + heading: 'Work Sans', + }, + fontSizes: { + text9xl: '8rem', + text8xl: '6rem', + text7xl: '4.5rem', + text6xl: '3.75rem', + text5xl: '3rem', + text4xl: '2.25rem', + text3xl: '1.875rem', + text2xl: '1.5rem', + textXl: '1.25rem', + textLg: '1.125rem', + textBase: '1rem', + textSm: '0.875rem', + textXs: '0.75rem', + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82da4d97ad..a387958e3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -798,6 +798,9 @@ importers: sonner: specifier: 1.4.3 version: 1.4.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + styled-components: + specifier: ^6.1.11 + version: 6.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: specifier: ^2.3.0 version: 2.4.0 @@ -829,6 +832,9 @@ importers: '@storybook/blocks': specifier: ^8.1.1 version: 8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/manager-api': + specifier: ^8.1.11 + version: 8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/preview-api': specifier: ^8.1.1 version: 8.1.11 @@ -838,6 +844,9 @@ importers: '@storybook/react-vite': specifier: 8.1.1 version: 8.1.1(prettier@3.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.18.1)(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1)) + '@storybook/theming': + specifier: ^8.1.11 + version: 8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/humanize-duration': specifier: ^3.27.4 version: 3.27.4