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/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/ > + + + + + ) +}`} + /> + + = { +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/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.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 + + + + = { +interface ClaimWidgetStoryArgs { + defaultTheme: 'light' | 'dark' + brandPreset: BrandPreset +} + +const meta: Meta = { title: 'Widgets/ClaimWidget Theme Demo/Showcase', component: ClaimWidget, tags: ['integrator', 'showcase'], parameters: { layout: 'padded' }, - goodWidgetProvider: { - 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/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/design-system/Card.stories.tsx b/examples/storybook/src/stories/design-system/Card.stories.tsx index be449457..7f3d01ea 100644 --- a/examples/storybook/src/stories/design-system/Card.stories.tsx +++ b/examples/storybook/src/stories/design-system/Card.stories.tsx @@ -15,11 +15,18 @@ 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 -/** 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: () => ( @@ -61,3 +68,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..3cca9385 100644 --- a/examples/storybook/src/stories/design-system/Drawer.stories.tsx +++ b/examples/storybook/src/stories/design-system/Drawer.stories.tsx @@ -15,11 +15,19 @@ 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 -/** 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) @@ -52,3 +60,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..af687424 100644 --- a/examples/storybook/src/stories/design-system/GlowCard.stories.tsx +++ b/examples/storybook/src/stories/design-system/GlowCard.stories.tsx @@ -15,11 +15,16 @@ 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 -/** 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: () => ( @@ -31,3 +36,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..a51128a0 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,27 @@ 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 + +/** Fixed reference story — the Controls panel is inert here; use "Controllable" below to + * drive props live. */ export const Default: Story = { render: () => ( @@ -40,3 +50,31 @@ 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: ({ activeStepId, maxHeight }) => ( + + Transaction steps} + maxHeight={maxHeight} + /> + + ), +} 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 = { {`('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,17 +81,27 @@ 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/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..dd73ad1f 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' @@ -73,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) { @@ -99,7 +112,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 +139,8 @@ export function InjectedWalletStory() { {!migrationApiBaseUrl && ( @@ -133,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 ( @@ -219,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 21dfb3b1..af80c7f4 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} /> ) @@ -234,11 +237,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 +254,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 +282,8 @@ export function InjectedWalletStory({ apiKey }: Pick ) } @@ -281,11 +293,17 @@ export function InjectedWalletStory({ apiKey }: Pick ) } -export function CustodialLocalFixtureStory({ apiKey }: Pick) { +export function CustodialLocalFixtureStory({ + apiKey, + defaultTheme, + themeOverrides, +}: Pick) { try { const provider = createCustodialEip1193Provider() return ( @@ -293,6 +311,8 @@ export function CustodialLocalFixtureStory({ apiKey }: Pick ) } catch (error: unknown) { @@ -312,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 ( @@ -432,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 ( @@ -578,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/helpers/themeOverridePresets.ts b/examples/storybook/src/stories/helpers/themeOverridePresets.ts new file mode 100644 index 00000000..0c735aec --- /dev/null +++ b/examples/storybook/src/stories/helpers/themeOverridePresets.ts @@ -0,0 +1,143 @@ +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: { + color: { + primary: '#2E5DE8', + primaryDark: '#1D3EB2', + primaryLight: '#6E8DFF', + }, + }, + themes: { + dark_ClaimCard: { borderColor: '#2E5DE8', shadowColor: 'rgba(46,93,232,0.7)' }, + 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' }, + // Button — the most visible themed surface across every widget (e.g. StreamingWidget's + // "Connect Wallet" action). Without this, only card borders/shadows would shift and the + // preset would look like a no-op on any wallet-gated fallback screen. + dark_Button: { + background: '#2E5DE8', + backgroundHover: '#1D3EB2', + backgroundPress: '#1D3EB2', + backgroundFocus: '#1D3EB2', + color: '#FFFFFF', + borderColor: '#2E5DE8', + borderColorFocus: '#6E8DFF', + shadowColor: 'rgba(46,93,232,0.8)', + }, + light_Button: { + background: '#2E5DE8', + backgroundHover: '#1D3EB2', + backgroundPress: '#1D3EB2', + backgroundFocus: '#1D3EB2', + color: '#FFFFFF', + borderColor: '#2E5DE8', + borderColorFocus: '#6E8DFF', + shadowColor: 'rgba(46,93,232,0.3)', + }, + // 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)' }, + }, +} + +export const tealOverrides: GoodWidgetThemeOverrides = { + tokens: { + color: { + primary: '#00A884', + primaryDark: '#007A61', + primaryLight: '#33C9AA', + }, + }, + themes: { + dark_ClaimCard: { borderColor: '#00A884', shadowColor: 'rgba(0,168,132,0.65)' }, + 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' }, + // Button — see cobaltOverrides comment above. + dark_Button: { + background: '#00A884', + backgroundHover: '#007A61', + backgroundPress: '#007A61', + backgroundFocus: '#007A61', + color: '#FFFFFF', + borderColor: '#00A884', + borderColorFocus: '#33C9AA', + shadowColor: 'rgba(0,168,132,0.8)', + }, + light_Button: { + background: '#00A884', + backgroundHover: '#007A61', + backgroundPress: '#007A61', + backgroundFocus: '#007A61', + color: '#FFFFFF', + borderColor: '#00A884', + borderColorFocus: '#33C9AA', + shadowColor: 'rgba(0,168,132,0.3)', + }, + // 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)' }, + }, +} + +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/StakingMigrationWidget.mdx b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.mdx index 03eb75fd..c17ff8a8 100644 --- a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.mdx +++ b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.mdx @@ -1,5 +1,6 @@ -import { Canvas, Meta } from '@storybook/blocks'; +import { Canvas, Meta, Source } from '@storybook/blocks'; import * as ShowcaseStories from './StakingMigrationWidgetShowcase.stories'; +import * as ThemeOverridesStories from './StakingMigrationWidgetThemeOverrides.stories'; import { DocsCallout, DocsCard, DocsGrid, DocsPage, DocsSection } from '../docs/DocsLayout'; @@ -11,11 +12,42 @@ import { DocsCallout, DocsCard, DocsGrid, DocsPage, DocsSection } from '../docs/ > + + + + + + + + + ) +}`} + /> + + = { +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/staking-migration-widget/StakingMigrationWidgetShowcase.stories.tsx b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetShowcase.stories.tsx index 236fb331..fc1852b2 100644 --- a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetShowcase.stories.tsx +++ b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidgetShowcase.stories.tsx @@ -1,22 +1,41 @@ 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: { - goodWidgetProvider: { - useShell: false, - }, - }, - render: () => , + render: ({ defaultTheme, brandPreset }) => ( + + ), } 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/StreamingWidget.mdx b/examples/storybook/src/stories/streaming-widget/StreamingWidget.mdx index e1bb84ec..847f3bec 100644 --- a/examples/storybook/src/stories/streaming-widget/StreamingWidget.mdx +++ b/examples/storybook/src/stories/streaming-widget/StreamingWidget.mdx @@ -1,5 +1,6 @@ import { Canvas, Meta } from '@storybook/blocks'; import * as ShowcaseStories from './StreamingWidget.stories'; +import * as ThemeOverridesStories from './StreamingWidgetThemeOverrides.stories'; import { DocsCallout, DocsCard, DocsGrid, DocsPage, DocsSection } from '../docs/DocsLayout'; @@ -11,11 +12,18 @@ import { DocsCallout, DocsCard, DocsGrid, DocsPage, DocsSection } from '../docs/ > + + + + = { +interface StreamingWidgetStoryArgs { + apiKey?: string + defaultTheme: 'light' | 'dark' + brandPreset: BrandPreset +} + +const meta: Meta = { title: 'Widgets/StreamingWidget/Showcase', component: StreamingWidget, tags: ['integrator', 'manual', 'showcase'], @@ -14,22 +21,33 @@ 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: { - goodWidgetProvider: { - useShell: false, - }, - }, - render: ({ apiKey }: Pick) => ( - + render: ({ apiKey, defaultTheme, 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..97470e09 100644 --- a/examples/storybook/src/stories/streaming-widget/StreamingWidgetQA.stories.tsx +++ b/examples/storybook/src/stories/streaming-widget/StreamingWidgetQA.stories.tsx @@ -23,17 +23,19 @@ 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'], - parameters: { - layout: 'padded', - goodWidgetProvider: { - useShell: false, - }, - }, + parameters: { layout: 'padded' }, argTypes: { apiKey: { control: 'text', @@ -41,95 +43,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 }) => ( + + ), } diff --git a/examples/storybook/src/stories/streaming-widget/StreamingWidgetThemeOverrides.stories.tsx b/examples/storybook/src/stories/streaming-widget/StreamingWidgetThemeOverrides.stories.tsx new file mode 100644 index 00000000..6a6dbbae --- /dev/null +++ b/examples/storybook/src/stories/streaming-widget/StreamingWidgetThemeOverrides.stories.tsx @@ -0,0 +1,171 @@ +/** + * 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 + * `themeOverrides.themes` fields as individual color-picker controls — one + * row per field, all visible in the Controls panel without expanding a + * 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`) — 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 { 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 }, + { name: 'ErrorStateCard', fields: CARD_FIELDS }, + { name: 'SetStreamFormCard', fields: CARD_FIELDS }, +] + +function CodeBlock({ children }: { children: string }) { + return ( +
    +      {children}
    +    
    + ) +} + +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/Theme 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 ( +
    + + {``} + + + + + {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(', ')} +
  • + ))} +
    +
    + + +
    + ) + }, +}