diff --git a/.storybook/main.ts b/.storybook/main.ts
index 1bed9c0..4239a54 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -5,6 +5,7 @@ const config: StorybookConfig = {
'../stories/**/*.mdx',
'../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
+ addons: ['@storybook/addon-a11y'],
framework: {
name: '@storybook/nextjs',
options: {},
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index bc5e780..8bac484 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -1,10 +1,38 @@
import React, { useEffect } from 'react'
import type { Preview } from '@storybook/react'
import '../app/globals.css'
-import { ThemeProvider } from '../components/theme/ThemeProvider'
+import {
+ ThemeProvider,
+ type ThemePreference,
+} from '../components/theme/ThemeProvider'
+
+/**
+ * Storybook ツールバーの `theme` グローバルに応じて preview ドキュメントに
+ * `dark` クラスを適用し、ThemeProvider でラップする。実アプリでは SSR の
+ * layout が に初期クラスを設定するが、Storybook に SSR は無いため
+ * ここで設定する。
+ */
+function WithTheme({
+ theme,
+ children,
+}: {
+ theme: ThemePreference
+ children: React.ReactNode
+}) {
+ useEffect(() => {
+ document.documentElement.classList.toggle('dark', theme === 'dark')
+ }, [theme])
+
+ // key でツールバー切替時に remount し、Provider の state を初期化し直す
+ return (
+
+ {children}
+
+ )
+}
const globalDecorators: Preview['decorators'] = [
- (Story) => {
+ (Story, context) => {
useEffect(() => {
const handler = (e: MouseEvent) => {
const anchor = (e.target as HTMLElement).closest('a[href]')
@@ -20,10 +48,13 @@ const globalDecorators: Preview['decorators'] = [
document.addEventListener('click', handler, true)
return () => document.removeEventListener('click', handler, true)
}, [])
+
+ const theme: ThemePreference =
+ context.globals.theme === 'dark' ? 'dark' : 'light'
return (
-
+
-
+
)
},
]
@@ -43,6 +74,25 @@ const preview: Preview = {
pathname: '/',
},
},
+ a11y: {
+ // a11y 違反は UI 上に表示するだけで、まだ実行は失敗させない
+ test: 'todo',
+ },
+ },
+ globalTypes: {
+ theme: {
+ description: 'テーマ',
+ defaultValue: 'light',
+ toolbar: {
+ title: 'Theme',
+ icon: 'sun',
+ items: [
+ { value: 'light', title: 'Light', icon: 'sun' },
+ { value: 'dark', title: 'Dark', icon: 'moon' },
+ ],
+ dynamicTitle: true,
+ },
+ },
},
}
diff --git a/app/globals.css b/app/globals.css
index b3415a2..f94d9fa 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,5 +1,7 @@
@import 'tailwindcss';
+@custom-variant dark (&:where(.dark, .dark *));
+
@layer base {
*,
::after,
@@ -14,65 +16,100 @@
color: var(--color-foreground);
}
+ input[type='number'] {
+ -moz-appearance: textfield;
+ appearance: textfield;
+ }
+
+ input[type='number']::-webkit-inner-spin-button,
+ input[type='number']::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ appearance: none;
+ }
+
+ /*
+ * shadcn/ui "new-york" / zinc base color (Tailwind v4 CSS variables).
+ * Extended with success / warning / info and table-header tokens.
+ * --ring is set to the primary color so focus rings keep a >= 3:1 contrast
+ * ratio (WCAG 2.4.11) instead of the low-contrast neutral default.
+ */
:root {
+ color-scheme: light;
--background: oklch(1 0 0);
- --foreground: oklch(0.145 0.004 285.823);
+ --foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
- --card-foreground: oklch(0.145 0.004 285.823);
+ --card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
- --popover-foreground: oklch(0.145 0.004 285.823);
- --primary: oklch(0.205 0.042 265.755);
- --primary-foreground: oklch(0.985 0.002 285.823);
- --secondary: oklch(0.97 0.001 285.823);
- --secondary-foreground: oklch(0.205 0.042 265.755);
- --muted: oklch(0.97 0.001 285.823);
- --muted-foreground: oklch(0.556 0.016 285.938);
- --accent: oklch(0.97 0.001 285.823);
- --accent-foreground: oklch(0.205 0.042 265.755);
+ --popover-foreground: oklch(0.141 0.005 285.823);
+ --primary: oklch(0.21 0.006 285.885);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.967 0.001 286.375);
+ --secondary-foreground: oklch(0.21 0.006 285.885);
+ --muted: oklch(0.967 0.001 286.375);
+ --muted-foreground: oklch(0.552 0.016 285.938);
+ --accent: oklch(0.967 0.001 286.375);
+ --accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
- --destructive-foreground: oklch(0.577 0.245 27.325);
- --border: oklch(0.922 0.004 285.823);
- --input: oklch(0.922 0.004 285.823);
- --ring: oklch(0.708 0.028 285.823);
+ --destructive-foreground: oklch(0.985 0 0);
+ --border: oklch(0.92 0.004 286.32);
+ --input: oklch(0.92 0.004 286.32);
+ --ring: oklch(0.21 0.006 285.885);
--radius: 0.625rem;
- --sidebar-background: oklch(0.985 0.002 285.823);
- --sidebar-foreground: oklch(0.145 0.004 285.823);
- --sidebar-primary: oklch(0.205 0.042 265.755);
- --sidebar-primary-foreground: oklch(0.985 0.002 285.823);
- --sidebar-accent: oklch(0.97 0.001 285.823);
- --sidebar-accent-foreground: oklch(0.205 0.042 265.755);
- --sidebar-border: oklch(0.922 0.004 285.823);
- --sidebar-ring: oklch(0.708 0.028 285.823);
+ --success: oklch(0.448 0.119 151.328);
+ --success-foreground: oklch(1 0 0);
+ --warning: oklch(0.769 0.188 70.08);
+ --warning-foreground: oklch(0.344 0.065 58.147);
+ --info: oklch(0.541 0.1 222.717);
+ --info-foreground: oklch(1 0 0);
+ --table-header: oklch(0.985 0.002 247.858);
+ --table-header-foreground: oklch(0.208 0.042 265.755);
+ --sidebar-background: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
+ --sidebar-primary: oklch(0.21 0.006 285.885);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.967 0.001 286.375);
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
+ --sidebar-border: oklch(0.92 0.004 286.32);
+ --sidebar-ring: oklch(0.21 0.006 285.885);
}
.dark {
- --background: oklch(0.145 0.004 285.823);
- --foreground: oklch(0.985 0.002 285.823);
- --card: oklch(0.205 0.006 285.885);
- --card-foreground: oklch(0.985 0.002 285.823);
- --popover: oklch(0.205 0.006 285.885);
- --popover-foreground: oklch(0.985 0.002 285.823);
- --primary: oklch(0.985 0.002 285.823);
- --primary-foreground: oklch(0.205 0.042 265.755);
- --secondary: oklch(0.274 0.006 285.885);
- --secondary-foreground: oklch(0.985 0.002 285.823);
- --muted: oklch(0.274 0.006 285.885);
- --muted-foreground: oklch(0.655 0.015 285.823);
- --accent: oklch(0.335 0.006 285.885);
- --accent-foreground: oklch(0.985 0.002 285.823);
+ color-scheme: dark;
+ --background: oklch(0.141 0.005 285.823);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.21 0.006 285.885);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.21 0.006 285.885);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.92 0.004 286.32);
+ --primary-foreground: oklch(0.21 0.006 285.885);
+ --secondary: oklch(0.274 0.006 286.033);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.274 0.006 286.033);
+ --muted-foreground: oklch(0.705 0.015 286.067);
+ --accent: oklch(0.274 0.006 286.033);
+ --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
- --destructive-foreground: oklch(0.577 0.245 27.325);
+ --destructive-foreground: oklch(0.985 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
- --ring: oklch(0.556 0.01 285.885);
- --sidebar-background: oklch(0.175 0.004 285.823);
- --sidebar-foreground: oklch(0.985 0.002 285.823);
- --sidebar-primary: oklch(0.985 0.002 285.823);
- --sidebar-primary-foreground: oklch(0.205 0.006 285.885);
- --sidebar-accent: oklch(0.274 0.006 285.885);
- --sidebar-accent-foreground: oklch(0.985 0.002 285.823);
+ --ring: oklch(0.92 0.004 286.32);
+ --success: oklch(0.596 0.145 151.328);
+ --success-foreground: oklch(0.145 0.03 151.328);
+ --warning: oklch(0.82 0.14 70.08);
+ --warning-foreground: oklch(0.2 0.04 70.08);
+ --info: oklch(0.646 0.116 222.717);
+ --info-foreground: oklch(0.145 0.03 222.717);
+ --table-header: oklch(0.22 0.015 249.2);
+ --table-header-foreground: oklch(0.85 0.02 249.2);
+ --sidebar-background: oklch(0.18 0.006 285.885);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.92 0.004 286.32);
+ --sidebar-primary-foreground: oklch(0.21 0.006 285.885);
+ --sidebar-accent: oklch(0.274 0.006 286.033);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.5 0.01 285.885);
+ --sidebar-ring: oklch(0.92 0.004 286.32);
}
}
@@ -96,6 +133,14 @@
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
+ --color-success: var(--success);
+ --color-success-foreground: var(--success-foreground);
+ --color-warning: var(--warning);
+ --color-warning-foreground: var(--warning-foreground);
+ --color-info: var(--info);
+ --color-info-foreground: var(--info-foreground);
+ --color-table-header: var(--table-header);
+ --color-table-header-foreground: var(--table-header-foreground);
--color-sidebar: var(--sidebar-background);
--color-sidebar-background: var(--sidebar-background);
--color-sidebar-foreground: var(--sidebar-foreground);
diff --git a/app/layout.tsx b/app/layout.tsx
index 0f57bc1..f29de53 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -2,6 +2,7 @@ import type { Metadata } from 'next'
import { headers } from 'next/headers'
import './globals.css'
import { ThemeProvider } from 'components/theme/ThemeProvider'
+import { NO_FLASH_THEME_SCRIPT } from 'components/theme/noFlashThemeScript'
export const metadata: Metadata = {
title: 'Grace',
@@ -14,13 +15,29 @@ export default async function RootLayout({
children: React.ReactNode
}>) {
const headerStore = await headers()
- const initialMode =
- headerStore.get('x-theme-mode') === 'dark' ? 'dark' : 'light'
+ const themeHeader = headerStore.get('x-theme-mode')
+ const explicitMode: 'light' | 'dark' | null =
+ themeHeader === 'dark' ? 'dark' : themeHeader === 'light' ? 'light' : null
+ const initialPreference: 'light' | 'dark' | 'system' =
+ explicitMode ?? 'system'
return (
-
-
- {children}
+
+
+ {explicitMode === null && (
+ // 定数のちらつき防止スクリプトを注入(ユーザー入力なし・XSS リスクなし)
+
+ )}
+
+ {children}
+
)
diff --git a/components/layout/AppShell.tsx b/components/layout/AppShell.tsx
index b63a3b8..892db14 100644
--- a/components/layout/AppShell.tsx
+++ b/components/layout/AppShell.tsx
@@ -27,8 +27,6 @@ import {
ChevronDown,
ChevronRight,
User,
- Sun,
- Moon,
Settings,
LogOut,
X,
@@ -36,7 +34,7 @@ import {
Pill,
ClipboardList,
} from 'lucide-react'
-import { useTheme } from 'components/theme/ThemeProvider'
+import { ThemeToggle } from 'components/theme/ThemeToggle'
import { cn } from '@/lib/utils'
const SIDEBAR_WIDTH = 280
@@ -115,14 +113,13 @@ export default function AppShell({
const pathname = usePathname()
const router = useRouter()
const isMobile = useIsMobile()
- const { mode, toggleMode } = useTheme()
const [sidebarOpen, setSidebarOpenState] = useState(initialSidebarOpen)
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [groupOpen, setGroupOpen] = useState(true)
const setSidebarOpen = useCallback((open: boolean) => {
setSidebarOpenState(open)
- document.cookie = `grace-sidebar-open=${open}; path=/; max-age=31536000`
+ document.cookie = `grace-sidebar-open=${open}; path=/; max-age=31536000; SameSite=Lax`
}, [])
const activeHref = pathname.startsWith('/medication/drugs')
@@ -314,18 +311,7 @@ export default function AppShell({
-
+
{user && (
diff --git a/components/page-component/SettingsPage.tsx b/components/page-component/SettingsPage.tsx
index bc3da5a..b9afa1c 100644
--- a/components/page-component/SettingsPage.tsx
+++ b/components/page-component/SettingsPage.tsx
@@ -14,7 +14,7 @@ interface SettingsPageProps {
}
export default function SettingsPage({ userName }: SettingsPageProps) {
- const { mode, toggleMode } = useTheme()
+ const { preference, setPreference } = useTheme()
const apiClient = useApiClient()
const [discordUserId, setDiscordUserId] = useState(null)
@@ -52,7 +52,10 @@ export default function SettingsPage({ userName }: SettingsPageProps) {
設定
-
+
void
+ preference: ThemePreference
+ onChangePreference: (preference: ThemePreference) => void
}
+const THEME_OPTIONS: { value: ThemePreference; label: string }[] = [
+ { value: 'light', label: 'ライト' },
+ { value: 'dark', label: 'ダーク' },
+ { value: 'system', label: 'システム' },
+]
+
export default function DisplaySettings({
- mode,
- onToggle,
+ preference,
+ onChangePreference,
}: DisplaySettingsProps) {
return (
@@ -17,17 +31,24 @@ export default function DisplaySettings({
-
-
+
diff --git a/components/theme/ThemeProvider.tsx b/components/theme/ThemeProvider.tsx
index fea396c..fe575c8 100644
--- a/components/theme/ThemeProvider.tsx
+++ b/components/theme/ThemeProvider.tsx
@@ -2,26 +2,34 @@
import {
createContext,
+ useCallback,
useContext,
+ useEffect,
useState,
- useCallback,
type ReactNode,
} from 'react'
type ThemeMode = 'light' | 'dark'
+export type ThemePreference = ThemeMode | 'system'
interface ThemeContextValue {
+ /** 実際に適用されているテーマ(`system` 時は matchMedia の解決結果) */
mode: ThemeMode
- toggleMode: () => void
+ /** ユーザーの選択(`light` / `dark` / `system`) */
+ preference: ThemePreference
+ /** 選択を変更する(Cookie に永続化) */
+ setPreference: (pref: ThemePreference) => void
}
const ThemeContext = createContext(undefined)
const COOKIE_KEY = 'grace-theme-mode'
-function applyTheme(mode: ThemeMode) {
- document.cookie = `${COOKIE_KEY}=${mode};path=/;max-age=31536000`
+function writeCookie(value: ThemePreference) {
+ document.cookie = `${COOKIE_KEY}=${value};path=/;max-age=31536000;SameSite=Lax`
+}
+function applyClass(mode: ThemeMode) {
if (mode === 'dark') {
document.documentElement.classList.add('dark')
} else {
@@ -29,25 +37,60 @@ function applyTheme(mode: ThemeMode) {
}
}
+function resolveSystemMode(): ThemeMode {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches
+ ? 'dark'
+ : 'light'
+}
+
export function ThemeProvider({
children,
- initialMode,
+ initialPreference,
}: {
children: ReactNode
- initialMode: ThemeMode
+ initialPreference: ThemePreference
}) {
- const [mode, setMode] = useState(initialMode)
-
- const toggleMode = useCallback(() => {
- setMode((prev) => {
- const next = prev === 'light' ? 'dark' : 'light'
- applyTheme(next)
- return next
- })
+ const [preference, setPreferenceState] =
+ useState(initialPreference)
+ const [mode, setMode] = useState(() =>
+ initialPreference === 'dark' ? 'dark' : 'light',
+ )
+
+ // useEffect の例外: ブラウザ API (window.matchMedia) の購読とクリーンアップ
+ useEffect(() => {
+ if (preference !== 'system') return
+
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
+ const apply = (matches: boolean) => {
+ const next: ThemeMode = matches ? 'dark' : 'light'
+ setMode(next)
+ applyClass(next)
+ }
+
+ apply(mediaQuery.matches)
+
+ const listener = (e: MediaQueryListEvent) => apply(e.matches)
+ mediaQuery.addEventListener('change', listener)
+ return () => mediaQuery.removeEventListener('change', listener)
+ }, [preference])
+
+ const setPreference = useCallback((next: ThemePreference) => {
+ setPreferenceState(next)
+ writeCookie(next)
+
+ if (next === 'system') {
+ // effect でも反映されるが、選択直後の描画を速くするためここでも適用する
+ const resolved = resolveSystemMode()
+ setMode(resolved)
+ applyClass(resolved)
+ } else {
+ setMode(next)
+ applyClass(next)
+ }
}, [])
return (
-
+
{children}
)
diff --git a/components/theme/ThemeToggle.tsx b/components/theme/ThemeToggle.tsx
new file mode 100644
index 0000000..b9aabde
--- /dev/null
+++ b/components/theme/ThemeToggle.tsx
@@ -0,0 +1,85 @@
+'use client'
+
+import { Laptop, Moon, Sun, Check, type LucideIcon } from 'lucide-react'
+import { Button } from 'components/ui/button'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from 'components/ui/dropdown-menu'
+import { useTheme, type ThemePreference } from 'components/theme/ThemeProvider'
+
+interface ThemeToggleProps {
+ className?: string
+ variant?: React.ComponentProps['variant']
+ size?: React.ComponentProps['size']
+}
+
+const OPTIONS: { value: ThemePreference; label: string; Icon: LucideIcon }[] = [
+ { value: 'light', label: 'ライト', Icon: Sun },
+ { value: 'dark', label: 'ダーク', Icon: Moon },
+ { value: 'system', label: 'システム', Icon: Laptop },
+]
+
+const MODE_LABELS: Record = {
+ light: 'ライト',
+ dark: 'ダーク',
+ system: 'システム',
+}
+
+/**
+ * テーマ切替。ライト / ダーク / システムの3択。
+ * トリガーアイコンは `preference` に追従する(`system` 選択時は Laptop)。
+ */
+export function ThemeToggle({
+ className,
+ variant = 'ghost',
+ size = 'icon-sm',
+}: ThemeToggleProps) {
+ const { mode, preference, setPreference } = useTheme()
+
+ const TriggerIcon =
+ preference === 'light' ? Sun : preference === 'dark' ? Moon : Laptop
+
+ return (
+
+
+
+
+
+ テーマ
+
+ {OPTIONS.map(({ value, label, Icon }) => {
+ const selected = preference === value
+ return (
+ setPreference(value)}
+ className="flex items-center justify-between"
+ >
+
+
+ {label}
+
+ {selected && }
+
+ )
+ })}
+
+
+ )
+}
diff --git a/components/theme/noFlashThemeScript.ts b/components/theme/noFlashThemeScript.ts
new file mode 100644
index 0000000..c1e598b
--- /dev/null
+++ b/components/theme/noFlashThemeScript.ts
@@ -0,0 +1,17 @@
+/**
+ * テーマ設定が `system`(Cookie 未設定または `system`)のとき、SSR では
+ * `prefers-color-scheme` を読めないため、hydration 前にこのインラインスクリプトで
+ * `dark` クラスを適用する。「ライトで描画→mount 後にダークへ切替」のちらつき防止。
+ * 明示的な light/dark が選択されていない場合のみ layout が注入する。
+ *
+ * 内容はユーザー入力を含まない定数文字列のため XSS リスクはない。
+ */
+export const NO_FLASH_THEME_SCRIPT = `
+(function(){
+ try {
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ document.documentElement.classList.add('dark');
+ }
+ } catch (_) {}
+})();
+`
diff --git a/middleware.ts b/middleware.ts
index 1aacea4..84f0bea 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -10,10 +10,19 @@ function redirectToLoginPage(request: NextRequest) {
}
export async function middleware(request: NextRequest) {
- const themeMode = request.cookies.get('grace-theme-mode')?.value ?? 'light'
+ // テーマ設定 Cookie を x-theme-mode ヘッダーとして転送し、layout が SSR 時点で
+ // 初期テーマを確定できるようにする。未設定・不明値は `system`(クライアントで
+ // prefers-color-scheme を解決)。
+ const themeCookie = request.cookies.get('grace-theme-mode')?.value
+ const themeMode =
+ themeCookie === 'dark' ||
+ themeCookie === 'light' ||
+ themeCookie === 'system'
+ ? themeCookie
+ : 'system'
const sidebarOpen = request.cookies.get('grace-sidebar-open')?.value ?? 'true'
const requestHeaders = new Headers(request.headers)
- requestHeaders.set('x-theme-mode', themeMode === 'dark' ? 'dark' : 'light')
+ requestHeaders.set('x-theme-mode', themeMode)
requestHeaders.set(
'x-sidebar-open',
sidebarOpen === 'false' ? 'false' : 'true',
@@ -53,5 +62,5 @@ export async function middleware(request: NextRequest) {
}
export const config = {
- matcher: ['/(dashboard|settings|medication)(/?.*)', '/setup'],
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}
diff --git a/package.json b/package.json
index f5c0bc3..02c461e 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
},
"devDependencies": {
"@eslint/compat": "^1.3.1",
+ "@storybook/addon-a11y": "^10.2.8",
"@storybook/nextjs": "^10.2.8",
"@storybook/react": "^10.2.8",
"@types/node": "^25.0.3",
diff --git a/stories/decorators.tsx b/stories/decorators.tsx
index e8f89db..f741814 100644
--- a/stories/decorators.tsx
+++ b/stories/decorators.tsx
@@ -1,5 +1,4 @@
import type { Decorator } from '@storybook/react'
-import { ThemeProvider } from '../components/theme/ThemeProvider'
import AppShell from '../components/layout/AppShell'
const sampleUser = {
@@ -7,16 +6,8 @@ const sampleUser = {
iconUrl: null,
}
-export const withThemeProvider: Decorator = (Story) => (
-
-
-
-)
-
export const withAppShell: Decorator = (Story) => (
-
-
-
-
-
+
+
+
)
diff --git a/stories/layout/AppShell.stories.tsx b/stories/layout/AppShell.stories.tsx
index db79d07..021895e 100644
--- a/stories/layout/AppShell.stories.tsx
+++ b/stories/layout/AppShell.stories.tsx
@@ -1,6 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react'
import AppShell from '../../components/layout/AppShell'
-import { withThemeProvider } from '../decorators'
const sampleUser = {
name: 'テストユーザー',
@@ -10,7 +9,6 @@ const sampleUser = {
const meta = {
title: 'Layout/AppShell',
component: AppShell,
- decorators: [withThemeProvider],
parameters: {
layout: 'fullscreen',
nextjs: {
diff --git a/stories/settings/AccountSettings.stories.tsx b/stories/settings/AccountSettings.stories.tsx
index d9809b3..8f4daeb 100644
--- a/stories/settings/AccountSettings.stories.tsx
+++ b/stories/settings/AccountSettings.stories.tsx
@@ -1,11 +1,9 @@
import type { Meta, StoryObj } from '@storybook/react'
import AccountSettings from '../../components/settings/AccountSettings'
-import { withThemeProvider } from '../decorators'
const meta = {
title: 'Components/AccountSettings',
component: AccountSettings,
- decorators: [withThemeProvider],
parameters: {
layout: 'padded',
},
diff --git a/stories/settings/DisplaySettings.stories.tsx b/stories/settings/DisplaySettings.stories.tsx
index 577443e..072da3a 100644
--- a/stories/settings/DisplaySettings.stories.tsx
+++ b/stories/settings/DisplaySettings.stories.tsx
@@ -1,11 +1,9 @@
import type { Meta, StoryObj } from '@storybook/react'
import DisplaySettings from '../../components/settings/DisplaySettings'
-import { withThemeProvider } from '../decorators'
const meta = {
title: 'Components/DisplaySettings',
component: DisplaySettings,
- decorators: [withThemeProvider],
parameters: {
layout: 'padded',
},
@@ -14,16 +12,23 @@ const meta = {
export default meta
type Story = StoryObj
-export const LightMode: Story = {
+export const Light: Story = {
args: {
- mode: 'light',
- onToggle: () => {},
+ preference: 'light',
+ onChangePreference: () => {},
},
}
-export const DarkMode: Story = {
+export const Dark: Story = {
args: {
- mode: 'dark',
- onToggle: () => {},
+ preference: 'dark',
+ onChangePreference: () => {},
+ },
+}
+
+export const System: Story = {
+ args: {
+ preference: 'system',
+ onChangePreference: () => {},
},
}
diff --git a/stories/settings/ExternalServiceSettings.stories.tsx b/stories/settings/ExternalServiceSettings.stories.tsx
index ba0699d..176cc47 100644
--- a/stories/settings/ExternalServiceSettings.stories.tsx
+++ b/stories/settings/ExternalServiceSettings.stories.tsx
@@ -1,11 +1,9 @@
import type { Meta, StoryObj } from '@storybook/react'
import ExternalServiceSettings from '../../components/settings/ExternalServiceSettings'
-import { withThemeProvider } from '../decorators'
const meta = {
title: 'Components/ExternalServiceSettings',
component: ExternalServiceSettings,
- decorators: [withThemeProvider],
parameters: {
layout: 'padded',
},
diff --git a/stories/settings/SettingsPage.stories.tsx b/stories/settings/SettingsPage.stories.tsx
index c6ee642..54fb217 100644
--- a/stories/settings/SettingsPage.stories.tsx
+++ b/stories/settings/SettingsPage.stories.tsx
@@ -1,11 +1,10 @@
import type { Meta, StoryObj } from '@storybook/react'
import SettingsPage from '../../components/page-component/SettingsPage'
-import { withAppShell, withThemeProvider } from '../decorators'
+import { withAppShell } from '../decorators'
const meta = {
title: 'Pages/Settings',
component: SettingsPage,
- decorators: [withThemeProvider],
parameters: {
layout: 'fullscreen',
nextjs: {
diff --git a/stories/theme/ThemeToggle.stories.tsx b/stories/theme/ThemeToggle.stories.tsx
new file mode 100644
index 0000000..9d301f3
--- /dev/null
+++ b/stories/theme/ThemeToggle.stories.tsx
@@ -0,0 +1,21 @@
+import type { Meta, StoryObj } from '@storybook/react'
+import { ThemeToggle } from '../../components/theme/ThemeToggle'
+
+const meta = {
+ title: 'Components/ThemeToggle',
+ component: ThemeToggle,
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {}
+
+export const Outline: Story = {
+ args: {
+ variant: 'outline',
+ },
+}
diff --git a/yarn.lock b/yarn.lock
index c4642f1..a5d8265 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2598,6 +2598,14 @@
resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
+"@storybook/addon-a11y@^10.2.8":
+ version "10.4.6"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-a11y/-/addon-a11y-10.4.6.tgz#eb5acd538a711d1fa0529d3a46731de021149228"
+ integrity sha512-XCJy+f0DFOiCgUU9knRDlLDxVFI+AAQ3/wE/NF85zB9iDPPS2DwkSN+mas3zDgHt66zhN8Cq3/UiyCDUweV9Zw==
+ dependencies:
+ "@storybook/global" "^5.0.0"
+ axe-core "^4.2.0"
+
"@storybook/builder-webpack5@10.4.6":
version "10.4.6"
resolved "https://registry.yarnpkg.com/@storybook/builder-webpack5/-/builder-webpack5-10.4.6.tgz#11b570c0cb084bb129a482043d3f3bc01d29d7eb"
@@ -3846,7 +3854,7 @@ available-typed-arrays@^1.0.7:
dependencies:
possible-typed-array-names "^1.0.0"
-axe-core@^4.10.0:
+axe-core@^4.10.0, axe-core@^4.2.0:
version "4.12.1"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.12.1.tgz#69fe2bf6dfc0b24973af91b1d8e945f79e1b7c44"
integrity sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==