Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
331 changes: 331 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions web/common/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ tsconfig.tsbuildinfo

*storybook.log
storybook-static
**/__snapshots__/**
**/__screenshots__/**
8 changes: 8 additions & 0 deletions web/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
"devDependencies": {
"@eslint/js": "^9.31.0",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.8",
"@storybook/addon-docs": "^9.1.2",
"@storybook/addon-essentials": "^9.0.0-alpha.12",
"@storybook/addon-onboarding": "^9.1.2",
"@storybook/react-vite": "^9.1.2",
"@storybook/test": "^8.6.14",
"@tailwindcss/typography": "^0.5.16",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.6.3",
Expand All @@ -17,20 +19,23 @@
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/browser": "^3.2.4",
"@xyflow/react": "^12.8.4",
"autoprefixer": "^10.4.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"eslint": "^9.31.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-storybook": "^9.1.2",
"globals": "^16.3.0",
"lucide-react": "^0.542.0",
"playwright": "^1.54.1",
"postcss": "^8.5.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"storybook": "^9.1.2",
"syncpack": "^13.0.4",
"tailwind-merge": "^3.3.1",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
Expand Down Expand Up @@ -62,9 +67,12 @@
"module": "dist/sqlmesh-common.es.js",
"peerDependencies": {
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/typography": "^0.5.16",
"@xyflow/react": "^12.8.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.542.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^3.3.1",
Expand Down
2 changes: 1 addition & 1 deletion web/common/src/components/Badge/Badge.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
:root {
--color-badge-background: var(--color-gray-100);
--color-badge-background: var(--color-neutral-100);
--color-badge-foreground: var(--color-prose);
}
33 changes: 33 additions & 0 deletions web/common/src/components/Button/Button.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
:root {
--color-button-focused: var(--color-gray-100);

--color-button-primary-background: var(--color-gray-100);
--color-button-primary-foreground: var(--color-prose);
--color-button-primary-hover: var(--color-gray-100);
--color-button-primary-active: var(--color-gray-100);

--color-button-secondary-background: var(--color-gray-100);
--color-button-secondary-foreground: var(--color-prose);
--color-button-secondary-hover: var(--color-gray-100);
--color-button-secondary-active: var(--color-gray-100);

--color-button-alternative-background: var(--color-gray-100);
--color-button-alternative-foreground: var(--color-prose);
--color-button-alternative-hover: var(--color-gray-100);
--color-button-alternative-active: var(--color-gray-100);

--color-button-destructive-background: var(--color-gray-100);
--color-button-destructive-foreground: var(--color-prose);
--color-button-destructive-hover: var(--color-gray-100);
--color-button-destructive-active: var(--color-gray-100);

--color-button-danger-background: var(--color-gray-100);
--color-button-danger-foreground: var(--color-prose);
--color-button-danger-hover: var(--color-gray-100);
--color-button-danger-active: var(--color-gray-100);

--color-button-transparent-background: var(--color-gray-100);
--color-button-transparent-foreground: var(--color-prose);
--color-button-transparent-hover: var(--color-gray-100);
--color-button-transparent-active: var(--color-gray-100);
}
157 changes: 157 additions & 0 deletions web/common/src/components/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { expect, userEvent, within } from 'storybook/test'

import { EnumSize } from '@/types/enums'
import { Button } from './Button'
import { EnumButtonVariant } from './help'

const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
onClick: { action: 'clicked' },
variant: {
control: { type: 'select' },
options: Object.values(EnumButtonVariant),
},
size: {
control: { type: 'select' },
options: Object.values(EnumSize),
},
type: {
control: { type: 'select' },
options: ['button', 'submit', 'reset'],
},
disabled: {
control: 'boolean',
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to write these? We should be able to set it up so an enum can be inferred?

},
}

export default meta

type Story = StoryObj<typeof Button>

export const Default: Story = {
args: {
children: 'Default Button',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)
await expect(canvas.getByText('Default Button')).toBeInTheDocument()
},
}

export const Variants: Story = {
render: args => (
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{Object.values(EnumButtonVariant).map(variant => (
<Button
key={variant}
{...args}
variant={variant}
>
{variant}
</Button>
))}
</div>
),
}

export const Sizes: Story = {
render: args => (
<div
style={{
display: 'flex',
gap: 12,
flexWrap: 'wrap',
alignItems: 'center',
}}
>
{Object.values(EnumSize).map(size => (
<Button
key={size}
{...args}
size={size}
>
{size}
</Button>
))}
</div>
),
}

export const Disabled: Story = {
args: {
children: 'Disabled Button',
disabled: true,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)
const button = canvas.getByRole('button')
await expect(button).toBeDisabled()
await expect(button).toHaveTextContent('Disabled Button')
},
}

export const AsChild: Story = {
render: args => (
<Button
asChild
{...args}
>
<a href="#">Link as Button</a>
</Button>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)
const linkElement = canvas.getByText('Link as Button')
await expect(linkElement.tagName).toBe('A')
await expect(linkElement).toHaveAttribute('href', '#')
},
}

export const Types: Story = {
render: args => (
<div style={{ display: 'flex', gap: 12 }}>
<Button
{...args}
type="button"
>
Button
</Button>
<Button
{...args}
type="submit"
>
Submit
</Button>
<Button
{...args}
type="reset"
>
Reset
</Button>
</div>
),
}

export const InteractiveClick: Story = {
args: {
children: 'Click Me',
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement)
const user = userEvent.setup()

const button = canvas.getByRole('button')
await expect(button).toBeInTheDocument()

await user.click(button)
await expect(args.onClick).toHaveBeenCalledTimes(1)

await user.click(button)
await expect(args.onClick).toHaveBeenCalledTimes(2)
},
}
35 changes: 35 additions & 0 deletions web/common/src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Slot } from '@radix-ui/react-slot'
import type { VariantProps } from 'class-variance-authority'
import React from 'react'

import { cn } from '@/utils'
import { buttonVariants } from './help'
import type { ButtonType } from './help'

import './Button.css'

const DEFAULT_BUTTON_TYPE = 'button'

export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
type?: ButtonType
}
Comment on lines +13 to +24
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have been looking at this Button and I worry that the structure of it makes it quite difficult to figure out how it works. I think we can do better to make these components easier to consume for each other.

Here is my thought process. When you use a component, the way you want to understand it is:

  1. What are the props?
  2. How does it function?

This is why in past lives I have always structured a component file like.

// Imports
import type { ButtonType } from './help'

// Prop of component, named Props so you know it's the props for that component
export interface Props {}

// Component
export const Component = () => {}

// Anything else that makes up the Component
const InternalComponent = () => {}

It's not a thing but figuring out how this Button component works requires quite a bit of jumping around files and while this is small, I think the complexity is pretty signficant to figure out for something so small.


const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, type, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : DEFAULT_BUTTON_TYPE
return (
<Comp
type={type}
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
},
)
Button.displayName = 'Button'

export { Button }
58 changes: 58 additions & 0 deletions web/common/src/components/Button/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { cva } from 'class-variance-authority'

import { EnumSize } from '@/types/enums'

export const EnumButtonType = {
Button: 'button',
Submit: 'submit',
Reset: 'reset',
} as const
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am going to keep slowly pushing against enums 👅

https://bluepnume.medium.com/nine-terrible-ways-to-use-typescript-enums-and-one-good-way-f9c7ec68bf15 or even subtypes.

type ButtonType = "button" | "submit" | "reset"

verifying switches are exhaustive

and the usage with just to me is a nicer experience.


export type ButtonType = (typeof EnumButtonType)[keyof typeof EnumButtonType]

export const EnumButtonVariant = {
Primary: 'primary',
Secondary: 'secondary',
Alternative: 'alternative',
Destructive: 'destructive',
Danger: 'danger',
Transparent: 'transparent',
} as const

export type ButtonVariant =
(typeof EnumButtonVariant)[keyof typeof EnumButtonVariant]

export const buttonVariants = cva(
'inline-flex items-center w-fit justify-center gap-1 whitespace-nowrap leading-none font-semibold ring-offset-light transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focused focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 border border-[transparent]',
{
variants: {
variant: {
[EnumButtonVariant.Primary]:
'bg-button-primary-background text-button-primary-foreground hover:bg-button-primary-hover active:bg-button-primary-active',
[EnumButtonVariant.Secondary]:
'bg-button-secondary-background text-button-secondary-foreground hover:bg-button-secondary-hover active:bg-button-secondary-active',
[EnumButtonVariant.Alternative]:
'bg-button-alternative-background text-button-alternative-foreground border-neutral-200 hover:bg-button-alternative-hover active:bg-button-alternative-active',
[EnumButtonVariant.Destructive]:
'bg-button-destructive-background text-button-destructive-foreground hover:bg-button-destructive-hover active:bg-button-destructive-active',
[EnumButtonVariant.Danger]:
'bg-button-danger-background text-button-danger-foreground hover:bg-button-danger-hover active:bg-button-danger-active',
[EnumButtonVariant.Transparent]:
'bg-button-transparent-background text-button-transparent-foreground hover:bg-button-transparent-hover active:bg-button-transparent-active',
},
size: {
[EnumSize.XXS]: 'h-5 px-2 text-2xs leading-none rounded-2xs',
[EnumSize.XS]: 'h-6 px-2 text-2xs rounded-xs',
[EnumSize.S]: 'h-7 px-3 text-xs rounded-sm',
[EnumSize.M]: 'h-8 px-4 rounded-md',
[EnumSize.L]: 'h-9 px-4 rounded-lg',
[EnumSize.XL]: 'h-10 px-4 rounded-xl',
[EnumSize.XXL]: 'h-11 px-6 rounded-2xl',
},
},
defaultVariants: {
variant: EnumButtonVariant.Primary,
size: EnumSize.S,
},
},
)
Loading
Loading