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:
Dorian
2026-03-04 01:15:39 +00:00
co-authored by Claude Opus 4.6
parent a01f43c95f
commit 18b972b351
29 changed files with 2361 additions and 1 deletions
@@ -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: [],
},
}