Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
113 changes: 113 additions & 0 deletions examples/storybook/src/stories/design-system/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Button — primary interactive action primitive.
*
* Covers all variants (primary, secondary, outline, ghost, pill, text, list)
* verified across Light and Dark themes.
*/
import React from 'react'
import type { Meta, StoryObj } from '@storybook/react'
import { Button, ButtonText, PillText, Card, Heading, Text, YStack, XStack } from '@goodwidget/ui'
import { Theme } from '@tamagui/core'
import { withDefaultPreset } from '../helpers/withDefaultPreset'

const meta: Meta<typeof Button> = {
title: 'Design System/Primitives/Button',
component: Button,
tags: ['autodocs', 'showcase'],
parameters: { layout: 'padded' },
decorators: [withDefaultPreset],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'outline', 'ghost', 'pill', 'text', 'list'],
description: 'Button visual variant',
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
description: 'Button size scale',
},
disabled: { control: 'boolean', description: 'Disabled state' },
fullWidth: { control: 'boolean', description: 'Full width expand' },
},
}
export default meta
type Story = StoryObj<typeof Button>

const ButtonMatrix = ({ label }: { label: string }) => (
<Card style={{ width: 360, gap: 12 }}>
<Heading level={5}>{label}</Heading>
<YStack gap="$2">
<Button variant="primary">
<ButtonText>Primary Button</ButtonText>
</Button>
<Button variant="secondary">
<ButtonText>Secondary Button</ButtonText>
</Button>
<Button variant="outline">
<ButtonText>Outline Button</ButtonText>
</Button>
<Button variant="ghost">
<ButtonText>Ghost Button</ButtonText>
</Button>
<Button variant="pill">
<PillText>Pill Button</PillText>
</Button>
<Button variant="text">
<ButtonText>Text Link Button</ButtonText>
</Button>
<Button variant="list">
<ButtonText>List Item Button</ButtonText>
</Button>
</YStack>
</Card>
)

/** Default Button rendering. */
export const Default: Story = {
render: () => (
<Button variant="primary">
<ButtonText>Primary Button</ButtonText>
</Button>
),
}

/** All variants in Light theme — verifies non-primary buttons render readable dark text. */
export const LightThemeVariants: Story = {
render: () => (
<Theme name="light">
<YStack backgroundColor="$background" padding="$4" borderRadius="$4">
<ButtonMatrix label="Light Theme Button Variants" />
</YStack>
</Theme>
),
}

/** All variants in Dark theme — verifies readability and contrast. */
export const DarkThemeVariants: Story = {
render: () => (
<Theme name="dark">
<YStack backgroundColor="$background" padding="$4" borderRadius="$4">
<ButtonMatrix label="Dark Theme Button Variants" />
</YStack>
</Theme>
),
}

/** Side-by-side comparison between Light and Dark themes. */
export const SideBySideComparison: Story = {
render: () => (
<XStack gap="$4" flexWrap="wrap">
<Theme name="light">
<YStack backgroundColor="$background" padding="$4" borderRadius="$4">
<ButtonMatrix label="Light Theme" />
</YStack>
</Theme>
<Theme name="dark">
<YStack backgroundColor="$background" padding="$4" borderRadius="$4">
<ButtonMatrix label="Dark Theme" />
</YStack>
</Theme>
</XStack>
),
}
98 changes: 98 additions & 0 deletions examples/storybook/src/stories/design-system/Separator.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Separator — divider line primitive.
*
* Covers horizontal sizes (sm: 1px, md: 2px, lg: 4px), color variants
* (default, muted, primary), and vertical orientation.
*/
import React from 'react'
import type { Meta, StoryObj } from '@storybook/react'
import { Separator, Card, Heading, Text, YStack, XStack } from '@goodwidget/ui'
import { Theme } from '@tamagui/core'
import { withDefaultPreset } from '../helpers/withDefaultPreset'

const meta: Meta<typeof Separator> = {
title: 'Design System/Primitives/Separator',
component: Separator,
tags: ['autodocs', 'showcase'],
parameters: { layout: 'padded' },
decorators: [withDefaultPreset],
argTypes: {
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
description: 'Separator thickness variant',
},
color: {
control: 'select',
options: ['default', 'muted', 'primary'],
description: 'Color style variant',
},
vertical: { control: 'boolean', description: 'Vertical orientation' },
},
}
export default meta
type Story = StoryObj<typeof Separator>

