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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ shared/types/lexicons

# output
.vercel

*storybook.log
storybook-static

.direnv
11 changes: 11 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { StorybookConfig } from '@nuxtjs/storybook'

const config = {
stories: ['../app/**/*.stories.@(js|ts|mdx)'],
addons: ['@storybook/addon-a11y', '@storybook/addon-docs'],
framework: '@storybook-vue/nuxt',
features: {
backgrounds: false,
},
} satisfies StorybookConfig
export default config
104 changes: 104 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { Preview } from '@nuxtjs/storybook'
import { currentLocales } from '../config/i18n'
import { fn } from 'storybook/test'
import { ACCENT_COLORS } from '../shared/utils/constants'

// Stub Nuxt specific globals
globalThis['__NUXT_COLOR_MODE__'] ??= {
preference: 'system',
value: 'dark',
getColorScheme: fn(() => 'dark'),
addColorScheme: fn(),
removeColorScheme: fn(),
}
globalThis.defineOgImageComponent = fn()

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
// Provides toolbars to switch things like theming and language
globalTypes: {
locale: {
name: 'Locale',
description: 'UI language',
defaultValue: 'en',
toolbar: {
icon: 'globe',
dynamicTitle: true,
items: [
// English is at the top so it's easier to reset to it
{ value: 'en-US', title: 'English (US)' },
...currentLocales
.filter(locale => locale.code !== 'en-US')
.map(locale => ({ value: locale.code, title: locale.name })),
],
},
},
accentColor: {
name: 'Accent Color',
description: 'Accent color',
toolbar: {
icon: 'paintbrush',
dynamicTitle: true,
items: [
...Object.keys(ACCENT_COLORS.light).map(color => ({ value: color, title: color })),
{ value: 'clear', title: 'clear' },
],
},
},
theme: {
name: 'Theme',
description: 'Color mode',
toolbar: {
icon: 'moon',
dynamicTitle: true,
items: [
{ value: 'light', icon: 'sun', title: 'Light' },
{ value: 'dark', icon: 'moon', title: 'Dark' },
],
},
},
},
decorators: [
(story, context) => {
const { locale, theme, accentColor } = context.globals as {
locale: string
theme: string
accentColor?: string
}

// Set theme from globals
document.documentElement.setAttribute('data-theme', theme)

// Set accent color from globals
if (accentColor) {
document.documentElement.style.setProperty('--accent-color', `var(--swatch-${accentColor})`)
} else if (accentColor === 'clear') {
document.documentElement.style.removeProperty('--accent-color')
}

return {
template: '<story />',
// Set locale from globals
created() {
if (this.$i18n) {
this.$i18n.setLocale(locale)
}
},
updated() {
if (this.$i18n) {
this.$i18n.setLocale(locale)
}
},
}
},
],
}

export default preview
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
"editor.formatOnSave": true,
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": ["./i18n/locales"],
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"*.vue": "${capture}.stories.ts"
}
}
61 changes: 61 additions & 0 deletions app/components/AppFooter.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { Meta, StoryObj } from '@nuxtjs/storybook'
import AppFooter from './AppFooter.vue'

const meta = {
component: AppFooter,
parameters: {
layout: 'fullscreen',
},
globals: {
viewport: { value: undefined },
},
} satisfies Meta<typeof AppFooter>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {}

export const InContext: Story = {
render: () => ({
components: { AppFooter },
template: `
<div style="display: flex; flex-direction: column; min-height: 100vh;">
<div style="flex: 1; padding: 2rem;">
<h1>Some page content</h1>
<span>See footer at the bottom</span>
</div>
<AppFooter />
</div>
`,
}),
}

export const InLongPage: Story = {
render: () => ({
components: { AppFooter },
template: `
<div style="display: flex; flex-direction: column; min-height: 100vh;">
<div style="flex: 1; padding: 2rem;">
<h1>Footer is all the way at the bottom!</h1> <br />
${Array.from({ length: 50 }, (_, i) => `<p key="${i}">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>`).join('')}
</div>
<AppFooter />
</div>
`,
}),
}

