maestro/ui/src/components/layout/ThemeToggle.tsx
oss-sync afae52873b
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (10f4905)
2026-06-05 06:52:55 +00:00

93 lines
2.5 KiB
TypeScript

import { useState } from 'react';
import { type ThemePref, readStoredTheme, setThemePref } from '../../lib/theme';
const ICON_PROPS = {
width: 14,
height: 14,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth: 1.8,
strokeLinecap: 'round' as const,
strokeLinejoin: 'round' as const,
'aria-hidden': true,
};
const OPTIONS: Array<{ value: ThemePref; label: string; icon: JSX.Element }> = [
{
value: 'system',
label: 'システム設定に合わせる',
icon: (
<svg {...ICON_PROPS}>
<rect x="2" y="4" width="20" height="13" rx="2" />
<path d="M8 21h8M12 17v4" />
</svg>
),
},
{
value: 'light',
label: 'ライト',
icon: (
<svg {...ICON_PROPS}>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
</svg>
),
},
{
value: 'dark',
label: 'ダーク',
icon: (
<svg {...ICON_PROPS}>
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
</svg>
),
},
];
/**
* Compact 3-way theme switch (system / light / dark) for the TopBar.
*
* All theme logic lives in lib/theme.ts: this only tracks the displayed
* preference and calls setThemePref (persist + apply) on selection. The
* pre-paint inline script in index.html + initTheme() keep the actual
* [data-theme] attribute correct on load and on OS changes.
*/
export function ThemeToggle() {
const [pref, setPref] = useState<ThemePref>(() => readStoredTheme());
const choose = (value: ThemePref) => {
setPref(value);
setThemePref(value);
};
return (
<div
role="group"
aria-label="テーマ"
className="inline-flex items-center gap-0.5 rounded-md border border-hairline bg-surface p-0.5"
>
{OPTIONS.map((opt) => {
const active = pref === opt.value;
return (
<button
key={opt.value}
type="button"
onClick={() => choose(opt.value)}
aria-label={opt.label}
aria-pressed={active}
title={opt.label}
className={`flex items-center justify-center w-6 h-6 rounded transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${
active
? 'bg-canvas text-slate-900 shadow-sm'
: 'text-slate-500 hover:text-slate-800'
}`}
>
{opt.icon}
</button>
);
})}
</div>
);
}