fix(gui-v2): scope AppTopbar dark story + DRY shell story renders
Code-review follow-up. AppTopbar DarkTheme mutated <html>.dark which leaked into Default/CompactDensity stacked on the same autodocs page; scope dark to the story subtree via a `.dark` wrapper (Aura darkModeSelector is the `.dark` class) — verified isolated on the docs page. Also factor the duplicated render scaffolds in AppDialog (shared dialogStory factory) and WorkspaceSwitcher (meta-level render). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -23,10 +23,18 @@ const meta: Meta<typeof AppDialog> = {
|
||||
export default meta
|
||||
type Story = StoryObj<typeof AppDialog>
|
||||
|
||||
export const Default: Story = {
|
||||
args: { open: true, title: 'Edit shift' },
|
||||
render: args => ({
|
||||
components: { AppDialog },
|
||||
/**
|
||||
* Shared render: only the inner markup (default body + optional
|
||||
* #footer / #tabs slots) varies between stories, so the v-model:open
|
||||
* scaffold lives here once. `extra` registers any extra components the
|
||||
* inner markup references (e.g. Button for the footer story).
|
||||
*/
|
||||
function dialogStory(
|
||||
inner: string,
|
||||
extra: Record<string, unknown> = {},
|
||||
): Story['render'] {
|
||||
return args => ({
|
||||
components: { AppDialog, ...extra },
|
||||
setup() {
|
||||
const open = ref(args.open)
|
||||
|
||||
@@ -34,86 +42,48 @@ export const Default: Story = {
|
||||
},
|
||||
template: `
|
||||
<AppDialog v-model:open="open" :title="args.title" :sub="args.sub" :width="args.width">
|
||||
<p class="text-sm">Dialog body content goes here.</p>
|
||||
${inner}
|
||||
</AppDialog>
|
||||
`,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
args: { open: true, title: 'Edit shift' },
|
||||
render: dialogStory('<p class="text-sm">Dialog body content goes here.</p>'),
|
||||
}
|
||||
|
||||
export const WithSubtitle: Story = {
|
||||
args: { open: true, title: 'Edit shift', sub: 'Saturday — Main Stage' },
|
||||
render: args => ({
|
||||
components: { AppDialog },
|
||||
setup() {
|
||||
const open = ref(args.open)
|
||||
|
||||
return { args, open }
|
||||
},
|
||||
template: `
|
||||
<AppDialog v-model:open="open" :title="args.title" :sub="args.sub" :width="args.width">
|
||||
<p class="text-sm">Dialog body with a subtitle in the header.</p>
|
||||
</AppDialog>
|
||||
`,
|
||||
}),
|
||||
render: dialogStory('<p class="text-sm">Dialog body with a subtitle in the header.</p>'),
|
||||
}
|
||||
|
||||
export const WithFooter: Story = {
|
||||
args: { open: true, title: 'Confirm action' },
|
||||
render: args => ({
|
||||
components: { AppDialog, Button },
|
||||
setup() {
|
||||
const open = ref(args.open)
|
||||
|
||||
return { args, open }
|
||||
},
|
||||
template: `
|
||||
<AppDialog v-model:open="open" :title="args.title" :sub="args.sub" :width="args.width">
|
||||
<p class="text-sm">Are you sure you want to continue?</p>
|
||||
<template #footer>
|
||||
<Button label="Cancel" severity="secondary" @click="open = false" />
|
||||
<Button label="Confirm" @click="open = false" />
|
||||
</template>
|
||||
</AppDialog>
|
||||
`,
|
||||
}),
|
||||
render: dialogStory(
|
||||
`<p class="text-sm">Are you sure you want to continue?</p>
|
||||
<template #footer>
|
||||
<Button label="Cancel" severity="secondary" @click="open = false" />
|
||||
<Button label="Confirm" @click="open = false" />
|
||||
</template>`,
|
||||
{ Button },
|
||||
),
|
||||
}
|
||||
|
||||
export const WithTabs: Story = {
|
||||
args: { open: true, title: 'Settings' },
|
||||
render: args => ({
|
||||
components: { AppDialog },
|
||||
setup() {
|
||||
const open = ref(args.open)
|
||||
|
||||
return { args, open }
|
||||
},
|
||||
template: `
|
||||
<AppDialog v-model:open="open" :title="args.title" :sub="args.sub" :width="args.width">
|
||||
<template #tabs>
|
||||
<div class="flex gap-4 px-6 py-2 border-b border-[var(--p-content-border-color)] text-[13px]">
|
||||
<span class="font-semibold text-[var(--p-primary-color)]">General</span>
|
||||
<span class="text-[var(--p-text-muted-color)]">Advanced</span>
|
||||
</div>
|
||||
</template>
|
||||
<p class="text-sm">Tab strip rendered between header and body.</p>
|
||||
</AppDialog>
|
||||
`,
|
||||
}),
|
||||
render: dialogStory(
|
||||
`<template #tabs>
|
||||
<div class="flex gap-4 px-6 py-2 border-b border-[var(--p-content-border-color)] text-[13px]">
|
||||
<span class="font-semibold text-[var(--p-primary-color)]">General</span>
|
||||
<span class="text-[var(--p-text-muted-color)]">Advanced</span>
|
||||
</div>
|
||||
</template>
|
||||
<p class="text-sm">Tab strip rendered between header and body.</p>`,
|
||||
),
|
||||
}
|
||||
|
||||
export const Wide: Story = {
|
||||
args: { open: true, title: 'Wide dialog', width: '960px' },
|
||||
render: args => ({
|
||||
components: { AppDialog },
|
||||
setup() {
|
||||
const open = ref(args.open)
|
||||
|
||||
return { args, open }
|
||||
},
|
||||
template: `
|
||||
<AppDialog v-model:open="open" :title="args.title" :sub="args.sub" :width="args.width">
|
||||
<p class="text-sm">This dialog uses an explicit width of 960px.</p>
|
||||
</AppDialog>
|
||||
`,
|
||||
}),
|
||||
render: dialogStory('<p class="text-sm">This dialog uses an explicit width of 960px.</p>'),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { onUnmounted } from 'vue'
|
||||
import { orgA, userFixture, withPinia } from './_helpers'
|
||||
import AppTopbar from '@/components-v2/layout/AppTopbar.vue'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
@@ -10,24 +9,17 @@ import { useShellUiStore } from '@/stores/useShellUiStore'
|
||||
* currentOrganisation) and useBreadcrumb (route-driven — router is global
|
||||
* in preview.ts). Each story seeds both stores on a fresh Pinia.
|
||||
*/
|
||||
function seedAuth(): void {
|
||||
const auth = useAuthStore()
|
||||
|
||||
auth.user = userFixture
|
||||
auth.organisations = [orgA]
|
||||
}
|
||||
|
||||
const meta: Meta<typeof AppTopbar> = {
|
||||
title: 'v2 Shell/AppTopbar',
|
||||
component: AppTopbar,
|
||||
tags: ['autodocs'],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof AppTopbar>
|
||||
|
||||
export const Default: Story = {
|
||||
decorators: [
|
||||
withPinia(() => {
|
||||
const auth = useAuthStore()
|
||||
|
||||
auth.user = userFixture
|
||||
auth.organisations = [orgA]
|
||||
}),
|
||||
],
|
||||
render: () => ({
|
||||
components: { AppTopbar },
|
||||
template: `
|
||||
@@ -38,29 +30,30 @@ export const Default: Story = {
|
||||
}),
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof AppTopbar>
|
||||
|
||||
export const Default: Story = {
|
||||
decorators: [withPinia(seedAuth)],
|
||||
}
|
||||
|
||||
/**
|
||||
* Dark mode is scoped to the story's own subtree via a `.dark` wrapper
|
||||
* (Aura darkModeSelector is the `.dark` class — see plugins/primevue).
|
||||
* Mutating <html> instead would leak into every other story stacked on
|
||||
* the same autodocs page.
|
||||
*/
|
||||
export const DarkTheme: Story = {
|
||||
decorators: [
|
||||
withPinia(() => {
|
||||
const auth = useAuthStore()
|
||||
|
||||
auth.user = userFixture
|
||||
auth.organisations = [orgA]
|
||||
|
||||
const shellUi = useShellUiStore()
|
||||
|
||||
shellUi.setTheme('dark')
|
||||
seedAuth()
|
||||
useShellUiStore().setTheme('dark')
|
||||
}),
|
||||
],
|
||||
render: () => ({
|
||||
components: { AppTopbar },
|
||||
setup() {
|
||||
document.documentElement.classList.add('dark')
|
||||
onUnmounted(() => {
|
||||
document.documentElement.classList.remove('dark')
|
||||
})
|
||||
},
|
||||
template: `
|
||||
<div class="min-h-[200px]">
|
||||
<div class="dark min-h-[200px] bg-[var(--p-content-background)]">
|
||||
<AppTopbar />
|
||||
</div>
|
||||
`,
|
||||
@@ -70,22 +63,8 @@ export const DarkTheme: Story = {
|
||||
export const CompactDensity: Story = {
|
||||
decorators: [
|
||||
withPinia(() => {
|
||||
const auth = useAuthStore()
|
||||
|
||||
auth.user = userFixture
|
||||
auth.organisations = [orgA]
|
||||
|
||||
const shellUi = useShellUiStore()
|
||||
|
||||
shellUi.setDensity('compact')
|
||||
seedAuth()
|
||||
useShellUiStore().setDensity('compact')
|
||||
}),
|
||||
],
|
||||
render: () => ({
|
||||
components: { AppTopbar },
|
||||
template: `
|
||||
<div class="min-h-[200px]">
|
||||
<AppTopbar />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -15,6 +15,19 @@ const meta: Meta<typeof WorkspaceSwitcher> = {
|
||||
argTypes: {
|
||||
collapsed: { control: 'boolean' },
|
||||
},
|
||||
render: args => ({
|
||||
components: { WorkspaceSwitcher },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
|
||||
// Rail width tracks collapsed so both states read correctly.
|
||||
template: `
|
||||
<div class="bg-[var(--p-content-background)]" :class="args.collapsed ? 'w-16' : 'w-64'">
|
||||
<WorkspaceSwitcher :collapsed="args.collapsed" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export default meta
|
||||
@@ -29,17 +42,6 @@ export const SingleOrg: Story = {
|
||||
auth.organisations = [orgA]
|
||||
}),
|
||||
],
|
||||
render: args => ({
|
||||
components: { WorkspaceSwitcher },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<div class="w-64 bg-[var(--p-content-background)]">
|
||||
<WorkspaceSwitcher :collapsed="args.collapsed" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const MultiOrg: Story = {
|
||||
@@ -51,17 +53,6 @@ export const MultiOrg: Story = {
|
||||
auth.organisations = [orgA, orgB, orgC]
|
||||
}),
|
||||
],
|
||||
render: args => ({
|
||||
components: { WorkspaceSwitcher },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<div class="w-64 bg-[var(--p-content-background)]">
|
||||
<WorkspaceSwitcher :collapsed="args.collapsed" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Collapsed: Story = {
|
||||
@@ -74,15 +65,4 @@ export const Collapsed: Story = {
|
||||
auth.organisations = [orgA, orgB, orgC]
|
||||
}),
|
||||
],
|
||||
render: args => ({
|
||||
components: { WorkspaceSwitcher },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<div class="w-16 bg-[var(--p-content-background)]">
|
||||
<WorkspaceSwitcher :collapsed="args.collapsed" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user