+
+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(', ')}
+
+ ))}
+
+
+
+
+
+ )
+ },
+}