Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5f73105
init
sacrosanctic Feb 9, 2026
b55d819
modify init
sacrosanctic Feb 9, 2026
ad5c030
nuxt+storybook integration workaround
sacrosanctic Feb 9, 2026
2c85494
stub nuxt modules
sacrosanctic Feb 10, 2026
c09c374
add toolbar app settings
sacrosanctic Feb 10, 2026
7f67e4c
add test dependency
sacrosanctic Feb 9, 2026
7ce2dde
add story: og images
sacrosanctic Feb 9, 2026
9412300
add story: license
sacrosanctic Feb 9, 2026
439f5c9
add story: `DownloadAnalytics`
sacrosanctic Feb 9, 2026
f2fa20f
add story: input
sacrosanctic Feb 9, 2026
bece123
Merge pull request #2 from sacrosanctic/improved-storybook-setup
sacrosanctic Feb 10, 2026
9545569
README
sacrosanctic Feb 10, 2026
e0b4a22
nit
sacrosanctic Feb 12, 2026
812c3f5
keep 2 stories
sacrosanctic Feb 12, 2026
6fd1da6
clean up story
sacrosanctic Feb 12, 2026
0fd5e03
move md to docsite
sacrosanctic Feb 12, 2026
49c8ff3
add Chromatic config, but disabled until Storybook builds properly
JReinhold Feb 13, 2026
88162a5
update md
sacrosanctic Feb 13, 2026
9d38bde
add @storybook-vue/nuxt to ignored dependencies in knip
JReinhold Feb 13, 2026
16cd8f5
Merge branch 'storybook' of github.com:sacrosanctic/npmx.dev into sto…
JReinhold Feb 13, 2026
f7d9e2b
readme
sacrosanctic Feb 13, 2026
596dde2
.
sacrosanctic Feb 14, 2026
5704e5e
.
sacrosanctic Feb 14, 2026
477859e
.
sacrosanctic Feb 14, 2026
d206283
.
sacrosanctic Feb 14, 2026
80c0e03
Merge remote-tracking branch 'upstream/main' into storybook
sacrosanctic Feb 14, 2026
feba078
Merge remote-tracking branch 'upstream/main' into storybook
sacrosanctic Feb 14, 2026
c3975f4
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 14, 2026
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
50 changes: 50 additions & 0 deletions .github/workflows/chromatic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: chromatic

on:
# Temporarily disabled until we can get Storybook to build properly.
# push:
# branches:
# - main
# pull_request:
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true

permissions:
contents: read

jobs:
chromatic:
name: 📚 Chromatic
runs-on: ubuntu-24.04-arm
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}

steps:
- name: ☑️ Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.ref || github.ref }}

- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: lts/*

- uses: pnpm/action-setup@1e1c8eafbd745f64b1ef30a7d7ed7965034c486c # 1e1c8eafbd745f64b1ef30a7d7ed7965034c486c
name: 🟧 Install pnpm
with:
cache: true

- name: 📦 Install dependencies
run: pnpm install

- name: 🧪 Run Chromatic Visual Tests
uses: chromaui/action@a8ce9c58f59be5cc7090cadfc8f130fb08fcf0c3 # v15.1.0
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
env:
CHROMATIC_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
CHROMATIC_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
CHROMATIC_SLUG: ${{ github.repository }}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,9 @@ file-tree-sprite.svg

# output
.vercel

# Storybook
*storybook.log
storybook-static

.nvmrc
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)'],
addons: ['@storybook/addon-a11y', '@storybook/addon-docs'],
framework: '@storybook-vue/nuxt',
features: {
backgrounds: false,
},
} satisfies StorybookConfig
export default config
111 changes: 111 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { Preview } from '@nuxtjs/storybook'
import { currentLocales } from '../config/i18n'
import { fn } from 'storybook/test'
import { ACCENT_COLORS } from '../shared/utils/constants'

// related: https://github.com/npmx-dev/npmx.dev/blob/1431d24be555bca5e1ae6264434d49ca15173c43/test/nuxt/setup.ts#L12-L26
// Stub Nuxt specific globals
// @ts-expect-error - dynamic global name
globalThis['__NUXT_COLOR_MODE__'] ??= {
preference: 'system',
value: 'dark',
getColorScheme: fn(() => 'dark'),
addColorScheme: fn(),
removeColorScheme: fn(),
}
// @ts-expect-error - dynamic global name
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-US',
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.charAt(0).toUpperCase() + color.slice(1),
})),
{ value: undefined, title: 'No Accent' },
],
},
},
theme: {
name: 'Theme',
description: 'Color mode',
defaultValue: 'dark',
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 {
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"
}
}
60 changes: 60 additions & 0 deletions app/components/Button/Base.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { Meta, StoryObj } from '@nuxtjs/storybook'
import Component from './Base.vue'

const meta = {
component: Component,
} satisfies Meta<typeof Component>

export default meta
type Story = StoryObj<typeof meta>

export const Primary: Story = {
args: {
default: 'Primary Button',
},
}

export const Secondary: Story = {
args: {
default: 'Secondary Button',
variant: 'secondary',
},
}

export const Small: Story = {
args: {
default: 'Small Button',
size: 'small',
variant: 'secondary',
},
}

export const Disabled: Story = {
args: {
default: 'Disabled Button',
disabled: true,
},
}

export const WithIcon: Story = {
args: {
default: 'Search',
classicon: 'i-carbon:search',
variant: 'secondary',
},
}

export const WithKeyboardShortcut: Story = {
args: {
ariaKeyshortcuts: '/',
default: 'Search',
variant: 'secondary',
},
}

export const Block: Story = {
args: {
block: true,
default: 'Full Width Button',
},
}
7 changes: 6 additions & 1 deletion app/components/Button/Base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
const props = withDefaults(
defineProps<{
disabled?: boolean
/** @default "button" */
type?: 'button' | 'submit'
/** @default "secondary" */
variant?: 'primary' | 'secondary'
/** @default "medium" */
size?: 'small' | 'medium'
/** Keyboard shortcut hint */
ariaKeyshortcuts?: string
/** Forces the button to occupy the entire width of its container. */
block?: boolean

