This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { PageId } from '../../lib/urlState';
|
||||
import { useBackdropClose } from '../../lib/useBackdropClose';
|
||||
|
||||
@@ -96,6 +97,7 @@ export function NavDrawer({
|
||||
logoUrl,
|
||||
returnFocusRef,
|
||||
}: NavDrawerProps) {
|
||||
const { t } = useTranslation('layout');
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const firstItemRef = useRef<HTMLButtonElement>(null);
|
||||
const backdrop = useBackdropClose(onClose);
|
||||
@@ -163,7 +165,7 @@ export function NavDrawer({
|
||||
id="nav-drawer"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="ナビゲーション"
|
||||
aria-label={t('drawer.nav')}
|
||||
aria-hidden={!open}
|
||||
tabIndex={-1}
|
||||
onKeyDown={onPanelKeyDown}
|
||||
@@ -189,7 +191,7 @@ export function NavDrawer({
|
||||
v{__APP_VERSION__}
|
||||
</span>
|
||||
</div>
|
||||
<nav className="flex-1 overflow-y-auto py-2" aria-label="メインナビゲーション">
|
||||
<nav className="flex-1 overflow-y-auto py-2" aria-label={t('nav.mainNav')}>
|
||||
{visibleNav.map((item, idx) => {
|
||||
const active = currentPage === item.id;
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// ui/src/components/layout/ResizeHandle.tsx
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ResizeHandleProps {
|
||||
/** drag 中に呼ばれる。新しい chatPx を渡す。ref-based で React 再 render しない想定。 */
|
||||
@@ -23,6 +24,7 @@ export function ResizeHandle({
|
||||
minWorkspacePx,
|
||||
handlePx,
|
||||
}: ResizeHandleProps) {
|
||||
const { t } = useTranslation('layout');
|
||||
// latest-ref pattern: callback が render 毎に新しくなっても useEffect の
|
||||
// listener を付け直さずに済む。これが無いと drag 中に listener が外れる。
|
||||
const onResizeRef = useRef(onResize);
|
||||
@@ -75,7 +77,7 @@ export function ResizeHandle({
|
||||
<div
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label="Chat と Workspace の幅を調整"
|
||||
aria-label={t('resize.chatWorkspace')}
|
||||
onPointerDown={handlePointerDown}
|
||||
onDoubleClick={onReset}
|
||||
className="cursor-col-resize bg-transparent hover:bg-slate-300/60 transition-colors flex items-stretch group"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { type ThemePref, isThemePref, readStoredTheme, setThemePref, THEME_CHANGE_EVENT } from '../../lib/theme';
|
||||
|
||||
const ICON_PROPS = {
|
||||
@@ -13,10 +14,10 @@ const ICON_PROPS = {
|
||||
'aria-hidden': true,
|
||||
};
|
||||
|
||||
const OPTIONS: Array<{ value: ThemePref; label: string; icon: JSX.Element }> = [
|
||||
const OPTIONS: Array<{ value: ThemePref; labelKey: string; icon: JSX.Element }> = [
|
||||
{
|
||||
value: 'system',
|
||||
label: 'システム設定に合わせる',
|
||||
labelKey: 'theme.system',
|
||||
icon: (
|
||||
<svg {...ICON_PROPS}>
|
||||
<rect x="2" y="4" width="20" height="13" rx="2" />
|
||||
@@ -26,7 +27,7 @@ const OPTIONS: Array<{ value: ThemePref; label: string; icon: JSX.Element }> = [
|
||||
},
|
||||
{
|
||||
value: 'light',
|
||||
label: 'ライト',
|
||||
labelKey: 'theme.light',
|
||||
icon: (
|
||||
<svg {...ICON_PROPS}>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
@@ -36,7 +37,7 @@ const OPTIONS: Array<{ value: ThemePref; label: string; icon: JSX.Element }> = [
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: 'ダーク',
|
||||
labelKey: 'theme.dark',
|
||||
icon: (
|
||||
<svg {...ICON_PROPS}>
|
||||
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
|
||||
@@ -54,6 +55,7 @@ const OPTIONS: Array<{ value: ThemePref; label: string; icon: JSX.Element }> = [
|
||||
* [data-theme] attribute correct on load and on OS changes.
|
||||
*/
|
||||
export function ThemeToggle() {
|
||||
const { t } = useTranslation('layout');
|
||||
const [pref, setPref] = useState<ThemePref>(() => readStoredTheme());
|
||||
|
||||
useEffect(() => {
|
||||
@@ -75,7 +77,7 @@ export function ThemeToggle() {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-label="テーマ"
|
||||
aria-label={t('theme.label')}
|
||||
className="inline-flex items-center gap-0.5 rounded-md border border-hairline bg-surface p-0.5"
|
||||
>
|
||||
{OPTIONS.map((opt) => {
|
||||
@@ -85,9 +87,9 @@ export function ThemeToggle() {
|
||||
key={opt.value}
|
||||
type="button"
|
||||
onClick={() => choose(opt.value)}
|
||||
aria-label={opt.label}
|
||||
aria-label={t(opt.labelKey)}
|
||||
aria-pressed={active}
|
||||
title={opt.label}
|
||||
title={t(opt.labelKey)}
|
||||
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'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { PageId } from '../../lib/urlState';
|
||||
import type { AuthUser } from '../../App';
|
||||
import { ThemeToggle } from './ThemeToggle';
|
||||
@@ -18,15 +19,17 @@ interface TopBarProps {
|
||||
onOpenCommandK?: () => void;
|
||||
}
|
||||
|
||||
export const NAV_ITEMS: Array<{ id: PageId; label: string; adminOnly: boolean; requiresAuth: boolean }> = [
|
||||
{ id: 'tasks', label: 'タスク', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'schedules', label: 'スケジュール', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'pieces', label: 'Pieces', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'captcha', label: 'CAPTCHA', adminOnly: true, requiresAuth: false },
|
||||
{ id: 'settings', label: '設定', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'users', label: 'ユーザー', adminOnly: true, requiresAuth: true },
|
||||
{ id: 'help', label: 'ヘルプ', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'userfolder', label: 'ユーザーフォルダ', adminOnly: false, requiresAuth: false },
|
||||
// labelKey resolves against the `layout` i18n namespace at render time (module
|
||||
// scope can't call hooks). All consumers translate: TopBar, NavDrawer, App.tsx.
|
||||
export const NAV_ITEMS: Array<{ id: PageId; labelKey: string; adminOnly: boolean; requiresAuth: boolean }> = [
|
||||
{ id: 'tasks', labelKey: 'nav.tasks', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'schedules', labelKey: 'nav.schedules', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'pieces', labelKey: 'nav.pieces', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'captcha', labelKey: 'nav.captcha', adminOnly: true, requiresAuth: false },
|
||||
{ id: 'settings', labelKey: 'nav.settings', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'users', labelKey: 'nav.users', adminOnly: true, requiresAuth: true },
|
||||
{ id: 'help', labelKey: 'nav.help', adminOnly: false, requiresAuth: false },
|
||||
{ id: 'userfolder', labelKey: 'nav.userfolder', adminOnly: false, requiresAuth: false },
|
||||
];
|
||||
|
||||
export function estimateCollapseThreshold(navCount: number): number {
|
||||
@@ -73,6 +76,7 @@ export function TopBar({
|
||||
navDrawerOpen = false,
|
||||
onOpenCommandK,
|
||||
}: TopBarProps) {
|
||||
const { t } = useTranslation('layout');
|
||||
const visibleNav = visibleNavItemsFor(isAdmin, authEnabled);
|
||||
const compactMode = useViewportNarrow(estimateCollapseThreshold(visibleNav.length));
|
||||
const [showPwChange, setShowPwChange] = useState(false);
|
||||
@@ -92,7 +96,7 @@ export function TopBar({
|
||||
ref={hamburgerButtonRef}
|
||||
type="button"
|
||||
onClick={onOpenDrawer}
|
||||
aria-label="メニューを開く"
|
||||
aria-label={t('nav.openMenu')}
|
||||
aria-expanded={navDrawerOpen}
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="nav-drawer"
|
||||
@@ -118,7 +122,7 @@ export function TopBar({
|
||||
</span>
|
||||
|
||||
{!compactMode && (
|
||||
<nav className="flex gap-5 items-stretch -mb-[13px] ml-2" aria-label="メインナビゲーション">
|
||||
<nav className="flex gap-5 items-stretch -mb-[13px] ml-2" aria-label={t('nav.mainNav')}>
|
||||
{visibleNav.map(item => {
|
||||
const active = currentPage === item.id;
|
||||
return (
|
||||
@@ -133,7 +137,7 @@ export function TopBar({
|
||||
: 'font-medium text-slate-500 border-transparent hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
{t(item.labelKey)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -146,8 +150,8 @@ export function TopBar({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenCommandK}
|
||||
aria-label="コマンドパレットを開く"
|
||||
title="コマンドパレット (⌘K)"
|
||||
aria-label={t('commandPalette.open')}
|
||||
title={t('commandPalette.title')}
|
||||
className="hidden sm:inline-flex items-center gap-1 px-2 h-7 rounded-md border border-hairline text-2xs text-slate-500 hover:text-slate-800 hover:bg-surface transition-colors"
|
||||
>
|
||||
<span aria-hidden>⌘K</span>
|
||||
@@ -177,13 +181,13 @@ export function TopBar({
|
||||
onClick={() => setShowPwChange(true)}
|
||||
className="px-2 py-1 rounded-md text-2xs text-slate-500 hover:text-slate-800 hover:bg-surface transition-colors"
|
||||
>
|
||||
パスワード変更
|
||||
{t('user.changePassword')}
|
||||
</button>
|
||||
<a
|
||||
href="/auth/logout"
|
||||
className="px-2 py-1 rounded-md text-2xs text-slate-500 hover:text-slate-800 hover:bg-surface transition-colors"
|
||||
>
|
||||
ログアウト
|
||||
{t('user.logout')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
/** drag 中に呼ばれる。upperPct (0..100) を渡す。 */
|
||||
@@ -17,6 +18,7 @@ export function VerticalResizeHandle({
|
||||
onResize, onResizeEnd, onReset, parentSelector,
|
||||
minUpperPct = 20, minLowerPct = 15,
|
||||
}: Props) {
|
||||
const { t } = useTranslation('layout');
|
||||
const onResizeRef = useRef(onResize);
|
||||
const onResizeEndRef = useRef(onResizeEnd);
|
||||
onResizeRef.current = onResize;
|
||||
@@ -57,7 +59,7 @@ export function VerticalResizeHandle({
|
||||
<div
|
||||
role="separator"
|
||||
aria-orientation="horizontal"
|
||||
aria-label="タスクリストと情報パネルの高さを調整"
|
||||
aria-label={t('resize.listPanel')}
|
||||
onPointerDown={(e) => {
|
||||
e.preventDefault();
|
||||
draggingRef.current = true;
|
||||
|
||||
Reference in New Issue
Block a user