const SeparatorMatrix = ({ label }: { label: string }) => (
<Card style={{ width: 360, gap: 12 }}>
<Heading level={5}>{label}</Heading>
<YStack gap="$3">
<Text tone="secondary">Horizontal sm (1px)</Text>
<Separator size="sm" />

<Text tone="secondary">Horizontal md (2px)</Text>
<Separator size="md" />

<Text tone="secondary">Horizontal lg (4px)</Text>
<Separator size="lg" />

<Text tone="secondary">Primary color</Text>
<Separator size="md" color="primary" />

<Text tone="secondary">Muted color</Text>
<Separator size="md" color="muted" />

<Text tone="secondary">Vertical separator in flex row</Text>
<XStack height={40} alignItems="center" gap="$3">
<Text>Left</Text>
<Separator vertical />
<Text>Middle</Text>
<Separator vertical color="primary" />
<Text>Right</Text>
</XStack>
</YStack>
</Card>
)

/** Default Separator. */
export const Default: Story = {
render: () => (
<YStack width={300} gap="$2">
<Text>Above</Text>
<Separator />
<Text>Below</Text>
</YStack>
),
}

/** Horizontal sizes and colors in Light theme. */
export const LightTheme: Story = {
render: () => (
<Theme name="light">
<YStack backgroundColor="$background" padding="$4" borderRadius="$4">
<SeparatorMatrix label="Light Theme Separators" />
</YStack>
</Theme>
),
}