/** Icon class (e.g., i-carbon-add) applied to the left of the text. */
classicon?: string
}>(),
{
Expand Down
79 changes: 79 additions & 0 deletions app/components/Input/Base.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { Meta, StoryObj } from '@nuxtjs/storybook'
import { expect, fn, userEvent } from 'storybook/test'
import Component from './Base.vue'

const meta = {
component: Component,
argTypes: {
disabled: { control: 'boolean' },
size: {
control: 'select',
options: ['small', 'medium', 'large'],
},
noCorrect: {
control: 'boolean',
},
onFocus: {
action: 'focus',
},
onBlur: {
action: 'blur',
},
},
} satisfies Meta<typeof Component>

export default meta
type Story = StoryObj<typeof meta>

export const Snapshot: Story = {
render: () => ({
template: `
<div style="display: flex; flex-direction: column; gap: 1rem; padding: 1rem;">
<Component size="small" model-value="Small input" />
<Component size="medium" model-value="Medium input" />
<Component size="large" model-value="Large input" />
<Component size="large" model-value="disabled" disabled />
</div>
`,
components: { Component },
}),
}

export const Event: Story = {
args: {
onFocus: fn(),
onBlur: fn(),
},
play: async ({ args, canvas }) => {
const input = canvas.getByRole('textbox')

await userEvent.click(input)
await expect(args.onFocus).toHaveBeenCalled()

await userEvent.tab()
await expect(args.onBlur).toHaveBeenCalled()
},
}

export const Disable: Story = {
args: { disabled: true },
play: async ({ canvas }) => {
const input = canvas.getByRole('textbox')

await expect(input).toBeDisabled()
},
}

export const NoCorrect: Story = {
args: {
noCorrect: true,
},
play: async ({ canvas }) => {
const input = canvas.getByRole('textbox')

await expect(input).toHaveAttribute('autocapitalize', 'off')
await expect(input).toHaveAttribute('autocorrect', 'off')
await expect(input).toHaveAttribute('autocomplete', 'off')
await expect(input).toHaveAttribute('spellcheck', 'false')
},
}
6 changes: 6 additions & 0 deletions app/components/Input/Base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ const model = defineModel<string>({ default: '' })
const props = withDefaults(
defineProps<{
disabled?: boolean
/** @default 'medium' */
size?: 'small' | 'medium' | 'large'
/**
* Prevents the browser from automatically modifying user input
* (e.g. autocorrect, autocomplete, autocapitalize, and spellcheck).
* @default true
*/
noCorrect?: boolean
}>(),
{
Expand Down
6 changes: 6 additions & 0 deletions chromatic.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"onlyChanged": true,
"autoAcceptChanges": "main",
"exitZeroOnChanges": false,
"externals": ["public/**", "app/assets/**", ".storybook/**", "nuxt.config.ts", "uno.config.ts"]
}
Loading
Loading