Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@goodwidget/claim-widget-theme-demo": "workspace:*",
"@goodwidget/citizen-claim-widget": "workspace:*",
"@goodwidget/connect-a-wallet-widget": "workspace:*",
"@goodwidget/passkey-login-widget": "workspace:*",
"@goodwidget/goodreserve-widget": "workspace:*",
"@goodwidget/governance-widget": "workspace:*",
"@goodwidget/streaming-widget": "workspace:*",
Expand Down
55 changes: 55 additions & 0 deletions examples/storybook/src/fixtures/passkeyMockStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
ProviderAuthError,
type PasskeyBackupStorage,
type PasskeyProviderId,
} from '@goodwidget/passkey-login-widget'

/** Failure switches a Playwright spec flips from the page to exercise the D screens. */
export interface MockBackupStorageControls {
/** File name -> backup JSON currently stored. */
files: Map<string, string>
/** Makes `authorize` fail like a closed or denied provider sign-in. */
failAuthorization: boolean
/** Makes `writeBackup` fail like a full or unreachable provider. */
failWrites: boolean
}

export interface MockBackupStorage extends PasskeyBackupStorage {
controls: MockBackupStorageControls
}

const PROVIDER_LABELS: Record<PasskeyProviderId, string> = {
'google-drive': 'Google Drive',
icloud: 'iCloud',
}

/**
* In-memory stand-in for a Drive / iCloud adapter. It stores the opaque backup JSON the widget
* hands over, so the widget's real crypto and flow run end to end without a network.
*/
export function createMockBackupStorage(providerId: PasskeyProviderId): MockBackupStorage {
const controls: MockBackupStorageControls = { files: new Map(), failAuthorization: false, failWrites: false }
return {
id: providerId,
label: PROVIDER_LABELS[providerId],
controls,
async authorize() {
if (controls.failAuthorization) {
// Adapters signal sign-in problems with ProviderAuthError; the widget shows the provider-auth screen
throw new ProviderAuthError('Mock provider sign-in failed')
}
},
async listBackupNames(prfTag) {
return [...controls.files.keys()].filter((fileName) => fileName.startsWith(`gwpk-${prfTag}-`))
},
async readBackup(fileName) {
return controls.files.get(fileName) ?? null
},
async writeBackup(fileName, backupJson) {
if (controls.failWrites) {
throw new Error('Mock provider write failed')
}
controls.files.set(fileName, backupJson)
},
}
}
4 changes: 4 additions & 0 deletions examples/storybook/src/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ interface ImportMetaEnv {
readonly VITE_AI_CREDITS_FUNDING_VAULT_ADDRESS?: string
readonly VITE_AI_CREDITS_VAULT_ADDRESS?: string
readonly VITE_AI_CREDITS_GOODID_ADDRESS?: string
readonly VITE_PASSKEY_GOOGLE_CLIENT_ID?: string
readonly VITE_PASSKEY_ICLOUD_CONTAINER_ID?: string
readonly VITE_PASSKEY_ICLOUD_API_TOKEN?: string
readonly VITE_PASSKEY_ICLOUD_ENVIRONMENT?: string
}

interface ImportMeta {
Expand Down
63 changes: 63 additions & 0 deletions examples/storybook/src/stories/design-system/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Button — interactive primitive.
*
* Every variant must keep a readable label in both themes. The variants grid below
* is the regression fixture for the label-contrast smoke test in
* tests/design-system/smoke.spec.ts.
*/
import React from 'react'
import type { Decorator, Meta, StoryObj } from '@storybook/react'
import { GoodWidgetProvider } from '@goodwidget/core'
import { Button, ButtonText, XStack, YStack } from '@goodwidget/ui'

/** Button variants that render a text label on a non-brand background. */
const LABELLED_VARIANTS = ['primary', 'secondary', 'outline', 'ghost', 'text'] as const

type ThemeMode = 'light' | 'dark'

/** Renders the story inside the real provider using the theme chosen by the story parameters. */
const withThemeModeProvider: Decorator = (Story, context) => {
const themeMode = (context.parameters.themeMode as ThemeMode | undefined) ?? 'light'
return (
<GoodWidgetProvider defaultTheme={themeMode}>
<YStack padding="$4" backgroundColor="$background">
<Story />
</YStack>
</GoodWidgetProvider>
)
}

const meta: Meta<typeof Button> = {
title: 'Design System/Primitives/Button',
component: Button,
tags: ['autodocs', 'showcase'],
parameters: { layout: 'padded' },
decorators: [withThemeModeProvider],
}
export default meta
type Story = StoryObj<typeof Button>

/** One button per labelled variant so label color can be asserted per variant. */
function renderVariantsGrid() {
return (
<XStack gap="$3" flexWrap="wrap" data-testid="Button-variants">
{LABELLED_VARIANTS.map((variant) => (
<Button key={variant} variant={variant} data-testid={`Button-${variant}`}>
<ButtonText data-testid={`ButtonText-${variant}`}>{variant}</ButtonText>
</Button>
))}
</XStack>
)
}

/** All labelled variants on the light theme. */
export const VariantsLight: Story = {
parameters: { themeMode: 'light' },
render: renderVariantsGrid,
}

/** All labelled variants on the dark theme. */
export const VariantsDark: Story = {
parameters: { themeMode: 'dark' },
render: renderVariantsGrid,
}
Loading
Loading