feat(frontend): make Storybook a validated, fully covered component workbench

Storybook existed only as an undocumented local tool: 9 of 24 reusable components had stories, autodocs pages were bare prop tables, nothing built or tested the stories, and no contributor doc mentioned the workbench existed.

Every reusable component under src/components/ now has a co-located story with enriched autodocs (component descriptions plus per-prop argTypes, kept as string metadata since the repo bans line comments). Stories double as headless Chromium tests through the Storybook vitest addon, with axe accessibility checks enforced as errors and play-function interaction tests covering the modals, the RHF field bridge, the config block, and the select-all buttons. The preview now mirrors the panel's real theme DOM (body class, shared AntD theme config, seeded theme storage) so what stories render matches production.

CI and make verify gain a static Storybook build as a compile gate, and the frontend test job installs Chromium so story tests run on every PR. Contributor docs (frontend README, CONTRIBUTING, agent guides) document the workbench, the story conventions, and the Controls setup. Node engines move to 24 LTS and gen:api drops the type-stripping flags that Node 24 makes default.
This commit is contained in:
MHSanaei
2026-07-14 03:37:21 +02:00
parent 4e928a1ce0
commit 7078abc14a
36 changed files with 2315 additions and 222 deletions
@@ -1,5 +1,6 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, waitFor, within } from 'storybook/test';
import { Button } from 'antd';
import PromptModal from './PromptModal';
@@ -8,6 +9,25 @@ const meta = {
title: 'Feedback/PromptModal',
component: PromptModal,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'Modal that prompts for a single value — a plain input, a multi-line textarea, or a JSON editor. Confirms on Enter (input) or Ctrl+S (textarea) and returns the entered text.',
},
},
},
argTypes: {
open: { description: 'Whether the modal is visible.' },
title: { description: 'Modal title text.' },
okText: { description: 'Confirm button label; defaults to the translated "confirm".' },
type: { description: 'Editor variant: single-line `input` or multi-line `textarea`.' },
initialValue: { description: 'Value pre-filled when the modal opens.' },
loading: { description: 'Shows a loading state on the confirm button.' },
json: { description: 'Render a JSON editor instead of a plain field.' },
onConfirm: { description: 'Called with the entered value on confirm.' },
onClose: { description: 'Called when the modal is dismissed.' },
},
} satisfies Meta<typeof PromptModal>;
export default meta;
@@ -62,9 +82,25 @@ const placeholderArgs = {
export const Input: Story = {
args: placeholderArgs,
render: () => <InputDemo />,
play: async ({ canvas, canvasElement, userEvent }) => {
const body = within(canvasElement.ownerDocument.body);
await userEvent.click(canvas.getByRole('button', { name: 'Rename client' }));
const input = await body.findByRole('textbox');
await userEvent.type(input, 'new-name');
await userEvent.keyboard('{Enter}');
await expect(await canvas.findByText(/Last confirmed: new-name/)).toBeVisible();
},
};
export const Textarea: Story = {
args: placeholderArgs,
render: () => <TextareaDemo />,
play: async ({ canvas, canvasElement, userEvent }) => {
const body = within(canvasElement.ownerDocument.body);
await userEvent.click(canvas.getByRole('button', { name: 'Edit note' }));
const textarea = await body.findByRole('textbox');
await expect(textarea).toHaveValue('line one\nline two');
await userEvent.click(body.getByRole('button', { name: 'Confirm' }));
await waitFor(() => expect(body.queryByRole('dialog')).not.toBeInTheDocument());
},
};
@@ -1,5 +1,6 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, waitFor, within } from 'storybook/test';
import { Button } from 'antd';
import TextModal from './TextModal';
@@ -8,6 +9,23 @@ const meta = {
title: 'Feedback/TextModal',
component: TextModal,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'Read-only modal for viewing generated text or JSON — a client config, subscription, or exported settings — with copy and optional download actions, plus optional tabs for multiple documents.',
},
},
},
argTypes: {
open: { description: 'Whether the modal is visible.' },
title: { description: 'Modal title text.' },
content: { description: 'Text shown when no `tabs` are provided.' },
fileName: { description: 'When set, adds a download button that saves the active content under this name.' },
json: { description: 'Render the content in a read-only JSON editor with syntax highlighting.' },
tabs: { description: 'Optional list of `{ key, label, content }` documents shown as tabs.' },
onClose: { description: 'Called when the modal is dismissed.' },
},
} satisfies Meta<typeof TextModal>;
export default meta;
@@ -43,6 +61,13 @@ const placeholderArgs = {
export const PlainText: Story = {
args: placeholderArgs,
render: () => <Demo fileName="client.txt" />,
play: async ({ canvas, canvasElement, userEvent }) => {
const body = within(canvasElement.ownerDocument.body);
await userEvent.click(canvas.getByRole('button', { name: 'Show config' }));
const content = await body.findByDisplayValue(/vless:\/\/uuid@example\.com/);
await waitFor(() => expect(content).toBeVisible());
await expect(body.getByRole('button', { name: /client\.txt/ })).toBeEnabled();
},
};
export const Json: Story = {