93 lines
2.5 KiB
TypeScript
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>
|
|
);
|
|
}
|