export const MobileView: Story = {
...InContext,
globals: {
viewport: { value: 'mobile1' },
},
}

export const TabletView: Story = {
...InContext,
globals: {
viewport: { value: 'tablet' },
},
}
22 changes: 22 additions & 0 deletions app/components/AppHeader.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@nuxtjs/storybook'
import AppHeader from './AppHeader.vue'

const meta = {
component: AppHeader,
parameters: {
layout: 'fullscreen',
},
globals: {
viewport: { value: undefined },
},
} satisfies Meta<typeof AppHeader>

export default meta

export const Default: StoryObj<typeof AppHeader> = {}

export const Mobile: StoryObj<typeof AppHeader> = {
globals: {
viewport: { value: 'mobile1' },
},
}
112 changes: 112 additions & 0 deletions app/components/Button/Base.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { Meta, StoryObj } from '@nuxtjs/storybook'
import ButtonBase from './Base.vue'

const meta = {
component: ButtonBase,
tags: ['autodocs'],
} satisfies Meta<typeof ButtonBase>

export default meta
type Story = StoryObj<typeof meta>

export const Primary: Story = {
args: {
variant: 'primary',
size: 'medium',
},
render: args => ({
components: { ButtonBase },
setup() {
return { args }
},
template: '<ButtonBase v-bind="args">Primary Button</ButtonBase>',
}),
}

export const Secondary: Story = {
args: {
variant: 'secondary',
size: 'medium',
},
render: args => ({
components: { ButtonBase },
setup() {
return { args }
},
template: '<ButtonBase v-bind="args">Secondary Button</ButtonBase>',
}),
}

export const Small: Story = {
args: {
variant: 'secondary',
size: 'small',
},
render: args => ({
components: { ButtonBase },
setup() {
return { args }
},
template: '<ButtonBase v-bind="args">Small Button</ButtonBase>',
}),
}

export const Disabled: Story = {
args: {
variant: 'primary',
size: 'medium',
disabled: true,
},
render: args => ({
components: { ButtonBase },
setup() {
return { args }
},
template: '<ButtonBase v-bind="args">Disabled Button</ButtonBase>',
}),
}

export const WithIcon: Story = {
args: {
variant: 'secondary',
size: 'medium',
classicon: 'i-carbon:search',
},
render: args => ({
components: { ButtonBase },
setup() {
return { args }
},
template: '<ButtonBase v-bind="args">Search</ButtonBase>',
}),
}

export const WithKeyboardShortcut: Story = {
args: {
variant: 'secondary',
size: 'medium',
ariaKeyshortcuts: '/',
},
render: args => ({
components: { ButtonBase },
setup() {
return { args }
},
template: '<ButtonBase v-bind="args">Search</ButtonBase>',
}),
}

export const Block: Story = {
args: {
variant: 'primary',
size: 'medium',
block: true,
},
render: args => ({
components: { ButtonBase },
setup() {
return { args }
},
template: '<ButtonBase v-bind="args">Full Width Button</ButtonBase>',
}),
}
25 changes: 24 additions & 1 deletion app/components/Button/Base.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
<script setup lang="ts">
const props = withDefaults(
defineProps<{
/**
* Whether the button is disabled
*/
disabled?: boolean
/**
* HTML button type attribute
* @default "button"
*/
type?: 'button' | 'submit'
/**
* Button visual style variant
* @default "secondary"
*/
variant?: 'primary' | 'secondary'
/**
* Button size
* @default "medium"
*/
size?: 'small' | 'medium'
/**
* Keyboard shortcut hint
*/
ariaKeyshortcuts?: string
/**
* Whether the button should take full width
*/
block?: boolean

/**
* Icon class (e.g., i-carbon-add)
*/
classicon?: string
}>(),
{
Expand Down
Loading
Loading