diff --git a/examples/storybook/src/stories/design-system/Button.stories.tsx b/examples/storybook/src/stories/design-system/Button.stories.tsx new file mode 100644 index 00000000..dc1c1b3c --- /dev/null +++ b/examples/storybook/src/stories/design-system/Button.stories.tsx @@ -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 = { + 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 + +const ButtonMatrix = ({ label }: { label: string }) => ( + + {label} + + + + + + + + + + +) + +/** Default Button rendering. */ +export const Default: Story = { + render: () => ( + + ), +} + +/** All variants in Light theme — verifies non-primary buttons render readable dark text. */ +export const LightThemeVariants: Story = { + render: () => ( + + + + + + ), +} + +/** All variants in Dark theme — verifies readability and contrast. */ +export const DarkThemeVariants: Story = { + render: () => ( + + + + + + ), +} + +/** Side-by-side comparison between Light and Dark themes. */ +export const SideBySideComparison: Story = { + render: () => ( + + + + + + + + + + + + + ), +} diff --git a/examples/storybook/src/stories/design-system/Separator.stories.tsx b/examples/storybook/src/stories/design-system/Separator.stories.tsx new file mode 100644 index 00000000..e5d0c0a6 --- /dev/null +++ b/examples/storybook/src/stories/design-system/Separator.stories.tsx @@ -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 = { + 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 + +const SeparatorMatrix = ({ label }: { label: string }) => ( + + {label} + + Horizontal sm (1px) + + + Horizontal md (2px) + + + Horizontal lg (4px) + + + Primary color + + + Muted color + + + Vertical separator in flex row + + Left + + Middle + + Right + + + +) + +/** Default Separator. */ +export const Default: Story = { + render: () => ( + + Above + + Below + + ), +} + +/** Horizontal sizes and colors in Light theme. */ +export const LightTheme: Story = { + render: () => ( + + + + + + ), +} + +/** Horizontal sizes and colors in Dark theme. */ +export const DarkTheme: Story = { + render: () => ( + + + + + + ), +} diff --git a/examples/storybook/src/stories/design-system/TokenAmount.stories.tsx b/examples/storybook/src/stories/design-system/TokenAmount.stories.tsx index 1916a982..f4093ddd 100644 --- a/examples/storybook/src/stories/design-system/TokenAmount.stories.tsx +++ b/examples/storybook/src/stories/design-system/TokenAmount.stories.tsx @@ -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 = { @@ -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: () => ( + + + Light Theme Amounts + + + + + + ), +} + +/** Token amounts rendered in Dark theme. */ +export const DarkTheme: Story = { + render: () => ( + + + Dark Theme Amounts + + + + + + ), +} + diff --git a/packages/ui/src/components-test/Input.tsx b/packages/ui/src/components-test/Input.tsx index 7d55795f..1222d879 100644 --- a/packages/ui/src/components-test/Input.tsx +++ b/packages/ui/src/components-test/Input.tsx @@ -15,6 +15,7 @@ const InputFrame = createComponent(Stack, { paddingHorizontal: '$3', height: '$10', color: '$color', + placeholderTextColor: '$placeholderColor', fontFamily: '$body', fontSize: '$3', outlineWidth: 0, diff --git a/packages/ui/src/components-test/Select.tsx b/packages/ui/src/components-test/Select.tsx index b71b63c0..effe436e 100644 --- a/packages/ui/src/components-test/Select.tsx +++ b/packages/ui/src/components-test/Select.tsx @@ -38,6 +38,9 @@ const OptionItem = styled(Stack, { paddingVertical: '$2', cursor: 'pointer', backgroundColor: '$background', + borderWidth: 0, + borderColor: 'transparent', + outlineWidth: 0, hoverStyle: { backgroundColor: '$backgroundHover', diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx index eac8fa18..66da703c 100644 --- a/packages/ui/src/components/Button.tsx +++ b/packages/ui/src/components/Button.tsx @@ -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' /** @@ -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', @@ -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 */ @@ -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 ( - - {needsReset ? {children} : children} + + {children} ) } diff --git a/packages/ui/src/components/CircularActionButton.tsx b/packages/ui/src/components/CircularActionButton.tsx index 6b0e41ea..c5cf4875 100644 --- a/packages/ui/src/components/CircularActionButton.tsx +++ b/packages/ui/src/components/CircularActionButton.tsx @@ -77,7 +77,7 @@ export function CircularActionButton({ pending = false, onPress, }: CircularActionButtonProps) { - const labelColor = pending || disabled ? '$grey600' : '$primary' + const labelColor = pending || disabled ? '$colorDim' : '$primary' return ( @@ -102,7 +102,7 @@ export function CircularActionButton({ {label} - + ) : ( diff --git a/packages/ui/src/components/Separator.ts b/packages/ui/src/components/Separator.ts index c048ce69..d5f8844e 100644 --- a/packages/ui/src/components/Separator.ts +++ b/packages/ui/src/components/Separator.ts @@ -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 @@ -37,6 +38,8 @@ export const Separator = createComponent(TamaguiSeparator as any, { vertical: { true: { height: '100%', + minHeight: 'unset', + maxHeight: 'none', width: 1, }, }, diff --git a/packages/ui/src/components/TokenAmount.tsx b/packages/ui/src/components/TokenAmount.tsx index 4dbdf658..4a7f5981 100644 --- a/packages/ui/src/components/TokenAmount.tsx +++ b/packages/ui/src/components/TokenAmount.tsx @@ -26,7 +26,7 @@ const TokenAmountText = createComponent(TamaguiText, { name: 'TokenAmountText', fontFamily: '$body', fontWeight: '700', - color: '$text', + color: '$color', variants: { size: { @@ -37,7 +37,7 @@ const TokenAmountText = createComponent(TamaguiText, { }, variant: { secondary: { - color: '$secondaryColor', + color: '$placeholderColor', fontWeight: 500, }, }, diff --git a/packages/ui/src/theme.ts b/packages/ui/src/theme.ts index 037a62cd..ed15d8af 100644 --- a/packages/ui/src/theme.ts +++ b/packages/ui/src/theme.ts @@ -135,6 +135,7 @@ export function createThemeValues( backgroundOverlay: color.overlay, color: color.text, + textColor: color.text, colorHover: color.text, colorPress: color.textSecondary, colorFocus: color.text, @@ -167,6 +168,7 @@ export function createThemeValues( backgroundOverlay: color.overlay, color: color.textDark, + textColor: color.textDark, colorHover: color.textDark, colorPress: color.textSecondaryDark, colorFocus: color.textDark, @@ -220,6 +222,7 @@ export function createThemeValues( backgroundFocus: color.surface, backgroundTransparent: color.transparent, color: color.text, + textColor: color.text, colorHover: color.text, colorPress: color.textSecondary, colorFocus: color.text, @@ -242,6 +245,7 @@ export function createThemeValues( backgroundFocus: color.white, backgroundTransparent: color.transparent, color: color.text, + textColor: color.text, colorHover: color.text, colorPress: color.text, colorFocus: color.text, @@ -287,6 +291,7 @@ export function createThemeValues( backgroundFocus: color.surfaceDark, backgroundTransparent: color.transparent, color: color.textDark, + textColor: color.textDark, colorHover: color.textDark, colorPress: color.textSecondaryDark, colorFocus: color.textDark, @@ -321,6 +326,7 @@ export function createThemeValues( backgroundFocus: color.surfaceDark, backgroundTransparent: color.transparent, color: color.textDark, + textColor: color.textDark, colorHover: color.textDark, colorPress: color.textDark, colorFocus: color.textDark,