From 569f7acae520debd911e27ee403e90c0348fdb98 Mon Sep 17 00:00:00 2001 From: Thompson <140930314+Godbrand0@users.noreply.github.com> Date: Wed, 1 Jul 2026 03:58:49 -0400 Subject: [PATCH 01/12] feat(storybook): wire defaultTheme/brandPreset controls into widget stories Adds interactive Storybook controls for defaultTheme and themeOverrides across all four widget showcase stories (StreamingWidget, CitizenClaimWidget, StakingMigrationWidget, ClaimWidget) plus the Card/GlowCard/Stepper/Drawer design-system primitives, so integrators can experiment with theming live in the Controls panel instead of editing story code. Extracts the cobalt/teal brand override presets into a shared helper reused across widgets. --- .../CitizenClaimWidgetShowcase.stories.tsx | 30 +++++++++- .../claim-widget/ClaimWidget.stories.tsx | 58 ++++++++++++++----- .../stories/design-system/Card.stories.tsx | 20 +++++++ .../stories/design-system/Drawer.stories.tsx | 35 +++++++++++ .../design-system/GlowCard.stories.tsx | 18 ++++++ .../stories/design-system/Stepper.stories.tsx | 47 +++++++++++---- .../helpers/citizenClaimWidgetStories.tsx | 19 +++++- .../stories/helpers/claimWidgetStories.tsx | 34 +---------- .../helpers/stakingMigrationWidgetStories.tsx | 11 +++- .../helpers/streamingWidgetStories.tsx | 12 +++- .../stories/helpers/themeOverridePresets.ts | 54 +++++++++++++++++ ...StakingMigrationWidgetShowcase.stories.tsx | 30 +++++++++- .../StreamingWidget.stories.tsx | 33 +++++++++-- 13 files changed, 328 insertions(+), 73 deletions(-) create mode 100644 examples/storybook/src/stories/helpers/themeOverridePresets.ts diff --git a/examples/storybook/src/stories/citizen-claim-widget/CitizenClaimWidgetShowcase.stories.tsx b/examples/storybook/src/stories/citizen-claim-widget/CitizenClaimWidgetShowcase.stories.tsx index 2f150a03..ea1475e0 100644 --- a/examples/storybook/src/stories/citizen-claim-widget/CitizenClaimWidgetShowcase.stories.tsx +++ b/examples/storybook/src/stories/citizen-claim-widget/CitizenClaimWidgetShowcase.stories.tsx @@ -1,17 +1,41 @@ import type { Meta, StoryObj } from '@storybook/react' import { CitizenClaimWidget } from '@goodwidget/citizen-claim-widget' import { InjectedWalletStory } from '../helpers/citizenClaimWidgetStories' +import { BRAND_PRESET_OPTIONS, brandPresetOverrides, type BrandPreset } from '../helpers/themeOverridePresets' -const meta: Meta = { +interface CitizenClaimWidgetStoryArgs { + defaultTheme: 'light' | 'dark' + brandPreset: BrandPreset +} + +const meta: Meta = { title: 'Widgets/CitizenClaimWidget/Showcase', component: CitizenClaimWidget, tags: ['integrator', 'manual', 'showcase'], parameters: { layout: 'padded' }, + argTypes: { + defaultTheme: { + control: 'radio', + options: ['dark', 'light'], + description: 'Base theme applied via the widget’s own defaultTheme prop.', + }, + brandPreset: { + control: 'select', + options: BRAND_PRESET_OPTIONS, + description: 'Sample host-branding themeOverrides preset.', + }, + }, + args: { + defaultTheme: 'dark', + brandPreset: 'None', + }, } export default meta -type Story = StoryObj +type Story = StoryObj export const InjectedWallet: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } diff --git a/examples/storybook/src/stories/claim-widget/ClaimWidget.stories.tsx b/examples/storybook/src/stories/claim-widget/ClaimWidget.stories.tsx index cf218fd2..bd619740 100644 --- a/examples/storybook/src/stories/claim-widget/ClaimWidget.stories.tsx +++ b/examples/storybook/src/stories/claim-widget/ClaimWidget.stories.tsx @@ -7,13 +7,15 @@ */ import type { Meta, StoryObj } from '@storybook/react' import { ClaimWidget } from '@goodwidget/claim-widget-theme-demo' -import { - ClaimWidgetStoryCanvas, - cobaltOverrides, - tealOverrides, -} from '../helpers/claimWidgetStories' +import { ClaimWidgetStoryCanvas } from '../helpers/claimWidgetStories' +import { BRAND_PRESET_OPTIONS, brandPresetOverrides, type BrandPreset } from '../helpers/themeOverridePresets' -const meta: Meta = { +interface ClaimWidgetStoryArgs { + defaultTheme: 'light' | 'dark' + brandPreset: BrandPreset +} + +const meta: Meta = { title: 'Widgets/ClaimWidget Theme Demo/Showcase', component: ClaimWidget, tags: ['integrator', 'showcase'], @@ -22,33 +24,63 @@ const meta: Meta = { disableProvider: true, useShell: false, }, + argTypes: { + defaultTheme: { + control: 'radio', + options: ['dark', 'light'], + description: 'Base theme applied via the widget’s own defaultTheme prop.', + }, + brandPreset: { + control: 'select', + options: BRAND_PRESET_OPTIONS, + description: 'Sample host-branding themeOverrides preset.', + }, + }, } export default meta -type Story = StoryObj +type Story = StoryObj export const Default: Story = { - render: () => , + args: { defaultTheme: 'dark', brandPreset: 'None' }, + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const LightTheme: Story = { - render: () => , + args: { defaultTheme: 'light', brandPreset: 'None' }, + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const CobaltBrand: Story = { - render: () => ( + args: { defaultTheme: 'dark', brandPreset: 'Cobalt' }, + render: ({ defaultTheme, brandPreset }) => ( ), } export const TealBrand: Story = { - render: () => ( + args: { defaultTheme: 'dark', brandPreset: 'Teal' }, + render: ({ defaultTheme, brandPreset }) => ( ), } diff --git a/examples/storybook/src/stories/design-system/Card.stories.tsx b/examples/storybook/src/stories/design-system/Card.stories.tsx index be449457..9f88e098 100644 --- a/examples/storybook/src/stories/design-system/Card.stories.tsx +++ b/examples/storybook/src/stories/design-system/Card.stories.tsx @@ -15,6 +15,12 @@ const meta: Meta = { tags: ['autodocs', 'showcase'], parameters: { layout: 'padded' }, decorators: [withDefaultPreset], + argTypes: { + elevated: { control: 'boolean', description: 'Applies the elevated shadow variant' }, + outlined: { control: 'boolean', description: 'Applies the outlined border variant' }, + backgroundColor: { control: 'color', description: 'Inline background color override' }, + borderColor: { control: 'color', description: 'Inline border color override' }, + }, } export default meta type Story = StoryObj @@ -61,3 +67,17 @@ export const InlineStyled: Story = { ), } + +/** Controllable instance — edit args in the Controls panel. */ +export const Controllable: Story = { + args: { + elevated: true, + outlined: false, + }, + render: (args) => ( + + Controllable Card + Use the Controls panel to toggle variants and colors. + + ), +} diff --git a/examples/storybook/src/stories/design-system/Drawer.stories.tsx b/examples/storybook/src/stories/design-system/Drawer.stories.tsx index 3de53a1b..008ad776 100644 --- a/examples/storybook/src/stories/design-system/Drawer.stories.tsx +++ b/examples/storybook/src/stories/design-system/Drawer.stories.tsx @@ -15,6 +15,13 @@ const meta: Meta = { tags: ['autodocs', 'showcase'], parameters: { layout: 'padded' }, decorators: [withDefaultPreset], + argTypes: { + height: { + control: 'radio', + options: ['half', 'full'], + description: 'How much of the viewport the Drawer covers when open', + }, + }, } export default meta type Story = StoryObj @@ -52,3 +59,31 @@ export const Default: Story = { await expect(closeButton).toBeDefined() }, } + +/** Controllable instance — edit the `height` arg, then click "Open Drawer". */ +export const Controllable: Story = { + args: { + height: 'half', + }, + render: ({ height }: { height?: 'half' | 'full' }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [open, setOpen] = useState(false) + return ( + + Trigger + A Drawer slides up from the bottom and overlays the content. + + setOpen(false)} height={height}> + + Drawer content. Close via the button below or tap outside. + + + + + ) + }, +} diff --git a/examples/storybook/src/stories/design-system/GlowCard.stories.tsx b/examples/storybook/src/stories/design-system/GlowCard.stories.tsx index 2e3f4a29..dc8f33a1 100644 --- a/examples/storybook/src/stories/design-system/GlowCard.stories.tsx +++ b/examples/storybook/src/stories/design-system/GlowCard.stories.tsx @@ -15,6 +15,10 @@ const meta: Meta = { tags: ['autodocs', 'showcase'], parameters: { layout: 'padded' }, decorators: [withDefaultPreset], + argTypes: { + elevated: { control: 'boolean', description: 'Applies the elevated shadow variant' }, + outlined: { control: 'boolean', description: 'Applies the outlined border variant' }, + }, } export default meta type Story = StoryObj @@ -31,3 +35,17 @@ export const Default: Story = { ), } + +/** Controllable instance — edit args in the Controls panel. */ +export const Controllable: Story = { + args: { + elevated: true, + outlined: false, + }, + render: (args) => ( + + Controllable GlowCard + Use the Controls panel to toggle variants. + + ), +} diff --git a/examples/storybook/src/stories/design-system/Stepper.stories.tsx b/examples/storybook/src/stories/design-system/Stepper.stories.tsx index 11e837c9..51123a01 100644 --- a/examples/storybook/src/stories/design-system/Stepper.stories.tsx +++ b/examples/storybook/src/stories/design-system/Stepper.stories.tsx @@ -3,17 +3,6 @@ import type { Meta, StoryObj } from '@storybook/react' import { Stepper, Text, YStack, type StepperStepItem } from '@goodwidget/ui' import { withDefaultPreset } from '../helpers/withDefaultPreset' -const meta: Meta = { - title: 'Design System/Primitives/Stepper', - component: Stepper, - tags: ['autodocs', 'showcase'], - parameters: { layout: 'padded' }, - decorators: [withDefaultPreset], -} - -export default meta -type Story = StoryObj - const STEPS: StepperStepItem[] = [ { id: 'connect', title: 'Connect wallet', status: 'completed' }, { id: 'approve', title: 'Approve transaction', status: 'completed' }, @@ -28,6 +17,25 @@ const STEPS: StepperStepItem[] = [ { id: 'confirm', title: 'Confirm receipt', status: 'pending' }, ] +const meta: Meta = { + title: 'Design System/Primitives/Stepper', + component: Stepper, + tags: ['autodocs', 'showcase'], + parameters: { layout: 'padded' }, + decorators: [withDefaultPreset], + argTypes: { + activeStepId: { + control: 'select', + options: STEPS.map((step) => step.id), + description: 'Which step is highlighted as active', + }, + maxHeight: { control: 'number', description: 'Max height of the scrollable step list' }, + }, +} + +export default meta +type Story = StoryObj + export const Default: Story = { render: () => ( @@ -40,3 +48,20 @@ export const Default: Story = { ), } + +/** Controllable instance — edit args in the Controls panel. */ +export const Controllable: Story = { + args: { + activeStepId: 'submit', + maxHeight: 280, + }, + render: (args) => ( + + Transaction steps} + {...args} + /> + + ), +} diff --git a/examples/storybook/src/stories/helpers/citizenClaimWidgetStories.tsx b/examples/storybook/src/stories/helpers/citizenClaimWidgetStories.tsx index 2c96adbc..8d981ee5 100644 --- a/examples/storybook/src/stories/helpers/citizenClaimWidgetStories.tsx +++ b/examples/storybook/src/stories/helpers/citizenClaimWidgetStories.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react' import { YStack } from '@goodwidget/ui' -import { CitizenClaimWidget } from '@goodwidget/citizen-claim-widget' +import { CitizenClaimWidget, type CitizenClaimWidgetProps } from '@goodwidget/citizen-claim-widget' import { getInjectedEip1193Provider, isInjectedProviderUsable, @@ -10,11 +10,14 @@ import { createCustodialEip1193Provider } from '../../fixtures/custodialEip1193' function CitizenClaimWidgetStoryShell({ provider, dataTestId, + defaultTheme, + themeOverrides, }: { provider: unknown dataTestId: string + defaultTheme?: 'light' | 'dark' + themeOverrides?: CitizenClaimWidgetProps['themeOverrides'] }) { - // const [activeTab, setActiveTab] = useState('claim') const [activeChainId, setActiveChainId] = useState(null) useEffect(() => { @@ -46,11 +49,19 @@ function CitizenClaimWidgetStoryShell({ environment="development" data-testid={dataTestId} chainId={activeChainId ?? 42220} + defaultTheme={defaultTheme} + themeOverrides={themeOverrides} /> ) } -export function InjectedWalletStory() { +export function InjectedWalletStory({ + defaultTheme, + themeOverrides, +}: { + defaultTheme?: 'light' | 'dark' + themeOverrides?: CitizenClaimWidgetProps['themeOverrides'] +} = {}) { const injectedProvider = getInjectedEip1193Provider() const usableProvider = isInjectedProviderUsable(injectedProvider) @@ -70,6 +81,8 @@ export function InjectedWalletStory() { ) } diff --git a/examples/storybook/src/stories/helpers/claimWidgetStories.tsx b/examples/storybook/src/stories/helpers/claimWidgetStories.tsx index 8f92ab6d..ba692bf9 100644 --- a/examples/storybook/src/stories/helpers/claimWidgetStories.tsx +++ b/examples/storybook/src/stories/helpers/claimWidgetStories.tsx @@ -6,39 +6,7 @@ import { createMockEip1193Provider } from '../../fixtures/mockEip1193' export const mockProvider = createMockEip1193Provider() -export const cobaltOverrides = { - tokens: { - color: { - primary: '#2E5DE8', - primaryDark: '#1D3EB2', - primaryLight: '#6E8DFF', - }, - }, - themes: { - dark_ClaimCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, - dark_ClaimActionGlow: { primary: '#4F7DFF', primaryLight: '#9DB4FF' }, - dark_ClaimActionRing: { primary: '#2E5DE8', primaryLight: '#6E8DFF' }, - dark_ClaimActionInner: { backgroundDark: '#0E1A3A', backgroundDarkHover: '#172B60' }, - dark_TokenAmountText: { color: '#BBD0FF', secondaryColor: '#7FA2FF' }, - }, -} - -export const tealOverrides = { - tokens: { - color: { - primary: '#00A884', - primaryDark: '#007A61', - primaryLight: '#33C9AA', - }, - }, - themes: { - dark_ClaimCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, - dark_ClaimActionGlow: { primary: '#33C9AA', primaryLight: '#78E0CB' }, - dark_ClaimActionRing: { primary: '#00A884', primaryLight: '#33C9AA' }, - dark_ClaimActionInner: { backgroundDark: '#062A23', backgroundDarkHover: '#0B3B31' }, - dark_TokenAmountText: { color: '#BFF5E7', secondaryColor: '#66D5BB' }, - }, -} +export { cobaltOverrides, tealOverrides } from './themeOverridePresets' interface ClaimWidgetStoryCanvasProps { config?: React.ComponentProps['config'] diff --git a/examples/storybook/src/stories/helpers/stakingMigrationWidgetStories.tsx b/examples/storybook/src/stories/helpers/stakingMigrationWidgetStories.tsx index b8c000fb..d925fc89 100644 --- a/examples/storybook/src/stories/helpers/stakingMigrationWidgetStories.tsx +++ b/examples/storybook/src/stories/helpers/stakingMigrationWidgetStories.tsx @@ -7,6 +7,7 @@ import { derivePrimaryLabel, type MigrationStep, type StakingMigrationWidgetAdapterFactory, + type StakingMigrationWidgetProps, type StakingMigrationWidgetState, type StakingMigrationWidgetStatus, } from '@goodwidget/staking-migration-widget' @@ -99,7 +100,13 @@ function MockStoryShell({ } } -export function InjectedWalletStory() { +export function InjectedWalletStory({ + defaultTheme, + themeOverrides, +}: { + defaultTheme?: 'light' | 'dark' + themeOverrides?: StakingMigrationWidgetProps['themeOverrides'] +} = {}) { const injectedProvider = getInjectedEip1193Provider() const migrationApiBaseUrl = import.meta.env.VITE_MIGRATION_API_BASE_URL @@ -120,6 +127,8 @@ export function InjectedWalletStory() { {!migrationApiBaseUrl && ( diff --git a/examples/storybook/src/stories/helpers/streamingWidgetStories.tsx b/examples/storybook/src/stories/helpers/streamingWidgetStories.tsx index 21dfb3b1..e1761b33 100644 --- a/examples/storybook/src/stories/helpers/streamingWidgetStories.tsx +++ b/examples/storybook/src/stories/helpers/streamingWidgetStories.tsx @@ -234,11 +234,13 @@ function StreamingWidgetStoryShell({ dataTestId, apiKey, defaultTheme, + themeOverrides, }: { provider: unknown dataTestId: string apiKey?: string defaultTheme?: 'light' | 'dark' + themeOverrides?: StreamingWidgetProps['themeOverrides'] }) { const trimmedApiKey = apiKey?.trim() @@ -249,12 +251,17 @@ function StreamingWidgetStoryShell({ environment="production" apiKey={trimmedApiKey || undefined} defaultTheme={defaultTheme} + themeOverrides={themeOverrides} /> ) } -export function InjectedWalletStory({ apiKey }: Pick) { +export function InjectedWalletStory({ + apiKey, + defaultTheme, + themeOverrides, +}: Pick) { const injectedProvider = getInjectedEip1193Provider() const usableProvider = isInjectedProviderUsable(injectedProvider) @@ -272,6 +279,7 @@ export function InjectedWalletStory({ apiKey }: Pick ) } @@ -281,6 +289,8 @@ export function InjectedWalletStory({ apiKey }: Pick ) } diff --git a/examples/storybook/src/stories/helpers/themeOverridePresets.ts b/examples/storybook/src/stories/helpers/themeOverridePresets.ts new file mode 100644 index 00000000..5d7deef7 --- /dev/null +++ b/examples/storybook/src/stories/helpers/themeOverridePresets.ts @@ -0,0 +1,54 @@ +import type { GoodWidgetThemeOverrides } from '@goodwidget/ui' + +/** + * Shared "brand preset" overrides used to drive the `brandPreset` Storybook + * control across widget showcase stories, demonstrating the host override + * surface with a couple of concrete brand colors. + */ +export const cobaltOverrides: GoodWidgetThemeOverrides = { + tokens: { + color: { + primary: '#2E5DE8', + primaryDark: '#1D3EB2', + primaryLight: '#6E8DFF', + }, + }, + themes: { + dark_ClaimCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, + dark_ClaimActionGlow: { primary: '#4F7DFF', primaryLight: '#9DB4FF' }, + dark_ClaimActionRing: { primary: '#2E5DE8', primaryLight: '#6E8DFF' }, + dark_ClaimActionInner: { backgroundDark: '#0E1A3A', backgroundDarkHover: '#172B60' }, + dark_TokenAmountText: { color: '#BBD0FF', secondaryColor: '#7FA2FF' }, + }, +} + +export const tealOverrides: GoodWidgetThemeOverrides = { + tokens: { + color: { + primary: '#00A884', + primaryDark: '#007A61', + primaryLight: '#33C9AA', + }, + }, + themes: { + dark_ClaimCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, + dark_ClaimActionGlow: { primary: '#33C9AA', primaryLight: '#78E0CB' }, + dark_ClaimActionRing: { primary: '#00A884', primaryLight: '#33C9AA' }, + dark_ClaimActionInner: { backgroundDark: '#062A23', backgroundDarkHover: '#0B3B31' }, + dark_TokenAmountText: { color: '#BFF5E7', secondaryColor: '#66D5BB' }, + }, +} + +export const BRAND_PRESET_OPTIONS = ['None', 'Cobalt', 'Teal'] as const +export type BrandPreset = (typeof BRAND_PRESET_OPTIONS)[number] + +export function brandPresetOverrides(preset: BrandPreset | undefined): GoodWidgetThemeOverrides | undefined { + switch (preset) { + case 'Cobalt': + return cobaltOverrides + case 'Teal': + return tealOverrides + default: + return undefined + } +} diff --git a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetShowcase.stories.tsx b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetShowcase.stories.tsx index 236fb331..233753a7 100644 --- a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetShowcase.stories.tsx +++ b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetShowcase.stories.tsx @@ -1,16 +1,38 @@ import type { Meta, StoryObj } from '@storybook/react' import { StakingMigrationWidget } from '@goodwidget/staking-migration-widget' import { InjectedWalletStory } from '../helpers/stakingMigrationWidgetStories' +import { BRAND_PRESET_OPTIONS, brandPresetOverrides, type BrandPreset } from '../helpers/themeOverridePresets' -const meta: Meta = { +interface StakingMigrationWidgetStoryArgs { + defaultTheme: 'light' | 'dark' + brandPreset: BrandPreset +} + +const meta: Meta = { title: 'Widgets/StakingMigrationWidget/Showcase', component: StakingMigrationWidget, tags: ['integrator', 'manual', 'showcase'], parameters: { layout: 'padded' }, + argTypes: { + defaultTheme: { + control: 'radio', + options: ['dark', 'light'], + description: 'Base theme applied via the widget’s own defaultTheme prop.', + }, + brandPreset: { + control: 'select', + options: BRAND_PRESET_OPTIONS, + description: 'Sample host-branding themeOverrides preset.', + }, + }, + args: { + defaultTheme: 'dark', + brandPreset: 'None', + }, } export default meta -type Story = StoryObj +type Story = StoryObj export const InjectedWallet: Story = { parameters: { @@ -18,5 +40,7 @@ export const InjectedWallet: Story = { useShell: false, }, }, - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } diff --git a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx index f93bd512..5eb914d1 100644 --- a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx +++ b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx @@ -1,8 +1,15 @@ import type { Meta, StoryObj } from '@storybook/react' -import { StreamingWidget, type StreamingWidgetProps } from '@goodwidget/streaming-widget' +import { StreamingWidget } from '@goodwidget/streaming-widget' import { InjectedWalletStory } from '../helpers/streamingWidgetStories' +import { BRAND_PRESET_OPTIONS, brandPresetOverrides, type BrandPreset } from '../helpers/themeOverridePresets' -const meta: Meta = { +interface StreamingWidgetStoryArgs { + apiKey?: string + defaultTheme: 'light' | 'dark' + brandPreset: BrandPreset +} + +const meta: Meta = { title: 'Widgets/StreamingWidget/Showcase', component: StreamingWidget, tags: ['integrator', 'manual', 'showcase'], @@ -14,14 +21,26 @@ const meta: Meta = { description: 'Optional TheGraph key passed to the SDK-backed streaming adapter for Base SUP reserve queries.', }, + defaultTheme: { + control: 'radio', + options: ['dark', 'light'], + description: 'Base theme applied via the widget’s own defaultTheme prop.', + }, + brandPreset: { + control: 'select', + options: BRAND_PRESET_OPTIONS, + description: 'Sample host-branding themeOverrides preset.', + }, }, args: { apiKey: '', + defaultTheme: 'dark', + brandPreset: 'None', }, } export default meta -type Story = StoryObj +type Story = StoryObj export const InjectedWallet: Story = { parameters: { @@ -29,7 +48,11 @@ export const InjectedWallet: Story = { useShell: false, }, }, - render: ({ apiKey }: Pick) => ( - + render: ({ apiKey, defaultTheme, brandPreset }) => ( + ), } From 17b8e6571cf61177b556287146b7df86e74b6199 Mon Sep 17 00:00:00 2001 From: Thompson <140930314+Godbrand0@users.noreply.github.com> Date: Wed, 1 Jul 2026 05:41:51 -0400 Subject: [PATCH 02/12] docs(storybook): add integration snippets to widget MDX guides CitizenClaimWidget.mdx and StakingMigrationWidget.mdx were missing the "How to mount it" code snippet that ClaimWidget.mdx already has, so integrators had no copy-paste starting point for those two widgets. Adds the same Source block pattern to both, and adds a one-line note to all three widget docs pages (CitizenClaimWidget, StakingMigrationWidget, StreamingWidget) pointing at the new defaultTheme/themeOverrides Storybook controls added in this branch. --- .../CitizenClaimWidget.mdx | 28 +++++++++++++++++-- .../StakingMigrationWidget.mdx | 28 +++++++++++++++++-- .../streaming-widget/StreamingWidget.mdx | 2 +- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/examples/storybook/src/stories/citizen-claim-widget/CitizenClaimWidget.mdx b/examples/storybook/src/stories/citizen-claim-widget/CitizenClaimWidget.mdx index 177240ed..4df267aa 100644 --- a/examples/storybook/src/stories/citizen-claim-widget/CitizenClaimWidget.mdx +++ b/examples/storybook/src/stories/citizen-claim-widget/CitizenClaimWidget.mdx @@ -1,4 +1,4 @@ -import { Canvas, Meta } from '@storybook/blocks'; +import { Canvas, Meta, Source } from '@storybook/blocks'; import * as ShowcaseStories from './CitizenClaimWidgetShowcase.stories'; import { DocsCallout, DocsCard, DocsGrid, DocsPage, DocsSection } from '../docs/DocsLayout'; @@ -11,11 +11,35 @@ import { DocsCallout, DocsCard, DocsGrid, DocsPage, DocsSection } from '../docs/ > + + + + + ) +}`} + /> + + + + + + + ) +}`} + /> + + From 6cbf8df78a2f0d00f5a2435bde24c154607f8bce Mon Sep 17 00:00:00 2001 From: Thompson <140930314+Godbrand0@users.noreply.github.com> Date: Wed, 1 Jul 2026 05:42:07 -0400 Subject: [PATCH 03/12] fix(storybook): Stepper Controllable story ignored activeStepId control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found during manual visual verification of issue #64 (each control must visibly change what renders): the Stepper Controllable story passed a static STEPS array with hardcoded per-step `status` values, so moving the `activeStepId` control only auto-scrolled the list — the blue "active" highlight stayed pinned to whichever step had status:'active' in the array (always "Submit migration"), regardless of the selected control value. Now recomputes each step's status relative to the chosen activeStepId so the control drives a real state change. --- .../stories/design-system/Stepper.stories.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/examples/storybook/src/stories/design-system/Stepper.stories.tsx b/examples/storybook/src/stories/design-system/Stepper.stories.tsx index 51123a01..b236622b 100644 --- a/examples/storybook/src/stories/design-system/Stepper.stories.tsx +++ b/examples/storybook/src/stories/design-system/Stepper.stories.tsx @@ -49,18 +49,29 @@ export const Default: Story = { ), } +/** Recomputes each step's status relative to the chosen active step, so moving the + * `activeStepId` control actually restyles the list instead of only scrolling to it. */ +function stepsWithActive(activeStepId: string): StepperStepItem[] { + const activeIndex = STEPS.findIndex((step) => step.id === activeStepId) + return STEPS.map((step, index) => ({ + ...step, + status: index < activeIndex ? 'completed' : index === activeIndex ? 'active' : 'pending', + })) +} + /** Controllable instance — edit args in the Controls panel. */ export const Controllable: Story = { args: { activeStepId: 'submit', maxHeight: 280, }, - render: (args) => ( + render: ({ activeStepId, maxHeight }) => ( Transaction steps} - {...args} + maxHeight={maxHeight} /> ), From 1549925f221b03c32c017a9ce96f43d8027cba61 Mon Sep 17 00:00:00 2001 From: Thompson <140930314+Godbrand0@users.noreply.github.com> Date: Wed, 1 Jul 2026 06:32:06 -0400 Subject: [PATCH 04/12] fix(storybook): brandPreset control was a no-op for StreamingWidget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found during manual verification: StreamingWidget's card surfaces use their own named Tamagui theme keys (StreamRow, PoolRow, BalanceCard, EmptyStateCard, ErrorStateCard, SetStreamFormCard — see packages/streaming-widget/src/components/shared.tsx), none of which overlap with the Claim-family keys (ClaimCard, ClaimActionGlow, etc.) the shared cobalt/teal presets originally targeted. So toggling brandPreset on the StreamingWidget showcase story changed nothing. Adds matching border/shadow overrides for StreamingWidget's card names to both presets so the control has a real effect there too. StakingMigrationWidget already reuses the ClaimCard/StreakCard names (packages/staking-migration-widget/src/migrationWidgetComponents.ts), so it was already covered — it just isn't visible on the "no wallet" fallback screen, since that screen renders plain text with no themed Card at all. --- .../src/stories/helpers/themeOverridePresets.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/examples/storybook/src/stories/helpers/themeOverridePresets.ts b/examples/storybook/src/stories/helpers/themeOverridePresets.ts index 5d7deef7..2a43a089 100644 --- a/examples/storybook/src/stories/helpers/themeOverridePresets.ts +++ b/examples/storybook/src/stories/helpers/themeOverridePresets.ts @@ -19,6 +19,14 @@ export const cobaltOverrides: GoodWidgetThemeOverrides = { dark_ClaimActionRing: { primary: '#2E5DE8', primaryLight: '#6E8DFF' }, dark_ClaimActionInner: { backgroundDark: '#0E1A3A', backgroundDarkHover: '#172B60' }, dark_TokenAmountText: { color: '#BBD0FF', secondaryColor: '#7FA2FF' }, + // StreamingWidget card surfaces — not part of the Claim component family, so they + // need their own entries for the brand preset to visibly affect that widget too. + dark_StreamRow: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, + dark_PoolRow: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, + dark_BalanceCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, + dark_EmptyStateCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, + dark_ErrorStateCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, + dark_SetStreamFormCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, }, } @@ -36,6 +44,13 @@ export const tealOverrides: GoodWidgetThemeOverrides = { dark_ClaimActionRing: { primary: '#00A884', primaryLight: '#33C9AA' }, dark_ClaimActionInner: { backgroundDark: '#062A23', backgroundDarkHover: '#0B3B31' }, dark_TokenAmountText: { color: '#BFF5E7', secondaryColor: '#66D5BB' }, + // StreamingWidget card surfaces — see cobaltOverrides comment above. + dark_StreamRow: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, + dark_PoolRow: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, + dark_BalanceCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, + dark_EmptyStateCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, + dark_ErrorStateCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, + dark_SetStreamFormCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, }, } From 28547d273fc1b73d44ec9e7183c95ebb8b97d2da Mon Sep 17 00:00:00 2001 From: Thompson <140930314+Godbrand0@users.noreply.github.com> Date: Wed, 1 Jul 2026 07:05:54 -0400 Subject: [PATCH 05/12] docs(storybook): clarify Default stories don't respond to Controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Controls panel is auto-generated for every story of a component, so it appears (and looks interactive) on Default/WithAction-style stories too — but their render functions use fixed hardcoded props so those stories stay stable for other docs pages and Playwright screenshot tests. Only the "Controllable" story actually reads args. This was confusing during manual verification, so each Default story across Card/GlowCard/Stepper/Drawer now says so explicitly. --- examples/storybook/src/stories/design-system/Card.stories.tsx | 3 ++- .../storybook/src/stories/design-system/Drawer.stories.tsx | 3 ++- .../storybook/src/stories/design-system/GlowCard.stories.tsx | 3 ++- .../storybook/src/stories/design-system/Stepper.stories.tsx | 2 ++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/storybook/src/stories/design-system/Card.stories.tsx b/examples/storybook/src/stories/design-system/Card.stories.tsx index 9f88e098..7f3d01ea 100644 --- a/examples/storybook/src/stories/design-system/Card.stories.tsx +++ b/examples/storybook/src/stories/design-system/Card.stories.tsx @@ -25,7 +25,8 @@ const meta: Meta = { export default meta type Story = StoryObj -/** Default Card using base theme values. */ +/** Default Card using base theme values. Fixed reference story — the Controls panel is + * inert here; use "Controllable" below to drive props live. */ export const Default: Story = { render: () => ( diff --git a/examples/storybook/src/stories/design-system/Drawer.stories.tsx b/examples/storybook/src/stories/design-system/Drawer.stories.tsx index 008ad776..3cca9385 100644 --- a/examples/storybook/src/stories/design-system/Drawer.stories.tsx +++ b/examples/storybook/src/stories/design-system/Drawer.stories.tsx @@ -26,7 +26,8 @@ const meta: Meta = { export default meta type Story = StoryObj -/** Controlled Drawer triggered by a button. */ +/** Controlled Drawer triggered by a button. Fixed reference story (has an interaction + * test) — the Controls panel is inert here; use "Controllable" below to drive props live. */ export const Default: Story = { render: () => { const [open, setOpen] = useState(false) diff --git a/examples/storybook/src/stories/design-system/GlowCard.stories.tsx b/examples/storybook/src/stories/design-system/GlowCard.stories.tsx index dc8f33a1..af687424 100644 --- a/examples/storybook/src/stories/design-system/GlowCard.stories.tsx +++ b/examples/storybook/src/stories/design-system/GlowCard.stories.tsx @@ -23,7 +23,8 @@ const meta: Meta = { export default meta type Story = StoryObj -/** Default GlowCard with theme-driven glow colour. */ +/** Default GlowCard with theme-driven glow colour. Fixed reference story — the Controls + * panel is inert here; use "Controllable" below to drive props live. */ export const Default: Story = { render: () => ( diff --git a/examples/storybook/src/stories/design-system/Stepper.stories.tsx b/examples/storybook/src/stories/design-system/Stepper.stories.tsx index b236622b..a51128a0 100644 --- a/examples/storybook/src/stories/design-system/Stepper.stories.tsx +++ b/examples/storybook/src/stories/design-system/Stepper.stories.tsx @@ -36,6 +36,8 @@ const meta: Meta = { export default meta type Story = StoryObj +/** Fixed reference story — the Controls panel is inert here; use "Controllable" below to + * drive props live. */ export const Default: Story = { render: () => ( From 7d006429f41293f42ef1d1b0c71b636a7fb5216a Mon Sep 17 00:00:00 2001 From: Thompson <140930314+Godbrand0@users.noreply.github.com> Date: Wed, 1 Jul 2026 07:35:51 -0400 Subject: [PATCH 06/12] fix(storybook): themeOverrides dropped on StreamingWidget's no-wallet fallback Found during manual verification: InjectedWalletStory threaded themeOverrides through to StreamingWidgetStoryShell (the "wallet connected" path) but not to PreviewStoryShell (the "no wallet" fallback path, which most reviewers hit since they don't have a matching injected wallet). So brandPreset silently did nothing on the common case of that story. PreviewStoryShell and its underlying StreamingWidgetPreview call now also receive themeOverrides. --- .../storybook/src/stories/helpers/streamingWidgetStories.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/storybook/src/stories/helpers/streamingWidgetStories.tsx b/examples/storybook/src/stories/helpers/streamingWidgetStories.tsx index e1761b33..f21c33f3 100644 --- a/examples/storybook/src/stories/helpers/streamingWidgetStories.tsx +++ b/examples/storybook/src/stories/helpers/streamingWidgetStories.tsx @@ -210,12 +210,14 @@ function PreviewStoryShell({ initialTab = 'streams', initialStreamsFormOpen = false, defaultTheme, + themeOverrides, }: { adapter: StreamingWidgetAdapterResult dataTestId: string initialTab?: StreamingWidgetTab initialStreamsFormOpen?: boolean defaultTheme?: 'light' | 'dark' + themeOverrides?: StreamingWidgetProps['themeOverrides'] }) { return ( @@ -224,6 +226,7 @@ function PreviewStoryShell({ initialTab={initialTab} initialStreamsFormOpen={initialStreamsFormOpen} defaultTheme={defaultTheme} + themeOverrides={themeOverrides} /> ) @@ -280,6 +283,7 @@ export function InjectedWalletStory({ })} dataTestId="StreamingWidget-no-injected-wallet" defaultTheme={defaultTheme} + themeOverrides={themeOverrides} /> ) } From 73cb7e2b9fec9371fa3138ff3d43ca02170f1735 Mon Sep 17 00:00:00 2001 From: Thompson <140930314+Godbrand0@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:43:47 -0400 Subject: [PATCH 07/12] feat(storybook): extend defaultTheme/brandPreset controls to QA fixture stories Per issue #64's literal DoD ("all stories are accompanied with controls"), the QA runtime-fixture stories previously had zero controls. Threads defaultTheme/themeOverrides through every QA helper function across all three widgets (20 StreamingWidget states, 1 CitizenClaimWidget state, 8 StakingMigrationWidget states) and wires matching argTypes into each QA story file, following the same pattern already used on the Showcase stories. QA determinism for Playwright is unaffected: automated tests navigate directly to story IDs without touching the Controls panel, so the fixed default args (dark theme, no brand preset) still apply during test runs. A human reviewer gets the same live override on top when browsing QA stories manually. LightThemePopulated/LightThemeReady keep their hardcoded defaultTheme="light" mount (that's the point of those specific stories) but now also accept the brandPreset control on top. --- .../CitizenClaimWidgetQA.stories.tsx | 30 ++++- .../helpers/citizenClaimWidgetStories.tsx | 10 +- .../helpers/stakingMigrationWidgetStories.tsx | 47 +++++-- .../helpers/streamingWidgetStories.tsx | 97 +++++++++++--- .../StakingMigrationWidgetQA.stories.tsx | 58 ++++++-- .../StreamingWidgetQA.stories.tsx | 125 ++++++++++++++---- 6 files changed, 298 insertions(+), 69 deletions(-) diff --git a/examples/storybook/src/stories/citizen-claim-widget/CitizenClaimWidgetQA.stories.tsx b/examples/storybook/src/stories/citizen-claim-widget/CitizenClaimWidgetQA.stories.tsx index a06642d0..b0cea408 100644 --- a/examples/storybook/src/stories/citizen-claim-widget/CitizenClaimWidgetQA.stories.tsx +++ b/examples/storybook/src/stories/citizen-claim-widget/CitizenClaimWidgetQA.stories.tsx @@ -1,17 +1,41 @@ import type { Meta, StoryObj } from '@storybook/react' import { CitizenClaimWidget } from '@goodwidget/citizen-claim-widget' import { CustodialLocalFixtureStory } from '../helpers/citizenClaimWidgetStories' +import { BRAND_PRESET_OPTIONS, brandPresetOverrides, type BrandPreset } from '../helpers/themeOverridePresets' -const meta: Meta = { +interface CitizenClaimWidgetQAArgs { + defaultTheme: 'light' | 'dark' + brandPreset: BrandPreset +} + +const meta: Meta = { title: 'QA/CitizenClaimWidget/Runtime Fixtures', component: CitizenClaimWidget, tags: ['autodocs', 'qa'], parameters: { layout: 'padded' }, + argTypes: { + defaultTheme: { + control: 'radio', + options: ['dark', 'light'], + description: 'Base theme applied via the widget’s own defaultTheme prop.', + }, + brandPreset: { + control: 'select', + options: BRAND_PRESET_OPTIONS, + description: 'Sample host-branding themeOverrides preset.', + }, + }, + args: { + defaultTheme: 'dark', + brandPreset: 'None', + }, } export default meta -type Story = StoryObj +type Story = StoryObj export const CustodialLocalFixture: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } diff --git a/examples/storybook/src/stories/helpers/citizenClaimWidgetStories.tsx b/examples/storybook/src/stories/helpers/citizenClaimWidgetStories.tsx index 8d981ee5..1cbf9576 100644 --- a/examples/storybook/src/stories/helpers/citizenClaimWidgetStories.tsx +++ b/examples/storybook/src/stories/helpers/citizenClaimWidgetStories.tsx @@ -87,13 +87,21 @@ export function InjectedWalletStory({ ) } -export function CustodialLocalFixtureStory() { +export function CustodialLocalFixtureStory({ + defaultTheme, + themeOverrides, +}: { + defaultTheme?: 'light' | 'dark' + themeOverrides?: CitizenClaimWidgetProps['themeOverrides'] +} = {}) { try { const provider = createCustodialEip1193Provider() return ( ) } catch (error: unknown) { diff --git a/examples/storybook/src/stories/helpers/stakingMigrationWidgetStories.tsx b/examples/storybook/src/stories/helpers/stakingMigrationWidgetStories.tsx index d925fc89..dd73ad1f 100644 --- a/examples/storybook/src/stories/helpers/stakingMigrationWidgetStories.tsx +++ b/examples/storybook/src/stories/helpers/stakingMigrationWidgetStories.tsx @@ -74,18 +74,30 @@ function createAdapterFactory( }) } +type ThemeArgs = { + defaultTheme?: 'light' | 'dark' + themeOverrides?: StakingMigrationWidgetProps['themeOverrides'] +} + function MockStoryShell({ adapterFactory, dataTestId, + defaultTheme, + themeOverrides, }: { adapterFactory: StakingMigrationWidgetAdapterFactory dataTestId: string -}) { +} & ThemeArgs) { try { const provider = createCustodialEip1193Provider() return ( - + ) } catch (error: unknown) { @@ -142,7 +154,7 @@ export function InjectedWalletStory({ ) } -export function EmptyBalanceStory() { +export function EmptyBalanceStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function ReadyStory() { +export function ReadyStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function WrongNetworkStory() { +export function WrongNetworkStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function ApprovalPendingStory() { +export function ApprovalPendingStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function MigratingStory() { +export function MigratingStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function SuccessStory() { +export function SuccessStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function ErrorStateStory() { +export function ErrorStateStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function LightThemeReadyStory() { +export function LightThemeReadyStory({ themeOverrides }: Pick = {}) { return ( @@ -228,6 +254,7 @@ export function LightThemeReadyStory() { diff --git a/examples/storybook/src/stories/helpers/streamingWidgetStories.tsx b/examples/storybook/src/stories/helpers/streamingWidgetStories.tsx index f21c33f3..af80c7f4 100644 --- a/examples/storybook/src/stories/helpers/streamingWidgetStories.tsx +++ b/examples/storybook/src/stories/helpers/streamingWidgetStories.tsx @@ -299,7 +299,11 @@ export function InjectedWalletStory({ ) } -export function CustodialLocalFixtureStory({ apiKey }: Pick) { +export function CustodialLocalFixtureStory({ + apiKey, + defaultTheme, + themeOverrides, +}: Pick) { try { const provider = createCustodialEip1193Provider() return ( @@ -307,6 +311,8 @@ export function CustodialLocalFixtureStory({ apiKey }: Pick ) } catch (error: unknown) { @@ -326,7 +332,12 @@ export function CustodialLocalFixtureStory({ apiKey }: Pick ) } -export function WrongChainStory() { +export function WrongChainStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function LoadingStateStory() { +export function LoadingStateStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function EmptyStateStory() { +export function EmptyStateStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function ErrorStateStory() { +export function ErrorStateStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function PopulatedStateStory() { +export function PopulatedStateStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( - + ) } -export function LightThemePopulatedStory() { +export function LightThemePopulatedStory({ themeOverrides }: Pick = {}) { return ( ) } -export function CreateUpdateFormStory() { +export function CreateUpdateFormStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { const [form, setForm] = React.useState(validForm) return ( @@ -446,21 +473,25 @@ export function CreateUpdateFormStory() { )} dataTestId="StreamingWidget-create-update-form" initialStreamsFormOpen + defaultTheme={defaultTheme} + themeOverrides={themeOverrides} /> ) } -export function CreateUpdateInvalidInputStory() { +export function CreateUpdateInvalidInputStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function CreateUpdatePendingStory() { +export function CreateUpdatePendingStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function CreateUpdateSuccessStory() { +export function CreateUpdateSuccessStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function CreateUpdateFailureStory() { +export function CreateUpdateFailureStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function PoolClaimStateStory() { +export function PoolClaimStateStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function PoolConnectedStateStory() { +export function PoolConnectedStateStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function PoolClaimPendingStory() { +export function PoolClaimPendingStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function PoolClaimSuccessStory() { +export function PoolClaimSuccessStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function PoolClaimErrorStory() { +export function PoolClaimErrorStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function PoolClaimableAmountErrorStory() { +export function PoolClaimableAmountErrorStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { const [retrying, setRetrying] = React.useState(false) return ( @@ -592,11 +639,13 @@ export function PoolClaimableAmountErrorStory() { )} dataTestId="StreamingWidget-pool-claimable-amount-error" initialTab="pools" + defaultTheme={defaultTheme} + themeOverrides={themeOverrides} /> ) } -export function BaseSupBalanceAndReserveStory() { +export function BaseSupBalanceAndReserveStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } -export function NonBaseSupReserveDisabledStory() { +export function NonBaseSupReserveDisabledStory({ defaultTheme, themeOverrides }: ThemeArgs = {}) { return ( ) } diff --git a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetQA.stories.tsx b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetQA.stories.tsx index f23de5fe..096a0107 100644 --- a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetQA.stories.tsx +++ b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetQA.stories.tsx @@ -10,50 +10,88 @@ import { SuccessStory, WrongNetworkStory, } from '../helpers/stakingMigrationWidgetStories' +import { BRAND_PRESET_OPTIONS, brandPresetOverrides, type BrandPreset } from '../helpers/themeOverridePresets' -const meta: Meta = { +interface StakingMigrationWidgetQAArgs { + defaultTheme: 'light' | 'dark' + brandPreset: BrandPreset +} + +const meta: Meta = { title: 'QA/StakingMigrationWidget/Runtime Fixtures', component: StakingMigrationWidget, tags: ['autodocs', 'qa'], parameters: { layout: 'padded' }, + argTypes: { + defaultTheme: { + control: 'radio', + options: ['dark', 'light'], + description: 'Base theme applied via the widget’s own defaultTheme prop.', + }, + brandPreset: { + control: 'select', + options: BRAND_PRESET_OPTIONS, + description: 'Sample host-branding themeOverrides preset.', + }, + }, + args: { + defaultTheme: 'dark', + brandPreset: 'None', + }, } export default meta -type Story = StoryObj +type Story = StoryObj export const EmptyBalance: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const Ready: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const WrongNetwork: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const ApprovalPending: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const Migrating: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const Success: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const ErrorState: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } +/** Always mounts with defaultTheme="light" regardless of the control, to demonstrate the + * explicit light-theme branch; brandPreset still applies on top. */ export const LightThemeReady: Story = { parameters: { goodWidgetProvider: { useShell: false, }, }, - render: () => , + render: ({ brandPreset }) => , } diff --git a/examples/storybook/src/stories/streaming-widget/StreamingWidgetQA.stories.tsx b/examples/storybook/src/stories/streaming-widget/StreamingWidgetQA.stories.tsx index 9b6f46ac..fcb85d81 100644 --- a/examples/storybook/src/stories/streaming-widget/StreamingWidgetQA.stories.tsx +++ b/examples/storybook/src/stories/streaming-widget/StreamingWidgetQA.stories.tsx @@ -23,8 +23,15 @@ import { PopulatedStateStory, WrongChainStory, } from '../helpers/streamingWidgetStories' +import { BRAND_PRESET_OPTIONS, brandPresetOverrides, type BrandPreset } from '../helpers/themeOverridePresets' -const meta: Meta = { +interface StreamingWidgetQAArgs { + apiKey?: string + defaultTheme: 'light' | 'dark' + brandPreset: BrandPreset +} + +const meta: Meta = { title: 'QA/StreamingWidget/Runtime Fixtures', component: StreamingWidget, tags: ['autodocs', 'qa'], @@ -41,95 +48,167 @@ const meta: Meta = { description: 'Optional TheGraph key passed to the SDK-backed streaming adapter for Base SUP reserve queries.', }, + defaultTheme: { + control: 'radio', + options: ['dark', 'light'], + description: 'Base theme applied via the widget’s own defaultTheme prop.', + }, + brandPreset: { + control: 'select', + options: BRAND_PRESET_OPTIONS, + description: 'Sample host-branding themeOverrides preset.', + }, }, args: { apiKey: '', + defaultTheme: 'dark', + brandPreset: 'None', }, } export default meta -type Story = StoryObj +type Story = StoryObj export const CustodialLocalFixture: Story = { - render: ({ apiKey }) => , + render: ({ apiKey, defaultTheme, brandPreset }) => ( + + ), } export const NoWallet: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const WrongChain: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const LoadingState: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const EmptyState: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const ErrorState: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const PopulatedState: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } +/** Always mounts with defaultTheme="light" regardless of the control, to demonstrate the + * explicit light-theme branch; brandPreset still applies on top. */ export const LightThemePopulated: Story = { - render: () => , + render: ({ brandPreset }) => ( + + ), } export const CreateUpdateForm: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const CreateUpdateInvalidInput: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const CreateUpdatePending: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const CreateUpdateSuccess: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const CreateUpdateFailure: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const PoolClaimState: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const PoolConnectedState: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const PoolClaimPending: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const PoolClaimSuccess: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const PoolClaimError: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const PoolClaimableAmountError: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const BaseSupBalanceAndReserve: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } export const NonBaseSupReserveDisabled: Story = { - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } From d8122857b2329e83310c962ee93c56ca709f4f7a Mon Sep 17 00:00:00 2001 From: Thompson <140930314+Godbrand0@users.noreply.github.com> Date: Thu, 9 Jul 2026 09:41:53 -0400 Subject: [PATCH 08/12] fix(storybook): add light_* brand preset overrides, fix ClaimActionGlow key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses PR #73 review comment: every brandPreset override was dark_* only, so selecting defaultTheme: light left only the top-level tokens.color.primary applied globally, while every component-specific border/shadow/glow override silently did nothing — the preset looked broken in light mode. Adds a light_* counterpart for every dark_* entry, using field values appropriate for light mode (much lower shadow opacity, matching the base design preset's light_Card/light_Toast/etc precedent in packages/ui/src/presets.ts; darker/more saturated text color for contrast on a light background instead of the pastel dark-mode shade). ClaimActionInner stays dark-only, matching the base preset's own precedent of never defining a light variant for it. Also fixes dark_ClaimActionGlow, which used the wrong field name (primary instead of backgroundColor) — CircularActionButton.tsx reads $backgroundColor for the glow's resting fill and $primaryLight only for its hover state, so the old key silently did nothing. --- .../stories/helpers/themeOverridePresets.ts | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/examples/storybook/src/stories/helpers/themeOverridePresets.ts b/examples/storybook/src/stories/helpers/themeOverridePresets.ts index 2a43a089..47feb319 100644 --- a/examples/storybook/src/stories/helpers/themeOverridePresets.ts +++ b/examples/storybook/src/stories/helpers/themeOverridePresets.ts @@ -4,6 +4,16 @@ import type { GoodWidgetThemeOverrides } from '@goodwidget/ui' * Shared "brand preset" overrides used to drive the `brandPreset` Storybook * control across widget showcase stories, demonstrating the host override * surface with a couple of concrete brand colors. + * + * Each named component theme gets both a `dark_*` and `light_*` entry so the + * preset stays visible regardless of the `defaultTheme` control — dark and + * light use different field values (light shadows are much lower-opacity, + * light text is a saturated/dark shade of the brand color for contrast on a + * light background) but target the same components. + * + * `ClaimActionInner` is intentionally dark-only: the base design preset + * (packages/ui/src/presets.ts) never defines a light variant for it either, + * so there's no light contrast baseline to diverge from here. */ export const cobaltOverrides: GoodWidgetThemeOverrides = { tokens: { @@ -15,18 +25,28 @@ export const cobaltOverrides: GoodWidgetThemeOverrides = { }, themes: { dark_ClaimCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, - dark_ClaimActionGlow: { primary: '#4F7DFF', primaryLight: '#9DB4FF' }, + light_ClaimCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.12)' }, + dark_ClaimActionGlow: { backgroundColor: '#4F7DFF', primaryLight: '#9DB4FF' }, + light_ClaimActionGlow: { backgroundColor: '#2E5DE8', glowOpacity: '0.1' }, dark_ClaimActionRing: { primary: '#2E5DE8', primaryLight: '#6E8DFF' }, + light_ClaimActionRing: { primary: '#2E5DE8', primaryLight: '#6E8DFF' }, dark_ClaimActionInner: { backgroundDark: '#0E1A3A', backgroundDarkHover: '#172B60' }, dark_TokenAmountText: { color: '#BBD0FF', secondaryColor: '#7FA2FF' }, + light_TokenAmountText: { color: '#1D3EB2', secondaryColor: '#3C5FC7' }, // StreamingWidget card surfaces — not part of the Claim component family, so they // need their own entries for the brand preset to visibly affect that widget too. dark_StreamRow: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, + light_StreamRow: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.12)' }, dark_PoolRow: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, + light_PoolRow: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.12)' }, dark_BalanceCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, + light_BalanceCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.12)' }, dark_EmptyStateCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, + light_EmptyStateCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.12)' }, dark_ErrorStateCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, + light_ErrorStateCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.12)' }, dark_SetStreamFormCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, + light_SetStreamFormCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.12)' }, }, } @@ -40,17 +60,27 @@ export const tealOverrides: GoodWidgetThemeOverrides = { }, themes: { dark_ClaimCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, - dark_ClaimActionGlow: { primary: '#33C9AA', primaryLight: '#78E0CB' }, + light_ClaimCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.12)' }, + dark_ClaimActionGlow: { backgroundColor: '#33C9AA', primaryLight: '#78E0CB' }, + light_ClaimActionGlow: { backgroundColor: '#00A884', glowOpacity: '0.1' }, dark_ClaimActionRing: { primary: '#00A884', primaryLight: '#33C9AA' }, + light_ClaimActionRing: { primary: '#00A884', primaryLight: '#33C9AA' }, dark_ClaimActionInner: { backgroundDark: '#062A23', backgroundDarkHover: '#0B3B31' }, dark_TokenAmountText: { color: '#BFF5E7', secondaryColor: '#66D5BB' }, + light_TokenAmountText: { color: '#007A61', secondaryColor: '#1F9C82' }, // StreamingWidget card surfaces — see cobaltOverrides comment above. dark_StreamRow: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, + light_StreamRow: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.12)' }, dark_PoolRow: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, + light_PoolRow: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.12)' }, dark_BalanceCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, + light_BalanceCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.12)' }, dark_EmptyStateCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, + light_EmptyStateCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.12)' }, dark_ErrorStateCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, + light_ErrorStateCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.12)' }, dark_SetStreamFormCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, + light_SetStreamFormCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.12)' }, }, } From bc797f4b83f7a1cb9146370c671fbf267f75baa0 Mon Sep 17 00:00:00 2001 From: Thompson <140930314+Godbrand0@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:21:47 -0400 Subject: [PATCH 09/12] fix(storybook): remove dead goodWidgetProvider parameter blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses PR #73 review comment: main removed the global GoodWidgetProvider/ MiniAppShell decorator from .storybook/preview.tsx entirely (every widget already wraps its own provider internally, so the outer one was redundant). That decorator was the only thing reading context.parameters.goodWidgetProvider, so every remaining goodWidgetProvider parameter block in the touched story files is now dead configuration with nothing left to consume it. Removes it from the five files this PR touches (ClaimWidget, StreamingWidget Showcase/QA, StakingMigrationWidget Showcase/QA). This also resolves the specific nit about ClaimWidget.stories.tsx: that block was placed at the meta top level instead of inside `parameters` and was already a no-op before this rebase — deleting it (rather than moving it) is correct now since there's no decorator left to read it either way. --- .../src/stories/claim-widget/ClaimWidget.stories.tsx | 4 ---- .../StakingMigrationWidgetQA.stories.tsx | 5 ----- .../StakingMigrationWidgetShowcase.stories.tsx | 5 ----- .../stories/streaming-widget/StreamingWidget.stories.tsx | 5 ----- .../stories/streaming-widget/StreamingWidgetQA.stories.tsx | 7 +------ 5 files changed, 1 insertion(+), 25 deletions(-) diff --git a/examples/storybook/src/stories/claim-widget/ClaimWidget.stories.tsx b/examples/storybook/src/stories/claim-widget/ClaimWidget.stories.tsx index bd619740..4a591870 100644 --- a/examples/storybook/src/stories/claim-widget/ClaimWidget.stories.tsx +++ b/examples/storybook/src/stories/claim-widget/ClaimWidget.stories.tsx @@ -20,10 +20,6 @@ const meta: Meta = { component: ClaimWidget, tags: ['integrator', 'showcase'], parameters: { layout: 'padded' }, - goodWidgetProvider: { - disableProvider: true, - useShell: false, - }, argTypes: { defaultTheme: { control: 'radio', diff --git a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetQA.stories.tsx b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetQA.stories.tsx index 096a0107..fa8fb3db 100644 --- a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetQA.stories.tsx +++ b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetQA.stories.tsx @@ -88,10 +88,5 @@ export const ErrorState: Story = { /** Always mounts with defaultTheme="light" regardless of the control, to demonstrate the * explicit light-theme branch; brandPreset still applies on top. */ export const LightThemeReady: Story = { - parameters: { - goodWidgetProvider: { - useShell: false, - }, - }, render: ({ brandPreset }) => , } diff --git a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetShowcase.stories.tsx b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetShowcase.stories.tsx index 233753a7..fc1852b2 100644 --- a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetShowcase.stories.tsx +++ b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetShowcase.stories.tsx @@ -35,11 +35,6 @@ export default meta type Story = StoryObj export const InjectedWallet: Story = { - parameters: { - goodWidgetProvider: { - useShell: false, - }, - }, render: ({ defaultTheme, brandPreset }) => ( ), diff --git a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx index 5eb914d1..129fa794 100644 --- a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx +++ b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx @@ -43,11 +43,6 @@ export default meta type Story = StoryObj export const InjectedWallet: Story = { - parameters: { - goodWidgetProvider: { - useShell: false, - }, - }, render: ({ apiKey, defaultTheme, brandPreset }) => ( = { title: 'QA/StreamingWidget/Runtime Fixtures', component: StreamingWidget, tags: ['autodocs', 'qa'], - parameters: { - layout: 'padded', - goodWidgetProvider: { - useShell: false, - }, - }, + parameters: { layout: 'padded' }, argTypes: { apiKey: { control: 'text', From a2bb33ca03f62ffb0164d848fd792fc2ce900266 Mon Sep 17 00:00:00 2001 From: Thompson <140930314+Godbrand0@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:46:19 -0400 Subject: [PATCH 10/12] feat(storybook): add StreamingWidget advanced-overrides playground, fix brand preset Button/glow gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses L03TJ3's review on PR #73: the existing brandPreset control (None/ Cobalt/Teal) only demonstrated picking from two curated palettes across a handful of hardcoded card targets, not "almost all UI aspects" being overridable as issue #64 asks for. - New Widgets/StreamingWidget/Advanced Overrides story: live color-picker controls (always visible, no collapsed JSON tree to expand) mapped to real themeOverrides.themes fields (dark_Button + dark_StreamRow/dark_BalanceCard from the rendered fixture), with a code snippet generated from the live arg values so it can never drift from what's rendered. Reference list for the remaining Card-derived targets (PoolRow, EmptyStateCard, ErrorStateCard, SetStreamFormCard) that share the same field shape. - Fixed dark_ClaimActionGlow using stale primary/primaryLight fields in ThemePlayground.stories.tsx (the component reads $backgroundColor, not $primary — same bug class already fixed in themeOverridePresets.ts). - Fixed cobaltOverrides/tealOverrides never touching dark_Button/light_Button: manually verified the InjectedWallet story's wallet-not-connected fallback (WalletGate.tsx) renders an EmptyStateCard + a Button-themed "Connect Wallet" action, so previously only a subtle card border tint changed between presets while the one loud, obviously-colored element never did. This also fixes the same gap for ClaimWidget/StakingMigrationWidget, which share the base Button theme. One widget scoped first (per plan) to validate the pattern before replicating to ClaimWidget and StakingMigrationWidget. --- .../design-system/ThemePlayground.stories.tsx | 8 +- .../stories/helpers/themeOverridePresets.ts | 44 ++++++ .../StreamingWidgetOverrides.stories.tsx | 129 ++++++++++++++++++ 3 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 examples/storybook/src/stories/streaming-widget/StreamingWidgetOverrides.stories.tsx diff --git a/examples/storybook/src/stories/design-system/ThemePlayground.stories.tsx b/examples/storybook/src/stories/design-system/ThemePlayground.stories.tsx index f18970ee..744971a3 100644 --- a/examples/storybook/src/stories/design-system/ThemePlayground.stories.tsx +++ b/examples/storybook/src/stories/design-system/ThemePlayground.stories.tsx @@ -112,7 +112,7 @@ export const ComponentThemeOverride: Story = { {` + +const CARD_FIELDS = ['background', 'borderColor', 'shadowColor'] + +const REFERENCE_ONLY_TARGETS: Array<{ name: string; fields: string[] }> = [ + { name: 'PoolRow', fields: CARD_FIELDS }, + { name: 'EmptyStateCard', fields: CARD_FIELDS }, + { name: 'ErrorStateCard', fields: CARD_FIELDS }, + { name: 'SetStreamFormCard', fields: CARD_FIELDS }, +] + +interface OverridesArgs { + buttonBackground: string + buttonBackgroundHover: string + buttonBackgroundPress: string + buttonBackgroundFocus: string + buttonColor: string + streamRowBorderColor: string + balanceCardBorderColor: string + balanceCardShadowColor: string +} + +function buildThemeOverrides(args: OverridesArgs): ThemeOverrides { + return { + themes: { + dark_Button: { + background: args.buttonBackground, + backgroundHover: args.buttonBackgroundHover, + backgroundPress: args.buttonBackgroundPress, + backgroundFocus: args.buttonBackgroundFocus, + color: args.buttonColor, + }, + dark_StreamRow: { + borderColor: args.streamRowBorderColor, + }, + dark_BalanceCard: { + borderColor: args.balanceCardBorderColor, + shadowColor: args.balanceCardShadowColor, + }, + }, + } +} + +const meta: Meta = { + title: 'Widgets/StreamingWidget/Advanced Overrides', + tags: ['integrator', 'showcase'], + parameters: { layout: 'padded' }, + decorators: [withDefaultPreset], + argTypes: { + buttonBackground: { control: 'color', description: 'themes.dark_Button.background' }, + buttonBackgroundHover: { control: 'color', description: 'themes.dark_Button.backgroundHover' }, + buttonBackgroundPress: { control: 'color', description: 'themes.dark_Button.backgroundPress' }, + buttonBackgroundFocus: { control: 'color', description: 'themes.dark_Button.backgroundFocus' }, + buttonColor: { control: 'color', description: 'themes.dark_Button.color' }, + streamRowBorderColor: { control: 'color', description: 'themes.dark_StreamRow.borderColor' }, + balanceCardBorderColor: { control: 'color', description: 'themes.dark_BalanceCard.borderColor' }, + balanceCardShadowColor: { control: 'color', description: 'themes.dark_BalanceCard.shadowColor' }, + }, + args: { + buttonBackground: '#7C3AED', + buttonBackgroundHover: '#6D28D9', + buttonBackgroundPress: '#5B21B6', + buttonBackgroundFocus: '#6D28D9', + buttonColor: '#FFFFFF', + streamRowBorderColor: '#7C3AED', + balanceCardBorderColor: '#7C3AED', + balanceCardShadowColor: 'rgba(124, 58, 237, 0.6)', + }, +} +export default meta +type Story = StoryObj + +export const Playground: Story = { + render: (args) => { + const themeOverrides = buildThemeOverrides(args) + return ( + + + Other Card-derived targets (same field shape) + + {REFERENCE_ONLY_TARGETS.map((target) => ( + + + dark_{target.name} / light_{target.name} + + {': '} + {target.fields.join(', ')} + + ))} + + + + How it works + + {``} + + + + + ) + }, +} From b296507b195fd1ce8f1c94821f9ed36b9bb5f4f5 Mon Sep 17 00:00:00 2001 From: Thompson <140930314+Godbrand0@users.noreply.github.com> Date: Thu, 16 Jul 2026 18:19:48 -0400 Subject: [PATCH 11/12] feat(storybook): replicate theme-overrides playground to ClaimWidget and StakingMigrationWidget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to L03TJ3's sign-off on the StreamingWidget pattern ("I think its the right direction yes"), replicating it to the other two widgets and applying their requested refinements across all three: - New Widgets/ClaimWidget Theme Demo/Theme overrides and Widgets/ StakingMigrationWidget/Theme overrides playgrounds, same shape as StreamingWidget's: flat always-visible color controls for a couple of high-impact targets per widget, a live code snippet, and a reference callout listing every overridable path (controlled or not). - ClaimWidget: dark_Button, dark_ClaimActionGlow, dark_ClaimActionRing, dark_ClaimCard (it defines its own local ClaimActionGlow/Ring/Inner, same shape as the shared CircularActionButton). - StakingMigrationWidget: dark_ClaimCard, dark_ClaimActionGlow, dark_ClaimActionRing (no dark_Button control — this widget has no plain Button usage, only the shared circular action button). - Renamed "Advanced Overrides" -> "Theme overrides" everywhere per feedback. - preview.tsx: parameters.controls.disableSaveFromUI = true globally, so editing any control (including the existing brandPreset/defaultTheme ones) no longer shows Storybook's "Update story / Create new story" save bar. - Each widget's .mdx docs page gets a new "Theme overrides" DocsSection embedding the Playground story via Canvas, using the intro copy L03TJ3 suggested. The header/description lives only in the docs page (not duplicated inside the story itself) to avoid the doubled heading that showed up when both had it. - Centered the code snippet + reference callout (maxWidth 560, margin auto) so they align with the widget preview below them instead of stretching full-bleed while the widget stays narrow and centered. --- .../ClaimWidgetThemeOverrides.stories.tsx | 164 ++++++++++++++++++ ...gMigrationWidgetThemeOverrides.stories.tsx | 139 +++++++++++++++ ...StreamingWidgetThemeOverrides.stories.tsx} | 92 +++++++--- 3 files changed, 370 insertions(+), 25 deletions(-) create mode 100644 examples/storybook/src/stories/claim-widget/ClaimWidgetThemeOverrides.stories.tsx create mode 100644 examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetThemeOverrides.stories.tsx rename examples/storybook/src/stories/streaming-widget/{StreamingWidgetOverrides.stories.tsx => StreamingWidgetThemeOverrides.stories.tsx} (63%) diff --git a/examples/storybook/src/stories/claim-widget/ClaimWidgetThemeOverrides.stories.tsx b/examples/storybook/src/stories/claim-widget/ClaimWidgetThemeOverrides.stories.tsx new file mode 100644 index 00000000..6d12b832 --- /dev/null +++ b/examples/storybook/src/stories/claim-widget/ClaimWidgetThemeOverrides.stories.tsx @@ -0,0 +1,164 @@ +/** + * ClaimWidget Theme Demo — Theme Overrides — demonstrates the widget's public + * theming surface as live color-picker controls, one row per field, all + * visible without expanding a collapsed tree. The code snippet is generated + * from the live arg values, so it can never drift from what's rendered. + * + * ClaimWidget (@goodwidget/claim-widget-theme-demo) defines its own local + * ClaimActionGlow/Ring/Inner components (same field shape as the shared + * packages/ui/src/components/CircularActionButton.tsx) plus a plain `Button` + * used for the "Reset Demo" action. See packages/claim-widget-theme-demo/src/ClaimWidget.tsx. + * + * Controls are wired for `dark_Button`, `dark_ClaimActionGlow`, + * `dark_ClaimActionRing`, and `dark_ClaimCard` — a handful of high-impact + * targets that visibly shift the default brand, not exhaustive coverage of + * every value. `dark_ClaimActionInner` and `dark_TokenAmountText` follow the + * same pattern and are documented below for reference. + */ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react' +import type { GoodWidgetThemeOverrides } from '@goodwidget/core' +import { ClaimWidgetStoryCanvas } from '../helpers/claimWidgetStories' +import { DocsCallout, DocsList } from '../docs/DocsLayout' + +const REFERENCE_ONLY_TARGETS: Array<{ name: string; fields: string[] }> = [ + { name: 'ClaimActionInner', fields: ['backgroundDark', 'backgroundDarkHover'] }, + { name: 'TokenAmountText', fields: ['color', 'secondaryColor'] }, +] + +function CodeBlock({ children }: { children: string }) { + return ( +
+      {children}
+    
+ ) +} + +interface OverridesArgs { + buttonBackground: string + buttonBackgroundHover: string + buttonBackgroundPress: string + buttonBackgroundFocus: string + buttonColor: string + claimActionGlowBackground: string + claimActionRingPrimary: string + claimCardBorderColor: string +} + +function buildThemeOverrides(args: OverridesArgs): GoodWidgetThemeOverrides { + return { + themes: { + dark_Button: { + background: args.buttonBackground, + backgroundHover: args.buttonBackgroundHover, + backgroundPress: args.buttonBackgroundPress, + backgroundFocus: args.buttonBackgroundFocus, + color: args.buttonColor, + }, + dark_ClaimActionGlow: { + backgroundColor: args.claimActionGlowBackground, + }, + dark_ClaimActionRing: { + primary: args.claimActionRingPrimary, + }, + dark_ClaimCard: { + borderColor: args.claimCardBorderColor, + }, + }, + } +} + +const meta: Meta = { + title: 'Widgets/ClaimWidget Theme Demo/Theme overrides', + tags: ['integrator', 'showcase'], + parameters: { layout: 'padded' }, + argTypes: { + buttonBackground: { control: 'color', description: 'themes.dark_Button.background' }, + buttonBackgroundHover: { control: 'color', description: 'themes.dark_Button.backgroundHover' }, + buttonBackgroundPress: { control: 'color', description: 'themes.dark_Button.backgroundPress' }, + buttonBackgroundFocus: { control: 'color', description: 'themes.dark_Button.backgroundFocus' }, + buttonColor: { control: 'color', description: 'themes.dark_Button.color' }, + claimActionGlowBackground: { + control: 'color', + description: 'themes.dark_ClaimActionGlow.backgroundColor', + }, + claimActionRingPrimary: { control: 'color', description: 'themes.dark_ClaimActionRing.primary' }, + claimCardBorderColor: { control: 'color', description: 'themes.dark_ClaimCard.borderColor' }, + }, + args: { + buttonBackground: '#7C3AED', + buttonBackgroundHover: '#6D28D9', + buttonBackgroundPress: '#5B21B6', + buttonBackgroundFocus: '#6D28D9', + buttonColor: '#FFFFFF', + claimActionGlowBackground: '#7C3AED', + claimActionRingPrimary: '#7C3AED', + claimCardBorderColor: '#7C3AED', + }, +} +export default meta +type Story = StoryObj + +export const Playground: Story = { + render: (args) => { + const themeOverrides = buildThemeOverrides(args) + return ( +
+ + {``} + + + + +
  • + dark_Button / light_Button: background, backgroundHover, + backgroundPress, backgroundFocus, color, borderColor, borderColorFocus, shadowColor — + wired to the controls above +
  • +
  • + dark_ClaimActionGlow / light_ClaimActionGlow: backgroundColor, + glowOpacity, glowOffset — wired to the controls above (backgroundColor only) +
  • +
  • + dark_ClaimActionRing / light_ClaimActionRing: primary, primaryLight — + wired to the controls above (primary only) +
  • +
  • + dark_ClaimCard / light_ClaimCard: background, borderColor, + shadowColor — wired to the controls above (borderColor only) +
  • + {REFERENCE_ONLY_TARGETS.map((target) => ( +
  • + + dark_{target.name} / light_{target.name} + + : {target.fields.join(', ')} +
  • + ))} +
    +
    + + +
    + ) + }, +} diff --git a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetThemeOverrides.stories.tsx b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetThemeOverrides.stories.tsx new file mode 100644 index 00000000..e99cdc6c --- /dev/null +++ b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetThemeOverrides.stories.tsx @@ -0,0 +1,139 @@ +/** + * StakingMigrationWidget — Theme Overrides — demonstrates the widget's public + * theming surface as live color-picker controls, one row per field, all + * visible without expanding a collapsed tree. The code snippet is generated + * from the live arg values, so it can never drift from what's rendered. + * + * StakingMigrationWidget has no local named-theme components of its own: its + * cards reuse the base preset's `ClaimCard`/`StreakCard` names (see + * packages/staking-migration-widget/src/migrationWidgetComponents.ts) and its + * circular action button is the shared + * packages/ui/src/components/CircularActionButton.tsx (ClaimActionGlow/Ring/ + * Inner) also used by ai-credits-widget. Its step list uses the shared + * Stepper component (dark_StepperStepContent). + * + * Controls are wired for `dark_ClaimCard`, `dark_ClaimActionGlow`, and + * `dark_ClaimActionRing` — a handful of high-impact targets that visibly + * shift the default brand, not exhaustive coverage of every value. + * `dark_ClaimActionInner`, `dark_StreakCard`, and `dark_StepperStepContent` + * follow the same pattern and are documented below for reference. There is + * no `dark_Button` override here since this widget has no plain `Button` + * usage — its only action is the circular claim/migrate button. + */ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react' +import type { GoodWidgetThemeOverrides } from '@goodwidget/core' +import { ReadyStory } from '../helpers/stakingMigrationWidgetStories' +import { DocsCallout, DocsList } from '../docs/DocsLayout' + +const REFERENCE_ONLY_TARGETS: Array<{ name: string; fields: string[] }> = [ + { name: 'ClaimActionInner', fields: ['backgroundDark', 'backgroundDarkHover'] }, + { name: 'StreakCard', fields: ['background', 'borderColor', 'shadowColor'] }, + { name: 'StepperStepContent', fields: ['background', 'borderColor'] }, +] + +function CodeBlock({ children }: { children: string }) { + return ( +
    +      {children}
    +    
    + ) +} + +interface OverridesArgs { + claimCardBorderColor: string + claimActionGlowBackground: string + claimActionRingPrimary: string +} + +function buildThemeOverrides(args: OverridesArgs): GoodWidgetThemeOverrides { + return { + themes: { + dark_ClaimCard: { + borderColor: args.claimCardBorderColor, + }, + dark_ClaimActionGlow: { + backgroundColor: args.claimActionGlowBackground, + }, + dark_ClaimActionRing: { + primary: args.claimActionRingPrimary, + }, + }, + } +} + +const meta: Meta = { + title: 'Widgets/StakingMigrationWidget/Theme overrides', + tags: ['integrator', 'showcase'], + parameters: { layout: 'padded' }, + argTypes: { + claimCardBorderColor: { control: 'color', description: 'themes.dark_ClaimCard.borderColor' }, + claimActionGlowBackground: { + control: 'color', + description: 'themes.dark_ClaimActionGlow.backgroundColor', + }, + claimActionRingPrimary: { control: 'color', description: 'themes.dark_ClaimActionRing.primary' }, + }, + args: { + claimCardBorderColor: '#7C3AED', + claimActionGlowBackground: '#7C3AED', + claimActionRingPrimary: '#7C3AED', + }, +} +export default meta +type Story = StoryObj + +export const Playground: Story = { + render: (args) => { + const themeOverrides = buildThemeOverrides(args) + return ( +
    + + {``} + + + + +
  • + dark_ClaimCard / light_ClaimCard: background, borderColor, + shadowColor — wired to the controls above (borderColor only) +
  • +
  • + dark_ClaimActionGlow / light_ClaimActionGlow: backgroundColor, + glowOpacity, glowOffset — wired to the controls above (backgroundColor only) +
  • +
  • + dark_ClaimActionRing / light_ClaimActionRing: primary, primaryLight — + wired to the controls above (primary only) +
  • + {REFERENCE_ONLY_TARGETS.map((target) => ( +
  • + + dark_{target.name} / light_{target.name} + + : {target.fields.join(', ')} +
  • + ))} +
    +
    + + +
    + ) + }, +} diff --git a/examples/storybook/src/stories/streaming-widget/StreamingWidgetOverrides.stories.tsx b/examples/storybook/src/stories/streaming-widget/StreamingWidgetThemeOverrides.stories.tsx similarity index 63% rename from examples/storybook/src/stories/streaming-widget/StreamingWidgetOverrides.stories.tsx rename to examples/storybook/src/stories/streaming-widget/StreamingWidgetThemeOverrides.stories.tsx index 24381a62..6a6dbbae 100644 --- a/examples/storybook/src/stories/streaming-widget/StreamingWidgetOverrides.stories.tsx +++ b/examples/storybook/src/stories/streaming-widget/StreamingWidgetThemeOverrides.stories.tsx @@ -1,5 +1,5 @@ /** - * StreamingWidget Advanced Overrides — demonstrates the widget's full public + * StreamingWidget Theme Overrides — demonstrates the widget's full public * theming surface (the named component sub-themes it renders through), live. * * Unlike the Showcase story's fixed brand-preset picker, this exposes real @@ -8,27 +8,41 @@ * collapsed tree. The code snippet is generated from the live arg values * rather than hardcoded, so it can never drift from what's actually rendered. * + * Guiding text reuses the same DocsLayout building blocks as the docs pages + * (Integrators/Theming And Overrides, etc.) so this reads as a continuation + * of that guide rather than a separate visual language. + * * StreamingWidget uses `Button` directly for its primary actions (no * StreamingWidget-specific sub-theme), plus six Card-derived named * components that fall back to Card's field set since they don't have their * own preset entries. See packages/streaming-widget/src/components/shared.tsx. * * Controls are wired for `dark_Button` and the two targets rendered in the - * PopulatedStateStory fixture (`dark_StreamRow`, `dark_BalanceCard`); the - * remaining Card-derived targets follow the same field shape and are listed - * for reference below rather than each getting their own control row. + * PopulatedStateStory fixture (`dark_StreamRow`, `dark_BalanceCard`) — a + * couple of high-impact targets that visibly shift the default brand, not + * exhaustive coverage of every value. The remaining Card-derived targets + * follow the same field shape and are documented below for reference. */ import React from 'react' import type { Meta, StoryObj } from '@storybook/react' import type { StreamingWidgetProps } from '@goodwidget/streaming-widget' -import { Card, Heading, Text, YStack } from '@goodwidget/ui' import { PopulatedStateStory } from '../helpers/streamingWidgetStories' import { withDefaultPreset } from '../helpers/withDefaultPreset' +import { DocsCallout, DocsList } from '../docs/DocsLayout' type ThemeOverrides = NonNullable const CARD_FIELDS = ['background', 'borderColor', 'shadowColor'] +const CONTROLLED_TARGETS: Array<{ name: string; fields: string[] }> = [ + { + name: 'Button', + fields: ['background', 'backgroundHover', 'backgroundPress', 'backgroundFocus', 'color'], + }, + { name: 'StreamRow', fields: ['borderColor'] }, + { name: 'BalanceCard', fields: ['borderColor', 'shadowColor'] }, +] + const REFERENCE_ONLY_TARGETS: Array<{ name: string; fields: string[] }> = [ { name: 'PoolRow', fields: CARD_FIELDS }, { name: 'EmptyStateCard', fields: CARD_FIELDS }, @@ -36,6 +50,29 @@ const REFERENCE_ONLY_TARGETS: Array<{ name: string; fields: string[] }> = [ { name: 'SetStreamFormCard', fields: CARD_FIELDS }, ] +function CodeBlock({ children }: { children: string }) { + return ( +
    +      {children}
    +    
    + ) +} + interface OverridesArgs { buttonBackground: string buttonBackgroundHover: string @@ -69,7 +106,7 @@ function buildThemeOverrides(args: OverridesArgs): ThemeOverrides { } const meta: Meta = { - title: 'Widgets/StreamingWidget/Advanced Overrides', + title: 'Widgets/StreamingWidget/Theme overrides', tags: ['integrator', 'showcase'], parameters: { layout: 'padded' }, decorators: [withDefaultPreset], @@ -101,29 +138,34 @@ export const Playground: Story = { render: (args) => { const themeOverrides = buildThemeOverrides(args) return ( - - - Other Card-derived targets (same field shape) - +
    + + {``} + + + + + {CONTROLLED_TARGETS.map((target) => ( +
  • + + dark_{target.name} / light_{target.name} + + : {target.fields.join(', ')} — wired to the controls above +
  • + ))} {REFERENCE_ONLY_TARGETS.map((target) => ( - - +
  • + dark_{target.name} / light_{target.name} - - {': '} - {target.fields.join(', ')} - + + : {target.fields.join(', ')} +
  • ))} - - - - How it works - - {``} - - +
    +
    + - +
    ) }, } From 4c77ccfe8b68be2687f0f06b73e2ac441d5056cc Mon Sep 17 00:00:00 2001 From: Thompson <140930314+Godbrand0@users.noreply.github.com> Date: Thu, 16 Jul 2026 18:25:20 -0400 Subject: [PATCH 12/12] fix(storybook): wire up docs pages and disableSaveFromUI missed in prior commit These belonged in the previous commit (theme-overrides playground replication) but were dropped by a staging mistake: a failed git add on a stale path aborted the whole add call. - preview.tsx: parameters.controls.disableSaveFromUI = true. - Each widget's .mdx docs page: new "Theme overrides" DocsSection embedding its Playground story via Canvas. --- examples/storybook/.storybook/preview.tsx | 1 + .../storybook/src/stories/claim-widget/ClaimWidget.mdx | 8 ++++++++ .../staking-migration-widget/StakingMigrationWidget.mdx | 8 ++++++++ .../src/stories/streaming-widget/StreamingWidget.mdx | 8 ++++++++ 4 files changed, 25 insertions(+) diff --git a/examples/storybook/.storybook/preview.tsx b/examples/storybook/.storybook/preview.tsx index d44d72c2..7fc93555 100644 --- a/examples/storybook/.storybook/preview.tsx +++ b/examples/storybook/.storybook/preview.tsx @@ -6,6 +6,7 @@ const preview: Preview = { layout: 'centered', controls: { expanded: true, + disableSaveFromUI: true, }, options: { storySort: { diff --git a/examples/storybook/src/stories/claim-widget/ClaimWidget.mdx b/examples/storybook/src/stories/claim-widget/ClaimWidget.mdx index 8da96ca6..e35c4831 100644 --- a/examples/storybook/src/stories/claim-widget/ClaimWidget.mdx +++ b/examples/storybook/src/stories/claim-widget/ClaimWidget.mdx @@ -1,5 +1,6 @@ import { Canvas, Meta, Source } from '@storybook/blocks'; import * as ClaimWidgetStories from './ClaimWidget.stories'; +import * as ThemeOverridesStories from './ClaimWidgetThemeOverrides.stories'; import { DocsCallout, DocsCard, DocsGrid, DocsList, DocsPage, DocsSection } from '../docs/DocsLayout'; @@ -36,6 +37,13 @@ import { DocsCallout, DocsCard, DocsGrid, DocsList, DocsPage, DocsSection } from
    + + + + @@ -16,6 +17,13 @@ import { DocsCallout, DocsCard, DocsGrid, DocsPage, DocsSection } from '../docs/ + + + + @@ -16,6 +17,13 @@ import { DocsCallout, DocsCard, DocsGrid, DocsPage, DocsSection } from '../docs/ + + + +