/** Horizontal sizes and colors in Dark theme. */
export const DarkTheme: Story = {
render: () => (
<Theme name="dark">
<YStack backgroundColor="$background" padding="$4" borderRadius="$4">
<SeparatorMatrix label="Dark Theme Separators" />
</YStack>
</Theme>
),
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
*/
import React from 'react'
import type { Meta, StoryObj } from '@storybook/react'
import { TokenAmount, Card, Heading, YStack } from '@goodwidget/ui'
import { TokenAmount, Card, Heading, YStack, XStack } from '@goodwidget/ui'
import { Theme } from '@tamagui/core'
import { withDefaultPreset } from '../helpers/withDefaultPreset'

const meta: Meta<typeof TokenAmount> = {
Expand Down Expand Up @@ -51,3 +52,32 @@ export const Controllable: Story = {
token: 'G$',
},
}

/** Token amounts rendered in Light theme — verifies visible spans and contrasting text. */
export const LightTheme: Story = {
render: () => (
<Theme name="light">
<YStack backgroundColor="$background" padding="$4" borderRadius="$4" width={320} gap="$2">
<Heading level={5}>Light Theme Amounts</Heading>
<TokenAmount amount="1234.56" token="G$" size="lg" />
<TokenAmount amount="100.00" token="USDC" size="md" />
<TokenAmount amount="42.50" token="CELO" size="sm" variant="secondary" />
</YStack>
</Theme>
),
}

/** Token amounts rendered in Dark theme. */
export const DarkTheme: Story = {
render: () => (
<Theme name="dark">
<YStack backgroundColor="$background" padding="$4" borderRadius="$4" width={320} gap="$2">
<Heading level={5}>Dark Theme Amounts</Heading>
<TokenAmount amount="1234.56" token="G$" size="lg" />
<TokenAmount amount="100.00" token="USDC" size="md" />
<TokenAmount amount="42.50" token="CELO" size="sm" variant="secondary" />
</YStack>
</Theme>
),
}

1 change: 1 addition & 0 deletions packages/ui/src/components-test/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const InputFrame = createComponent(Stack, {
paddingHorizontal: '$3',
height: '$10',
color: '$color',
placeholderTextColor: '$placeholderColor',
fontFamily: '$body',
fontSize: '$3',
outlineWidth: 0,
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/src/components-test/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ const OptionItem = styled(Stack, {
paddingVertical: '$2',
cursor: 'pointer',
backgroundColor: '$background',
borderWidth: 0,
borderColor: 'transparent',
outlineWidth: 0,

hoverStyle: {
backgroundColor: '$backgroundHover',
Expand Down
33 changes: 24 additions & 9 deletions packages/ui/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import type { ReactNode } from 'react'
import { styled, Stack, Text as TamaguiText, Theme } from 'tamagui'
import { styled, Stack, Text as TamaguiText } from 'tamagui'
import { createComponent } from '../createComponent'

/**
Expand All @@ -23,7 +23,7 @@ import { createComponent } from '../createComponent'
* Override per-instance with explicit borderRadius prop if needed.
*/
export const ButtonFrame = createComponent(Stack, {
name: 'Button',
name: 'ButtonFrame',
tag: 'button',
role: 'button',
flexDirection: 'row',
Expand Down Expand Up @@ -177,6 +177,18 @@ export const ButtonText = styled(TamaguiText, {
fontWeight: '600',
color: '$textColor',
userSelect: 'none',

variants: {
variant: {
primary: { color: '$textColor' },
secondary: { color: '$color' },
outline: { color: '$color' },
ghost: { color: '$color' },
pill: { color: '$color' },
text: { color: '$color' },
list: { color: '$color' },
},
} as const,
})

/** Pill label text — uppercase 11px, tracking wide */
Expand Down Expand Up @@ -206,16 +218,19 @@ export interface ButtonProps {
* The light_Button / dark_Button component theme sets `color: white` for
* primary buttons rendered on the brand-colored background. Non-primary
* variants (secondary, outline, ghost, text, list) use transparent or no
* background, so the inherited white would be invisible. `Theme reset`
* discards the Button component theme, letting children (ButtonText) pick
* up the parent theme's text color — which is the correct readable color
* for transparent-background buttons.
* background, so they render with the ambient theme's text color.
* The `Button` component theme is only bound to primary buttons, preventing
* non-primary buttons from resolving to primary-button white in Light theme.
*/
export function Button({ variant = 'primary', children, ...props }: ButtonProps) {
const needsReset = variant !== 'primary'
const isPrimary = variant === 'primary'
return (
<ButtonFrame variant={variant} {...props}>
{needsReset ? <Theme reset>{children}</Theme> : children}
<ButtonFrame
theme={isPrimary ? 'Button' : undefined}
variant={variant}
{...props}
>
{children}
</ButtonFrame>
)
}
4 changes: 2 additions & 2 deletions packages/ui/src/components/CircularActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function CircularActionButton({
pending = false,
onPress,
}: CircularActionButtonProps) {
const labelColor = pending || disabled ? '$grey600' : '$primary'
const labelColor = pending || disabled ? '$colorDim' : '$primary'

return (
<ClaimActionButton onPress={disabled ? undefined : onPress} disabled={disabled}>
Expand All @@ -102,7 +102,7 @@ export function CircularActionButton({
<ButtonText color={labelColor} textAlign="center">
{label}
</ButtonText>
<Spinner size="sm" color="$grey600" />
<Spinner size="sm" color={labelColor} />
</XStack>
) : (
<ButtonText color={labelColor} textAlign="center">
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/components/Separator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ export const Separator = createComponent(TamaguiSeparator as any, {
backgroundColor: '$borderColor',
borderWidth: 0,
borderColor: 'transparent',
maxHeight: 'none',

variants: {
size: {
sm: { height: 1 },
md: { height: 2 },
lg: { height: 4 },
sm: { height: 1, minHeight: 1, maxHeight: 1 },
md: { height: 2, minHeight: 2, maxHeight: 2 },
lg: { height: 4, minHeight: 4, maxHeight: 4 },
},
color: {
// Uses the current theme borderColor — adapts to light/dark and presets
Expand All @@ -37,6 +38,8 @@ export const Separator = createComponent(TamaguiSeparator as any, {
vertical: {
true: {
height: '100%',
minHeight: 'unset',
maxHeight: 'none',
width: 1,
},
},
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/components/TokenAmount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const TokenAmountText = createComponent(TamaguiText, {
name: 'TokenAmountText',
fontFamily: '$body',
fontWeight: '700',
color: '$text',
color: '$color',

variants: {
size: {
Expand All @@ -37,7 +37,7 @@ const TokenAmountText = createComponent(TamaguiText, {
},
variant: {
secondary: {
color: '$secondaryColor',
color: '$placeholderColor',
fontWeight: 500,
},
},
Expand Down
Loading