feat(client): add ThemeSwitcher and relocate stories to .storybook/
- add lucide-react for Sun/Moon icons - add useTheme hook persisting choice to localStorage with prefers-color-scheme fallback - add ThemeSwitcher button component wired into SidePanel header - add blocking inline script to index.html to prevent flash-of-wrong-theme - move all *.stories.tsx from src/ into .storybook/stories/ and update imports - update main.ts stories glob to .storybook/stories/**
This commit is contained in:
@@ -13,8 +13,8 @@ function getAbsolutePath(value: string) {
|
||||
}
|
||||
const config: StorybookConfig = {
|
||||
"stories": [
|
||||
"../src/**/*.mdx",
|
||||
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||
"./stories/**/*.mdx",
|
||||
"./stories/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||
],
|
||||
"addons": [
|
||||
getAbsolutePath('@storybook/addon-vitest'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { Badge } from './Badge';
|
||||
import { Badge } from '../../src/ui/Badge/Badge';
|
||||
|
||||
const meta: Meta<typeof Badge> = {
|
||||
title: 'UI/Badge',
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { Breadcrumbs } from './Breadcrumbs';
|
||||
import { Breadcrumbs } from '../../src/ui/Breadcrumbs/Breadcrumbs';
|
||||
|
||||
const meta: Meta<typeof Breadcrumbs> = {
|
||||
title: 'UI/Breadcrumbs',
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { Button } from './Button';
|
||||
import { Button } from '../../src/ui/Button/Button';
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: 'UI/Button',
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { Card } from './Card';
|
||||
import { Badge } from '../Badge/Badge';
|
||||
import { Button } from '../Button/Button';
|
||||
import { Card } from '../../src/ui/Card/Card';
|
||||
import { Badge } from '../../src/ui/Badge/Badge';
|
||||
import { Button } from '../../src/ui/Button/Button';
|
||||
|
||||
const meta: Meta<typeof Card> = {
|
||||
title: 'UI/Card',
|
||||
@@ -32,7 +32,7 @@ const surfaceColors = [
|
||||
{ name: 'Primary fg', token: '--color-primary-fg', hex: '#2D3339' },
|
||||
];
|
||||
|
||||
function Swatch({ name, token, hex, role }: { name: string; token: string; hex: string; role?: string }) {;
|
||||
function Swatch({ name, token, hex, role }: { name: string; token: string; hex: string; role?: string }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { Input } from './Input';
|
||||
import { Input } from '../../src/ui/Input/Input';
|
||||
|
||||
const meta: Meta<typeof Input> = {
|
||||
title: 'UI/Input',
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { Select } from './Select';
|
||||
import { Select } from '../../src/ui/Select/Select';
|
||||
|
||||
const ENVIRONMENTS = [
|
||||
{ value: 'dev', label: 'Development' },
|
||||
+1
-25
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { SidePanel } from './SidePanel';
|
||||
import { SidePanel } from '../../src/ui/SidePanel/SidePanel';
|
||||
|
||||
const NAV_ITEMS = ['Dashboard', 'Scenarios', 'Environments', 'Reports', 'Settings'];
|
||||
|
||||
@@ -57,27 +57,3 @@ export const Default: Story = {
|
||||
children: <NavList />,
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultCollapsed: Story = {
|
||||
args: {
|
||||
header: 'Navigation',
|
||||
children: <NavList />,
|
||||
defaultCollapsed: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const NarrowWidth: Story = {
|
||||
args: {
|
||||
header: 'Filters',
|
||||
width: 180,
|
||||
children: <NavList />,
|
||||
},
|
||||
};
|
||||
|
||||
export const WideWidth: Story = {
|
||||
args: {
|
||||
header: 'Scenarios',
|
||||
width: 320,
|
||||
children: <NavList />,
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Table } from './Table';
|
||||
import { Badge } from '../Badge/Badge';
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { Table } from '../../src/ui/Table/Table';
|
||||
import { Badge } from '../../src/ui/Badge/Badge';
|
||||
|
||||
const meta: Meta<typeof Table> = {
|
||||
title: 'UI/Table',
|
||||
@@ -10,8 +10,6 @@ const meta: Meta<typeof Table> = {
|
||||
};
|
||||
export default meta;
|
||||
|
||||
// ── Simple string rows ────────────────────────────────────────────────────────
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { ThemeSwitcher } from '../../src/ui/ThemeSwitcher/ThemeSwitcher';
|
||||
|
||||
const meta: Meta<typeof ThemeSwitcher> = {
|
||||
title: 'UI/ThemeSwitcher',
|
||||
component: ThemeSwitcher,
|
||||
parameters: { layout: 'centered' },
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ThemeSwitcher>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@@ -4,6 +4,15 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Liquio QA Bot</title>
|
||||
<script>
|
||||
(function () {
|
||||
var stored = localStorage.getItem('qa-bot-theme');
|
||||
var theme = stored === 'light' || stored === 'dark'
|
||||
? stored
|
||||
: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^1.7.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
|
||||
+7
-2
@@ -1,5 +1,5 @@
|
||||
import styles from './App.module.css';
|
||||
import { SidePanel } from './ui';
|
||||
import { SidePanel, ThemeSwitcher } from './ui';
|
||||
import { EnvironmentsPage } from './pages/EnvironmentsPage';
|
||||
import { KeysPage } from './pages/KeysPage';
|
||||
import { SessionsPage } from './pages/SessionsPage';
|
||||
@@ -21,7 +21,12 @@ export default function App() {
|
||||
return (
|
||||
<div className={styles.shell}>
|
||||
<SidePanel
|
||||
header={<span className={styles.brand}>QA Bot</span>}
|
||||
header={
|
||||
<div className={styles.header}>
|
||||
<span className={styles.brand}>QA Bot</span>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
}
|
||||
width={220}
|
||||
>
|
||||
<nav className={styles.nav}>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
type Theme = 'light' | 'dark';
|
||||
|
||||
const STORAGE_KEY = 'qa-bot-theme';
|
||||
|
||||
function getInitialTheme(): Theme {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored === 'light' || stored === 'dark') return stored;
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const [theme, setTheme] = useState<Theme>(getInitialTheme);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem(STORAGE_KEY, theme);
|
||||
}, [theme]);
|
||||
|
||||
function toggleTheme() {
|
||||
setTheme(t => (t === 'light' ? 'dark' : 'light'));
|
||||
}
|
||||
|
||||
return { theme, toggleTheme } as const;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: var(--border-width) solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: background var(--transition), color var(--transition), border-color var(--transition);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: var(--color-bg-subtle);
|
||||
color: var(--color-text);
|
||||
border-color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-secondary-fg);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Sun, Moon } from 'lucide-react';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import styles from './ThemeSwitcher.module.css';
|
||||
|
||||
export function ThemeSwitcher() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<button
|
||||
className={styles.btn}
|
||||
onClick={toggleTheme}
|
||||
aria-label={theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
|
||||
title={theme === 'dark' ? 'Light mode' : 'Dark mode'}
|
||||
>
|
||||
{theme === 'dark' ? <Sun size={16} /> : <Moon size={16} />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -19,5 +19,7 @@ export type { SidePanelProps } from './SidePanel/SidePanel';
|
||||
export { Breadcrumbs } from './Breadcrumbs/Breadcrumbs';
|
||||
export type { BreadcrumbsProps, BreadcrumbItem } from './Breadcrumbs/Breadcrumbs';
|
||||
|
||||
export { ThemeSwitcher } from './ThemeSwitcher/ThemeSwitcher';
|
||||
|
||||
export { Table } from './Table/Table';
|
||||
export type { TableProps, TableColumn } from './Table/Table';
|
||||
|
||||
Generated
+10
@@ -16,6 +16,7 @@
|
||||
"name": "liquio-qa-bot-client",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"lucide-react": "^1.7.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
@@ -9798,6 +9799,15 @@
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz",
|
||||
"integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
|
||||
|
||||
Reference in New Issue
Block a user