chore(stories): eslint/tsconfig migration

Migrate Storybook package to eslint flat config + composite tsconfig.
This commit is contained in:
2026-06-07 16:30:05 +07:00
parent 09272dffeb
commit 23a2795523
18 changed files with 687 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules
storybook-static
*.log
+21
View File
@@ -0,0 +1,21 @@
import type { StorybookConfig } from '@storybook/vue3-vite';
const config: StorybookConfig = {
stories: ['../stories/**/*.stories.ts'],
addons: [
'@storybook/addon-docs',
'@storybook/addon-a11y',
],
framework: {
name: '@storybook/vue3-vite',
options: {},
},
docs: {
defaultName: 'Docs',
},
typescript: {
check: false,
},
};
export default config;
+110
View File
@@ -0,0 +1,110 @@
.sb-dialog-trigger,
.sb-dialog-close,
.sb-toggle {
font: inherit;
padding: 0.5rem 1rem;
border-radius: 6px;
border: 1px solid #888;
background: #fff;
cursor: pointer;
}
.sb-toggle[aria-pressed='true'] { background: #222; color: #fff; }
.sb-toggle[data-disabled] { opacity: 0.5; cursor: not-allowed; }
.sb-dialog-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4); }
.sb-dialog-content {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
padding: 1.5rem;
border-radius: 8px;
min-width: 320px;
max-width: 90vw;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
display: grid;
gap: 0.75rem;
}
.sb-dialog-title { margin: 0; font-size: 1.125rem; font-weight: 600; }
.sb-dialog-desc { margin: 0; color: #555; }
.sb-switch {
position: relative;
width: 44px;
height: 24px;
padding: 0;
border-radius: 999px;
border: 1px solid #888;
background: #ddd;
cursor: pointer;
transition: background 0.15s;
}
.sb-switch[aria-checked='true'] { background: #10b981; border-color: #10b981; }
.sb-switch-thumb {
display: block;
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
transform: translateX(2px);
transition: transform 0.15s;
}
.sb-switch[aria-checked='true'] .sb-switch-thumb { transform: translateX(22px); }
.sb-progress {
position: relative;
width: 240px;
height: 8px;
background: #eee;
border-radius: 999px;
overflow: hidden;
}
.sb-progress-ind { height: 100%; background: #3b82f6; transition: width 0.2s; }
.sb-progress[data-state='indeterminate'] .sb-progress-ind {
animation: sb-progress-indeterminate 1.2s ease-in-out infinite;
background: linear-gradient(90deg, transparent, #3b82f6, transparent);
}
@keyframes sb-progress-indeterminate {
from { transform: translateX(-100%); }
to { transform: translateX(100%); }
}
.sb-collapsible { font-family: system-ui; max-width: 320px; }
.sb-collapsible-trigger {
font: inherit;
padding: 0.5rem 0.75rem;
border-radius: 6px;
border: 1px solid #888;
background: #fff;
cursor: pointer;
width: 100%;
text-align: left;
}
.sb-collapsible-content {
padding: 0.5rem 0.75rem;
border: 1px dashed #bbb;
border-top: none;
border-radius: 0 0 6px 6px;
}
.sb-avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: 50%;
overflow: hidden;
background: #e5e7eb;
font-family: system-ui;
font-weight: 600;
color: #374151;
vertical-align: middle;
}
.sb-avatar-img { width: 100%; height: 100%; object-fit: cover; }
.sb-avatar-fallback { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
+19
View File
@@ -0,0 +1,19 @@
import type { Preview } from '@storybook/vue3-vite';
import './preview-styles.css';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
a11y: {
test: 'todo',
},
},
tags: ['autodocs'],
};
export default preview;
+9
View File
@@ -0,0 +1,9 @@
import { base, compose, imports, stylistic, typescript } from '@robonen/eslint';
export default compose(base, typescript, imports, stylistic, {
name: 'stories/overrides',
files: ['**/*.vue', '**/*.stories.ts'],
rules: {
'@stylistic/no-multiple-empty-lines': 'off',
},
});
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@robonen/stories",
"version": "0.0.1",
"private": true,
"license": "Apache-2.0",
"description": "Storybook for @robonen/primitives",
"type": "module",
"scripts": {
"lint:check": "eslint .",
"lint:fix": "eslint . --fix",
"dev": "storybook dev -p 6006 --no-open",
"build": "storybook build -o storybook-static"
},
"dependencies": {
"@robonen/primitives": "workspace:*",
"@robonen/vue": "workspace:*",
"vue": "catalog:"
},
"devDependencies": {
"@robonen/eslint": "workspace:*",
"@robonen/tsconfig": "workspace:*",
"@storybook/addon-a11y": "^10.2.1",
"@storybook/addon-docs": "^10.2.1",
"@storybook/vue3-vite": "^10.2.1",
"@vitejs/plugin-vue": "^6.0.6",
"eslint": "catalog:",
"storybook": "^10.2.1",
"vite": "^7.1.9"
}
}
@@ -0,0 +1,48 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import { AspectRatio } from '@robonen/primitives';
const meta = {
title: 'Layout/AspectRatio',
component: AspectRatio,
tags: ['autodocs'],
argTypes: {
ratio: { control: { type: 'number', min: 0.1, step: 0.1 } },
},
args: { ratio: 16 / 9 },
} satisfies Meta<typeof AspectRatio>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Widescreen: Story = {
render: args => ({
components: { AspectRatio },
setup: () => ({ args }),
template: `
<div style="width: 400px; border-radius: 8px; overflow: hidden">
<AspectRatio v-bind="args">
<img
src="https://images.unsplash.com/photo-1535025183041-0991a977e25b?w=800"
alt="landscape"
style="width:100%;height:100%;object-fit:cover"
/>
</AspectRatio>
</div>
`,
}),
};
export const Square: Story = {
args: { ratio: 1 },
render: args => ({
components: { AspectRatio },
setup: () => ({ args }),
template: `
<div style="width: 200px; background: #eee; border-radius: 8px; overflow: hidden">
<AspectRatio v-bind="args">
<div style="display:flex;align-items:center;justify-content:center;height:100%;font-family:system-ui">1 : 1</div>
</AspectRatio>
</div>
`,
}),
};
+35
View File
@@ -0,0 +1,35 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import { AvatarFallback, AvatarImage, AvatarRoot } from '@robonen/primitives';
const meta = {
title: 'Media/Avatar',
component: AvatarRoot,
tags: ['autodocs'],
} satisfies Meta<typeof AvatarRoot>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Loaded: Story = {
render: () => ({
components: { AvatarRoot, AvatarImage, AvatarFallback },
template: `
<AvatarRoot class="sb-avatar">
<AvatarImage src="https://i.pravatar.cc/96?img=5" alt="User" class="sb-avatar-img" />
<AvatarFallback class="sb-avatar-fallback" :delayMs="300">CT</AvatarFallback>
</AvatarRoot>
`,
}),
};
export const Fallback: Story = {
render: () => ({
components: { AvatarRoot, AvatarImage, AvatarFallback },
template: `
<AvatarRoot class="sb-avatar">
<AvatarImage src="https://invalid.example.com/missing.png" alt="User" class="sb-avatar-img" />
<AvatarFallback class="sb-avatar-fallback">AB</AvatarFallback>
</AvatarRoot>
`,
}),
};
@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import { CollapsibleContent, CollapsibleRoot, CollapsibleTrigger } from '@robonen/primitives';
const meta = {
title: 'Disclosure/Collapsible',
component: CollapsibleRoot,
tags: ['autodocs'],
args: { defaultOpen: false, disabled: false },
} satisfies Meta<typeof CollapsibleRoot>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: args => ({
components: { CollapsibleRoot, CollapsibleTrigger, CollapsibleContent },
setup: () => ({ args }),
template: `
<CollapsibleRoot v-bind="args" class="sb-collapsible">
<CollapsibleTrigger class="sb-collapsible-trigger">
<template #default="{ open }">{{ open ? 'Hide' : 'Show' }} details</template>
</CollapsibleTrigger>
<CollapsibleContent class="sb-collapsible-content">
<p style="margin:0.5rem 0 0">Hidden content revealed when the trigger is activated.</p>
</CollapsibleContent>
</CollapsibleRoot>
`,
}),
};
export const OpenByDefault: Story = { args: { defaultOpen: true }, render: Default.render };
export const Disabled: Story = { args: { disabled: true }, render: Default.render };
+94
View File
@@ -0,0 +1,94 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import {
DialogClose,
DialogContent,
DialogDescription,
DialogOverlay,
DialogPortal,
DialogRoot,
DialogTitle,
DialogTrigger,
} from '@robonen/primitives';
const meta = {
title: 'Overlays/Dialog',
component: DialogRoot,
tags: ['autodocs'],
argTypes: {
modal: { control: 'boolean' },
defaultOpen: { control: 'boolean' },
},
args: {
modal: true,
defaultOpen: false,
},
} satisfies Meta<typeof DialogRoot>;
export default meta;
type Story = StoryObj<typeof meta>;
const render = (args: Record<string, unknown>) => ({
components: {
DialogRoot,
DialogTrigger,
DialogPortal,
DialogOverlay,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
},
setup: () => ({ args }),
template: `
<DialogRoot v-bind="args">
<DialogTrigger class="sb-dialog-trigger">Open Dialog</DialogTrigger>
<DialogPortal>
<DialogOverlay class="sb-dialog-overlay" />
<DialogContent class="sb-dialog-content">
<DialogTitle class="sb-dialog-title">Dialog Title</DialogTitle>
<DialogDescription class="sb-dialog-desc">
Traps focus, locks scroll, and dismisses on Escape or outside click.
</DialogDescription>
<DialogClose class="sb-dialog-close">Close</DialogClose>
</DialogContent>
</DialogPortal>
</DialogRoot>
`,
});
export const Default: Story = { render };
export const OpenByDefault: Story = {
args: { defaultOpen: true },
render,
};
export const NonModal: Story = {
args: { modal: false },
render: args => ({
components: {
DialogRoot,
DialogTrigger,
DialogPortal,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
},
setup: () => ({ args }),
template: `
<DialogRoot v-bind="args">
<DialogTrigger class="sb-dialog-trigger">Open non-modal</DialogTrigger>
<DialogPortal>
<DialogContent class="sb-dialog-content">
<DialogTitle class="sb-dialog-title">Non-modal dialog</DialogTitle>
<DialogDescription class="sb-dialog-desc">
No overlay, no scroll lock, no focus trap.
</DialogDescription>
<DialogClose class="sb-dialog-close">Close</DialogClose>
</DialogContent>
</DialogPortal>
</DialogRoot>
`,
}),
};
+37
View File
@@ -0,0 +1,37 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import { Label } from '@robonen/primitives';
const meta = {
title: 'Forms/Label',
component: Label,
tags: ['autodocs'],
} satisfies Meta<typeof Label>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => ({
components: { Label },
template: `
<div style="font-family: system-ui; display: grid; gap: 0.25rem; max-width: 300px">
<Label for="email">Email address</Label>
<input id="email" type="email" placeholder="you@example.com" style="padding:0.5rem;border:1px solid #888;border-radius:4px" />
</div>
`,
}),
};
export const WithCheckbox: Story = {
render: () => ({
components: { Label },
template: `
<div style="font-family: system-ui">
<Label style="display:flex;align-items:center;gap:0.5rem;cursor:pointer">
<input type="checkbox" />
Subscribe to the newsletter
</Label>
</div>
`,
}),
};
+43
View File
@@ -0,0 +1,43 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import { ProgressIndicator, ProgressRoot } from '@robonen/primitives';
const meta = {
title: 'Feedback/Progress',
component: ProgressRoot,
tags: ['autodocs'],
argTypes: {
modelValue: { control: { type: 'number', min: 0, max: 100, step: 1 } },
max: { control: { type: 'number', min: 1 } },
},
args: { modelValue: 40, max: 100 },
} satisfies Meta<typeof ProgressRoot>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Determinate: Story = {
render: args => ({
components: { ProgressRoot, ProgressIndicator },
setup: () => ({ args }),
template: `
<ProgressRoot v-bind="args" class="sb-progress">
<template #default="{ value, max }">
<ProgressIndicator
class="sb-progress-ind"
:style="{ width: value == null ? '100%' : (value / max * 100) + '%' }"
/>
</template>
</ProgressRoot>
`,
}),
};
export const Indeterminate: Story = {
args: { modelValue: null },
render: Determinate.render,
};
export const Complete: Story = {
args: { modelValue: 100 },
render: Determinate.render,
};
+54
View File
@@ -0,0 +1,54 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import { Separator } from '@robonen/primitives';
const meta = {
title: 'Layout/Separator',
component: Separator,
tags: ['autodocs'],
argTypes: {
orientation: { control: 'radio', options: ['horizontal', 'vertical'] },
decorative: { control: 'boolean' },
},
args: { orientation: 'horizontal', decorative: false },
} satisfies Meta<typeof Separator>;
export default meta;
type Story = StoryObj<typeof meta>;
const baseStyle = `
background: #888;
display: block;
`;
const horizontal = `${baseStyle} height: 1px; width: 100%;`;
const vertical = `${baseStyle} height: 24px; width: 1px; display: inline-block; margin: 0 0.75rem;`;
export const Horizontal: Story = {
render: args => ({
components: { Separator },
setup: () => ({ args, horizontal }),
template: `
<div style="max-width: 320px; font-family: system-ui">
<p style="margin:0 0 0.5rem">Section one</p>
<Separator v-bind="args" :style="horizontal" />
<p style="margin:0.5rem 0 0">Section two</p>
</div>
`,
}),
};
export const Vertical: Story = {
args: { orientation: 'vertical' },
render: args => ({
components: { Separator },
setup: () => ({ args, vertical }),
template: `
<nav style="font-family: system-ui">
<a href="#">Home</a>
<Separator v-bind="args" :style="vertical" />
<a href="#">About</a>
<Separator v-bind="args" :style="vertical" />
<a href="#">Contact</a>
</nav>
`,
}),
};
+94
View File
@@ -0,0 +1,94 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import { Label, Switch } from '@robonen/primitives';
import { ref } from 'vue';
const meta = {
title: 'Forms/Switch',
component: Switch,
tags: ['autodocs'],
} satisfies Meta<typeof Switch>;
export default meta;
type Story = StoryObj<typeof meta>;
export const BooleanDefault: Story = {
name: 'Boolean (default)',
render: () => ({
components: { Switch, Label },
template: `
<div style="display:flex;align-items:center;gap:0.5rem;font-family:system-ui">
<Switch id="airplane" class="sb-switch">
<span class="sb-switch-thumb" />
</Switch>
<Label for="airplane">Airplane mode</Label>
</div>
`,
}),
};
export const StringPair: Story = {
name: 'String pair ("on" / "off")',
render: () => ({
components: { Switch, Label },
setup() {
const value = ref<'on' | 'off'>('off');
return { value };
},
template: `
<div style="display:flex;align-items:center;gap:0.5rem;font-family:system-ui">
<Switch
v-model="value"
truthy="on"
falsy="off"
id="mode"
class="sb-switch"
>
<span class="sb-switch-thumb" />
</Switch>
<Label for="mode">Mode: {{ value }}</Label>
</div>
`,
}),
};
export const ObjectPair: Story = {
name: 'Object pair (generic)',
render: () => ({
components: { Switch, Label },
setup() {
const LIGHT = { theme: 'light' as const };
const DARK = { theme: 'dark' as const };
const value = ref<typeof LIGHT | typeof DARK>(LIGHT);
return { value, LIGHT, DARK };
},
template: `
<div style="display:flex;align-items:center;gap:0.5rem;font-family:system-ui">
<Switch
v-model="value"
:truthy="DARK"
:falsy="LIGHT"
id="theme"
class="sb-switch"
>
<span class="sb-switch-thumb" />
</Switch>
<Label for="theme">Theme: {{ value.theme }}</Label>
</div>
`,
}),
};
export const Disabled: Story = {
name: 'Disabled',
render: () => ({
components: { Switch, Label },
template: `
<div style="display:flex;align-items:center;gap:0.5rem;font-family:system-ui">
<Switch id="disabled-sw" class="sb-switch" disabled :default-value="true">
<span class="sb-switch-thumb" />
</Switch>
<Label for="disabled-sw">Disabled (checked)</Label>
</div>
`,
}),
};
+33
View File
@@ -0,0 +1,33 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import { Toggle } from '@robonen/primitives';
const meta = {
title: 'Forms/Toggle',
component: Toggle,
tags: ['autodocs'],
argTypes: { disabled: { control: 'boolean' }, defaultPressed: { control: 'boolean' } },
args: { disabled: false, defaultPressed: false },
} satisfies Meta<typeof Toggle>;
export default meta;
type Story = StoryObj<typeof meta>;
const template = `
<Toggle v-bind="args" class="sb-toggle">
<template #default="{ pressed }">{{ pressed ? 'Bold ●' : 'Bold' }}</template>
</Toggle>
`;
export const Default: Story = {
render: args => ({ components: { Toggle }, setup: () => ({ args }), template }),
};
export const Pressed: Story = {
args: { defaultPressed: true },
render: args => ({ components: { Toggle }, setup: () => ({ args }), template }),
};
export const Disabled: Story = {
args: { disabled: true },
render: args => ({ components: { Toggle }, setup: () => ({ args }), template }),
};
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.src.json" },
{ "path": "./tsconfig.node.json" }
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@robonen/tsconfig/tsconfig.node.json",
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
},
"include": ["*.config.ts", ".storybook/**/*.ts"]
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "@robonen/tsconfig/tsconfig.vue.json",
"compilerOptions": {
"composite": true,
"types": ["vite/client"],
"allowImportingTsExtensions": false,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.src.tsbuildinfo"
},
"include": ["stories/**/*.ts", "stories/**/*.vue"]
}