feat(devx): developer experience & quality (M19.1-M19.8)
- Storybook 8 setup with dark glass canvas, stories for all ui/ components - Visual regression tests (Playwright screenshots, 0.5% pixel diff) - Bundle size CI gate (fail > 250KB gzipped) - Comprehensive mock data: 20+ items for films, songs, books, TV, images, places, podcasts, news, nostr events; mock TMDB responses - E2E cross-browser matrix: Chromium + Firefox + WebKit + iPhone 14 + Galaxy S21 - Proxy integration tests (SSE format, tool_use, error handling, disconnect) - Lighthouse CI config (LCP < 3s, CLS < 0.15) - Dependency audit: weekly GitHub Actions workflow with license checker - CI workflow: lint, typecheck, test, bundle size, e2e, lighthouse, audit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a01f43c95f
commit
18b972b351
@@ -0,0 +1,49 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import { ref } from 'vue'
|
||||
import BottomSheet from '../BottomSheet.vue'
|
||||
|
||||
const meta: Meta<typeof BottomSheet> = {
|
||||
title: 'UI/BottomSheet',
|
||||
component: BottomSheet,
|
||||
tags: ['autodocs'],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof BottomSheet>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { BottomSheet },
|
||||
setup() {
|
||||
const isOpen = ref(true)
|
||||
return { isOpen }
|
||||
},
|
||||
template: `
|
||||
<BottomSheet :is-open="isOpen" @close="isOpen = false">
|
||||
<div class="p-6 space-y-4">
|
||||
<h3 class="text-lg font-semibold text-white/90">Bottom Sheet</h3>
|
||||
<p class="text-sm text-white/60">Drag the handle to resize or swipe down to close.</p>
|
||||
<div v-for="i in 5" :key="i" class="h-12 rounded-xl bg-white/5 border border-white/10" />
|
||||
</div>
|
||||
</BottomSheet>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const CustomSnapPoints: Story = {
|
||||
render: () => ({
|
||||
components: { BottomSheet },
|
||||
setup() {
|
||||
const isOpen = ref(true)
|
||||
return { isOpen }
|
||||
},
|
||||
template: `
|
||||
<BottomSheet :is-open="isOpen" :snap-points="[30, 60, 100]" @close="isOpen = false">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold text-white/90">Custom Snap Points</h3>
|
||||
<p class="text-sm text-white/60 mt-2">Snaps to 30%, 60%, and 100%.</p>
|
||||
</div>
|
||||
</BottomSheet>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import ContextMenu from '../ContextMenu.vue'
|
||||
import ContextMenuItem from '../ContextMenuItem.vue'
|
||||
|
||||
const meta: Meta<typeof ContextMenu> = {
|
||||
title: 'UI/ContextMenu',
|
||||
component: ContextMenu,
|
||||
tags: ['autodocs'],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof ContextMenu>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { ContextMenu, ContextMenuItem },
|
||||
setup() {
|
||||
const menuRef = ref<InstanceType<typeof ContextMenu> | null>(null)
|
||||
onMounted(() => {
|
||||
menuRef.value?.open(200, 100)
|
||||
})
|
||||
return { menuRef }
|
||||
},
|
||||
template: `
|
||||
<ContextMenu ref="menuRef">
|
||||
<ContextMenuItem @click="() => {}">Edit</ContextMenuItem>
|
||||
<ContextMenuItem @click="() => {}">Copy</ContextMenuItem>
|
||||
<ContextMenuItem @click="() => {}">Share</ContextMenuItem>
|
||||
<ContextMenuItem destructive @click="() => {}">Delete</ContextMenuItem>
|
||||
</ContextMenu>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import ContextMenuItem from '../ContextMenuItem.vue'
|
||||
|
||||
const meta: Meta<typeof ContextMenuItem> = {
|
||||
title: 'UI/ContextMenuItem',
|
||||
component: ContextMenuItem,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div class="glass-card p-1.5 rounded-xl min-w-[140px]"><story /></div>',
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof ContextMenuItem>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { ContextMenuItem },
|
||||
template: '<ContextMenuItem @click="() => {}">Edit Message</ContextMenuItem>',
|
||||
}),
|
||||
}
|
||||
|
||||
export const Destructive: Story = {
|
||||
render: () => ({
|
||||
components: { ContextMenuItem },
|
||||
template: '<ContextMenuItem destructive @click="() => {}">Delete</ContextMenuItem>',
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import { defineComponent } from 'vue'
|
||||
import ErrorBoundary from '../ErrorBoundary.vue'
|
||||
|
||||
const meta: Meta<typeof ErrorBoundary> = {
|
||||
title: 'UI/ErrorBoundary',
|
||||
component: ErrorBoundary,
|
||||
tags: ['autodocs'],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof ErrorBoundary>
|
||||
|
||||
const BrokenChild = defineComponent({
|
||||
setup() {
|
||||
throw new Error('Something went wrong in the child component')
|
||||
},
|
||||
template: '<div>This should not render</div>',
|
||||
})
|
||||
|
||||
export const WithError: Story = {
|
||||
render: () => ({
|
||||
components: { ErrorBoundary, BrokenChild },
|
||||
template: `
|
||||
<ErrorBoundary title="Component Error">
|
||||
<BrokenChild />
|
||||
</ErrorBoundary>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const NoError: Story = {
|
||||
render: () => ({
|
||||
components: { ErrorBoundary },
|
||||
template: `
|
||||
<ErrorBoundary>
|
||||
<div class="p-4 text-white/80">Everything is working fine.</div>
|
||||
</ErrorBoundary>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import { ref } from 'vue'
|
||||
import FavoriteButton from '../FavoriteButton.vue'
|
||||
|
||||
const meta: Meta<typeof FavoriteButton> = {
|
||||
title: 'UI/FavoriteButton',
|
||||
component: FavoriteButton,
|
||||
tags: ['autodocs'],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof FavoriteButton>
|
||||
|
||||
export const Unfavorited: Story = {
|
||||
render: () => ({
|
||||
components: { FavoriteButton },
|
||||
setup() {
|
||||
const favorited = ref(false)
|
||||
return { favorited }
|
||||
},
|
||||
template: '<FavoriteButton :favorited="favorited" @toggle="favorited = !favorited" />',
|
||||
}),
|
||||
}
|
||||
|
||||
export const Favorited: Story = {
|
||||
render: () => ({
|
||||
components: { FavoriteButton },
|
||||
setup() {
|
||||
const favorited = ref(true)
|
||||
return { favorited }
|
||||
},
|
||||
template: '<FavoriteButton :favorited="favorited" @toggle="favorited = !favorited" />',
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import LightningInvoice from '../LightningInvoice.vue'
|
||||
|
||||
const meta: Meta<typeof LightningInvoice> = {
|
||||
title: 'UI/LightningInvoice',
|
||||
component: LightningInvoice,
|
||||
tags: ['autodocs'],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof LightningInvoice>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
invoice: 'lnbc10u1pjexampleinvoicethatisverylongandcontainsmanycharacters0123456789abcdef',
|
||||
expirySeconds: 600,
|
||||
},
|
||||
}
|
||||
|
||||
export const Expiring: Story = {
|
||||
args: {
|
||||
invoice: 'lnbc10u1pjexampleinvoicethatisverylongandcontainsmanycharacters0123456789abcdef',
|
||||
expirySeconds: 30,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import PassphraseDialog from '../PassphraseDialog.vue'
|
||||
|
||||
const meta: Meta<typeof PassphraseDialog> = {
|
||||
title: 'UI/PassphraseDialog',
|
||||
component: PassphraseDialog,
|
||||
tags: ['autodocs'],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof PassphraseDialog>
|
||||
|
||||
export const Unlock: Story = {
|
||||
args: {
|
||||
visible: true,
|
||||
isCreating: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const Create: Story = {
|
||||
args: {
|
||||
visible: true,
|
||||
isCreating: true,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import PaymentButton from '../PaymentButton.vue'
|
||||
|
||||
const meta: Meta<typeof PaymentButton> = {
|
||||
title: 'UI/PaymentButton',
|
||||
component: PaymentButton,
|
||||
tags: ['autodocs'],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof PaymentButton>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
invoice: 'lnbc1000n1pjexample',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithAmount: Story = {
|
||||
args: {
|
||||
invoice: 'lnbc1000n1pjexample',
|
||||
amount: 1000,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import SearchResults from '../SearchResults.vue'
|
||||
|
||||
const meta: Meta<typeof SearchResults> = {
|
||||
title: 'UI/SearchResults',
|
||||
component: SearchResults,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div style="position:relative;height:400px;width:360px;"><story /></div>',
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof SearchResults>
|
||||
|
||||
export const WithResults: Story = {
|
||||
args: {
|
||||
isSearching: false,
|
||||
results: [
|
||||
{ id: '1', title: 'Blade Runner 2049', subtitle: 'Denis Villeneuve · 2017', type: 'film', data: null },
|
||||
{ id: '2', title: 'Arrival', subtitle: 'Denis Villeneuve · 2016', type: 'film', data: null },
|
||||
{ id: '3', title: 'Never Meant', subtitle: 'American Football', type: 'song', data: null },
|
||||
{ id: '4', title: 'What Bitcoin Did', subtitle: 'Peter McCormack', type: 'podcast', data: null },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const Searching: Story = {
|
||||
args: {
|
||||
isSearching: true,
|
||||
results: [],
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user