このコミットが含まれているのは:
@@ -105,6 +105,7 @@ const TagSearch: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
|
data-shortcut-focus-search="true"
|
||||||
placeholder="タグ検索..."
|
placeholder="タグ検索..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={whenChanged}
|
onChange={whenChanged}
|
||||||
|
|||||||
@@ -3,15 +3,18 @@ import { useEffect, useMemo, useState } from 'react'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { toast } from '@/components/ui/use-toast'
|
import { toast } from '@/components/ui/use-toast'
|
||||||
import {
|
import {
|
||||||
|
getConflictingShortcutActionIds,
|
||||||
formatKeyBinding,
|
formatKeyBinding,
|
||||||
isSpaceBinding,
|
isSpaceBinding,
|
||||||
keyBindingFromKeyboardEvent,
|
keyBindingFromKeyboardEvent,
|
||||||
SHORTCUT_DEFINITIONS,
|
SHORTCUT_DEFINITIONS,
|
||||||
SHORTCUT_SCOPE_LABELS,
|
SHORTCUT_SCOPE_LABELS,
|
||||||
} from '@/lib/keyboardShortcuts'
|
} from '@/lib/keyboardShortcuts'
|
||||||
|
import { getEffectiveKeyBindings } from '@/lib/settings'
|
||||||
import { useKeyboardShortcutSettings } from '@/lib/useKeyboardShortcuts'
|
import { useKeyboardShortcutSettings } from '@/lib/useKeyboardShortcuts'
|
||||||
|
|
||||||
import type { ShortcutActionId } from '@/lib/keyboardShortcuts'
|
import type { ShortcutActionId } from '@/lib/keyboardShortcuts'
|
||||||
|
import type { ClientKeyboardSettings } from '@/lib/settings'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -21,16 +24,30 @@ type Props = {
|
|||||||
|
|
||||||
const KeyboardSettingsSection: FC<Props> = ({ sectionClassName }) => {
|
const KeyboardSettingsSection: FC<Props> = ({ sectionClassName }) => {
|
||||||
const {
|
const {
|
||||||
conflictActionIds,
|
|
||||||
effectiveBindings,
|
|
||||||
keyboardSettings,
|
keyboardSettings,
|
||||||
resetAllBindings,
|
saveKeyboardSettings,
|
||||||
resetBinding,
|
|
||||||
setBinding,
|
|
||||||
setEnabled,
|
|
||||||
} = useKeyboardShortcutSettings ()
|
} = useKeyboardShortcutSettings ()
|
||||||
const [capturingActionId, setCapturingActionId] =
|
const [capturingActionId, setCapturingActionId] =
|
||||||
useState<ShortcutActionId | null> (null)
|
useState<ShortcutActionId | null> (null)
|
||||||
|
const [draftKeyboardSettings, setDraftKeyboardSettings] =
|
||||||
|
useState<ClientKeyboardSettings> (keyboardSettings)
|
||||||
|
|
||||||
|
useEffect (() => {
|
||||||
|
setDraftKeyboardSettings (keyboardSettings)
|
||||||
|
}, [keyboardSettings])
|
||||||
|
|
||||||
|
const draftEffectiveBindings = useMemo (
|
||||||
|
() => getEffectiveKeyBindings (draftKeyboardSettings.bindings),
|
||||||
|
[draftKeyboardSettings.bindings],
|
||||||
|
)
|
||||||
|
const draftConflictActionIds = useMemo (
|
||||||
|
() => getConflictingShortcutActionIds (draftEffectiveBindings),
|
||||||
|
[draftEffectiveBindings],
|
||||||
|
)
|
||||||
|
const hasUnsavedChanges = useMemo (
|
||||||
|
() => JSON.stringify (draftKeyboardSettings) !== JSON.stringify (keyboardSettings),
|
||||||
|
[draftKeyboardSettings, keyboardSettings],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
if (capturingActionId == null)
|
if (capturingActionId == null)
|
||||||
@@ -51,7 +68,13 @@ const KeyboardSettingsSection: FC<Props> = ({ sectionClassName }) => {
|
|||||||
|
|
||||||
if (event.key === 'Backspace' || event.key === 'Delete')
|
if (event.key === 'Backspace' || event.key === 'Delete')
|
||||||
{
|
{
|
||||||
setBinding (capturingActionId, null)
|
setDraftKeyboardSettings (current => ({
|
||||||
|
...current,
|
||||||
|
bindings: {
|
||||||
|
...(current.bindings ?? { }),
|
||||||
|
[capturingActionId]: null,
|
||||||
|
},
|
||||||
|
}))
|
||||||
setCapturingActionId (null)
|
setCapturingActionId (null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -66,46 +89,92 @@ const KeyboardSettingsSection: FC<Props> = ({ sectionClassName }) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setBinding (capturingActionId, binding)
|
setDraftKeyboardSettings (current => ({
|
||||||
|
...current,
|
||||||
|
bindings: {
|
||||||
|
...(current.bindings ?? { }),
|
||||||
|
[capturingActionId]: binding,
|
||||||
|
},
|
||||||
|
}))
|
||||||
setCapturingActionId (null)
|
setCapturingActionId (null)
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener ('keydown', onKeyDown, true)
|
document.addEventListener ('keydown', onKeyDown, true)
|
||||||
return () => document.removeEventListener ('keydown', onKeyDown, true)
|
return () => document.removeEventListener ('keydown', onKeyDown, true)
|
||||||
}, [capturingActionId, setBinding])
|
}, [capturingActionId])
|
||||||
|
|
||||||
const rows = useMemo (() => SHORTCUT_DEFINITIONS.map (definition => ({
|
const rows = useMemo (() => SHORTCUT_DEFINITIONS.map (definition => ({
|
||||||
...definition,
|
...definition,
|
||||||
currentBinding: effectiveBindings[definition.id],
|
currentBinding: draftEffectiveBindings[definition.id],
|
||||||
hasConflict: conflictActionIds.has (definition.id),
|
hasConflict: draftConflictActionIds.has (definition.id),
|
||||||
hasCustomBinding: Object.prototype.hasOwnProperty.call (
|
hasCustomBinding: Object.prototype.hasOwnProperty.call (
|
||||||
keyboardSettings.bindings ?? { },
|
draftKeyboardSettings.bindings ?? { },
|
||||||
definition.id,
|
definition.id,
|
||||||
),
|
),
|
||||||
})), [conflictActionIds, effectiveBindings, keyboardSettings.bindings])
|
})), [
|
||||||
|
draftConflictActionIds,
|
||||||
|
draftEffectiveBindings,
|
||||||
|
draftKeyboardSettings.bindings,
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
saveKeyboardSettings (draftKeyboardSettings)
|
||||||
|
toast ({ title: 'キーボード設定を保存しました' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDiscard = () => {
|
||||||
|
setDraftKeyboardSettings (keyboardSettings)
|
||||||
|
setCapturingActionId (null)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={sectionClassName}>
|
<section className={sectionClassName}>
|
||||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h2 className="text-xl font-bold">キーボード</h2>
|
<h2 className="text-xl font-bold">キーボード</h2>
|
||||||
<p className="text-sm text-muted-foreground">
|
{hasUnsavedChanges && (
|
||||||
この設定は現在のブラウザに自動保存されます.競合した割当は保存されますが,
|
<p className="text-sm text-amber-700 dark:text-amber-300">
|
||||||
dispatcher では発火しません.
|
未保存の変更があります
|
||||||
</p>
|
</p>)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="button" variant="outline" onClick={resetAllBindings}>
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setDraftKeyboardSettings (current => ({
|
||||||
|
...current,
|
||||||
|
bindings: { },
|
||||||
|
}))
|
||||||
|
}}>
|
||||||
すべて既定に戻す
|
すべて既定に戻す
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
disabled={!(hasUnsavedChanges)}
|
||||||
|
onClick={handleDiscard}>
|
||||||
|
変更を破棄
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
disabled={!(hasUnsavedChanges) || draftConflictActionIds.size > 0}
|
||||||
|
onClick={handleSave}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="flex items-center gap-2 text-sm">
|
<label className="flex items-center gap-2 text-sm">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={keyboardSettings.enabled !== false}
|
checked={draftKeyboardSettings.enabled !== false}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
setEnabled (event.target.checked)
|
setDraftKeyboardSettings (current => ({
|
||||||
|
...current,
|
||||||
|
enabled: event.target.checked,
|
||||||
|
}))
|
||||||
}}/>
|
}}/>
|
||||||
<span>キーボード・ショートカットを有効にする</span>
|
<span>キーボード・ショートカットを有効にする</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -154,7 +223,16 @@ const KeyboardSettingsSection: FC<Props> = ({ sectionClassName }) => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8"
|
className="h-8"
|
||||||
disabled={!(row.hasCustomBinding)}
|
disabled={!(row.hasCustomBinding)}
|
||||||
onClick={() => resetBinding (row.id)}>
|
onClick={() => {
|
||||||
|
setDraftKeyboardSettings (current => {
|
||||||
|
const nextBindings = { ...(current.bindings ?? { }) }
|
||||||
|
delete nextBindings[row.id]
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
bindings: nextBindings,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}>
|
||||||
既定に戻す
|
既定に戻す
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import {
|
||||||
|
formatKeyBinding,
|
||||||
|
SHORTCUT_DEFINITIONS,
|
||||||
|
SHORTCUT_SCOPE_LABELS,
|
||||||
|
} from '@/lib/keyboardShortcuts'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
KeyBinding,
|
||||||
|
ShortcutActionId,
|
||||||
|
ShortcutScope,
|
||||||
|
} from '@/lib/keyboardShortcuts'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
activeScope: ShortcutScope
|
||||||
|
effectiveBindings: Record<ShortcutActionId, KeyBinding | null>
|
||||||
|
conflictActionIds: Set<ShortcutActionId>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const ShortcutHelpDialog: FC<Props> = (
|
||||||
|
{ open, onOpenChange, activeScope, effectiveBindings, conflictActionIds },
|
||||||
|
) => {
|
||||||
|
const rows = SHORTCUT_DEFINITIONS.filter (definition =>
|
||||||
|
definition.scope === 'global' || definition.scope === activeScope)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-3xl px-6 pb-6 pt-7">
|
||||||
|
<DialogHeader className="pl-8">
|
||||||
|
<DialogTitle>キーボード・ショートカット</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
この画面で利用できるショートカットを表示します。
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full min-w-[40rem] table-fixed border-collapse text-sm">
|
||||||
|
<thead className="border-b border-border">
|
||||||
|
<tr>
|
||||||
|
<th className="p-2 text-left">操作</th>
|
||||||
|
<th className="p-2 text-left">適用範囲</th>
|
||||||
|
<th className="p-2 text-left">キー</th>
|
||||||
|
<th className="p-2 text-left">競合</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.map (row => (
|
||||||
|
<tr key={row.id} className="border-b border-border/60 last:border-b-0">
|
||||||
|
<td className="p-2">{row.label}</td>
|
||||||
|
<td className="p-2">{SHORTCUT_SCOPE_LABELS[row.scope]}</td>
|
||||||
|
<td className="p-2 font-mono">
|
||||||
|
{formatKeyBinding (effectiveBindings[row.id])}
|
||||||
|
</td>
|
||||||
|
<td className="p-2">
|
||||||
|
{conflictActionIds.has (row.id)
|
||||||
|
? <span className="text-red-600 dark:text-red-400">競合あり</span>
|
||||||
|
: 'なし'}
|
||||||
|
</td>
|
||||||
|
</tr>))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShortcutHelpDialog
|
||||||
@@ -9,10 +9,9 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
import { toast } from '@/components/ui/use-toast'
|
import ShortcutHelpDialog from '@/components/users/ShortcutHelpDialog'
|
||||||
import {
|
import {
|
||||||
DEFAULT_KEY_BINDINGS,
|
DEFAULT_KEY_BINDINGS,
|
||||||
formatKeyBinding,
|
|
||||||
getConflictingShortcutActionIds,
|
getConflictingShortcutActionIds,
|
||||||
isEditableEventTarget,
|
isEditableEventTarget,
|
||||||
keyBindingFromKeyboardEvent,
|
keyBindingFromKeyboardEvent,
|
||||||
@@ -46,6 +45,7 @@ type KeyboardShortcutsContextValue = {
|
|||||||
keyboardSettings: ClientKeyboardSettings
|
keyboardSettings: ClientKeyboardSettings
|
||||||
effectiveBindings: Record<ShortcutActionId, KeyBinding | null>
|
effectiveBindings: Record<ShortcutActionId, KeyBinding | null>
|
||||||
conflictActionIds: Set<ShortcutActionId>
|
conflictActionIds: Set<ShortcutActionId>
|
||||||
|
saveKeyboardSettings: (settings: ClientKeyboardSettings) => void
|
||||||
setEnabled: (enabled: boolean) => void
|
setEnabled: (enabled: boolean) => void
|
||||||
setBinding: (actionId: ShortcutActionId, binding: KeyBinding | null) => void
|
setBinding: (actionId: ShortcutActionId, binding: KeyBinding | null) => void
|
||||||
resetBinding: (actionId: ShortcutActionId) => void
|
resetBinding: (actionId: ShortcutActionId) => void
|
||||||
@@ -57,17 +57,47 @@ const KeyboardShortcutsContext =
|
|||||||
createContext<KeyboardShortcutsContextValue | null> (null)
|
createContext<KeyboardShortcutsContextValue | null> (null)
|
||||||
|
|
||||||
|
|
||||||
const buildShortcutHelpText = (
|
const isFocusableShortcutTarget = (element: HTMLElement): boolean => {
|
||||||
activeScope: ShortcutScope,
|
if (element.closest ('[hidden], [aria-hidden="true"]') != null)
|
||||||
effectiveBindings: Record<ShortcutActionId, KeyBinding | null>,
|
return false
|
||||||
): string =>
|
|
||||||
SHORTCUT_DEFINITIONS
|
if (
|
||||||
.filter (definition =>
|
(element instanceof HTMLInputElement
|
||||||
definition.scope === 'global' || definition.scope === activeScope)
|
|| element instanceof HTMLButtonElement
|
||||||
.filter (definition => effectiveBindings[definition.id] != null)
|
|| element instanceof HTMLSelectElement
|
||||||
.map (definition =>
|
|| element instanceof HTMLTextAreaElement)
|
||||||
`${ definition.label }: ${ formatKeyBinding (effectiveBindings[definition.id]) }`)
|
&& element.disabled
|
||||||
.join (' / ')
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
const style = window.getComputedStyle (element)
|
||||||
|
if (
|
||||||
|
style.display === 'none'
|
||||||
|
|| style.visibility === 'hidden'
|
||||||
|
|| style.pointerEvents === 'none'
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
return element.getClientRects ().length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const focusSearchTarget = (): void => {
|
||||||
|
const targets = [
|
||||||
|
...document.querySelectorAll<HTMLElement> (
|
||||||
|
'[data-shortcut-focus-search="true"]',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
const target = targets.find (isFocusableShortcutTarget)
|
||||||
|
|
||||||
|
if (target == null)
|
||||||
|
return
|
||||||
|
|
||||||
|
target.focus ()
|
||||||
|
|
||||||
|
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)
|
||||||
|
target.select ()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
||||||
@@ -78,6 +108,7 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
|||||||
useState<ClientKeyboardSettings> (() => getClientKeyboardSettings ())
|
useState<ClientKeyboardSettings> (() => getClientKeyboardSettings ())
|
||||||
const [registeredHandlers, setRegisteredHandlers] =
|
const [registeredHandlers, setRegisteredHandlers] =
|
||||||
useState<Record<number, ShortcutHandlers>> ({ })
|
useState<Record<number, ShortcutHandlers>> ({ })
|
||||||
|
const [shortcutHelpOpen, setShortcutHelpOpen] = useState (false)
|
||||||
const registrationIdRef = useRef (0)
|
const registrationIdRef = useRef (0)
|
||||||
|
|
||||||
const activeScope = useMemo<ShortcutScope> (
|
const activeScope = useMemo<ShortcutScope> (
|
||||||
@@ -108,6 +139,11 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
|||||||
setKeyboardSettingsState (next)
|
setKeyboardSettingsState (next)
|
||||||
}, [keyboardSettings])
|
}, [keyboardSettings])
|
||||||
|
|
||||||
|
const saveKeyboardSettings = useCallback ((settings: ClientKeyboardSettings) => {
|
||||||
|
const next = setClientKeyboardSettings (settings)
|
||||||
|
setKeyboardSettingsState (next)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const setBinding = useCallback ((
|
const setBinding = useCallback ((
|
||||||
actionId: ShortcutActionId,
|
actionId: ShortcutActionId,
|
||||||
binding: KeyBinding | null,
|
binding: KeyBinding | null,
|
||||||
@@ -157,6 +193,9 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
|||||||
if (isEditableEventTarget (event.target))
|
if (isEditableEventTarget (event.target))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if (shortcutHelpOpen)
|
||||||
|
return
|
||||||
|
|
||||||
const binding = keyBindingFromKeyboardEvent (event)
|
const binding = keyBindingFromKeyboardEvent (event)
|
||||||
if (binding == null)
|
if (binding == null)
|
||||||
return
|
return
|
||||||
@@ -183,21 +222,10 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
|||||||
const builtinHandler = (
|
const builtinHandler = (
|
||||||
{
|
{
|
||||||
'global.openShortcutHelp': () => {
|
'global.openShortcutHelp': () => {
|
||||||
const description =
|
setShortcutHelpOpen (true)
|
||||||
buildShortcutHelpText (activeScope, effectiveBindings)
|
|
||||||
|| 'この画面で使えるショートカットはありません.'
|
|
||||||
toast ({
|
|
||||||
title: 'キーボード・ショートカット',
|
|
||||||
description,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
'global.focusSearch': () => {
|
'global.focusSearch': () => {
|
||||||
const target = document.querySelector<HTMLElement> (
|
focusSearchTarget ()
|
||||||
'[data-shortcut-focus-search="true"]',
|
|
||||||
)
|
|
||||||
target?.focus ()
|
|
||||||
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)
|
|
||||||
target.select ()
|
|
||||||
},
|
},
|
||||||
'navigation.posts': () => navigate ('/posts'),
|
'navigation.posts': () => navigate ('/posts'),
|
||||||
'navigation.tags': () => navigate ('/tags'),
|
'navigation.tags': () => navigate ('/tags'),
|
||||||
@@ -226,6 +254,7 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
|||||||
keyboardSettings.enabled,
|
keyboardSettings.enabled,
|
||||||
mergedRegisteredHandlers,
|
mergedRegisteredHandlers,
|
||||||
navigate,
|
navigate,
|
||||||
|
shortcutHelpOpen,
|
||||||
])
|
])
|
||||||
|
|
||||||
const value = useMemo<KeyboardShortcutsContextValue> (() => ({
|
const value = useMemo<KeyboardShortcutsContextValue> (() => ({
|
||||||
@@ -234,6 +263,7 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
|||||||
effectiveBindings,
|
effectiveBindings,
|
||||||
conflictActionIds,
|
conflictActionIds,
|
||||||
setEnabled,
|
setEnabled,
|
||||||
|
saveKeyboardSettings,
|
||||||
setBinding,
|
setBinding,
|
||||||
resetBinding,
|
resetBinding,
|
||||||
resetAllBindings,
|
resetAllBindings,
|
||||||
@@ -246,6 +276,7 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
|||||||
registerHandlers,
|
registerHandlers,
|
||||||
resetAllBindings,
|
resetAllBindings,
|
||||||
resetBinding,
|
resetBinding,
|
||||||
|
saveKeyboardSettings,
|
||||||
setBinding,
|
setBinding,
|
||||||
setEnabled,
|
setEnabled,
|
||||||
])
|
])
|
||||||
@@ -253,6 +284,12 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
|||||||
return (
|
return (
|
||||||
<KeyboardShortcutsContext.Provider value={value}>
|
<KeyboardShortcutsContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
|
<ShortcutHelpDialog
|
||||||
|
open={shortcutHelpOpen}
|
||||||
|
onOpenChange={setShortcutHelpOpen}
|
||||||
|
activeScope={activeScope}
|
||||||
|
effectiveBindings={effectiveBindings}
|
||||||
|
conflictActionIds={conflictActionIds}/>
|
||||||
</KeyboardShortcutsContext.Provider>)
|
</KeyboardShortcutsContext.Provider>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -254,28 +254,10 @@ const ThemeSection: FC<SharedSectionProps> = (
|
|||||||
</select>)}
|
</select>)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<div className="space-y-2 rounded-lg border border-dashed border-border/70 p-3 text-sm">
|
|
||||||
<p>
|
|
||||||
テーマの選択状態は現在のブラウザに保存し,背景・文字色はそのテーマの base に従います.
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
backend には system / light / dark の mode だけ保存します.
|
|
||||||
カスタムテーマ本体は今は browser-local です.
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
将来は themes テーブルなどへ移して,端末間同期できる構造に拡張できます.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium">タグカテゴリ色</h3>
|
<h3 className="font-medium">タグカテゴリ色</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{selectedTheme.builtin || draftActiveThemeId === 'system'
|
|
||||||
? '組み込みテーマは直接変更されません.色を変更するとカスタムテーマを作成します.'
|
|
||||||
: `現在の編輯対象: ${ selectedTheme.name }`}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -344,14 +326,6 @@ const ThemeSection: FC<SharedSectionProps> = (
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 rounded-lg border border-dashed border-border/70 p-3 text-sm">
|
|
||||||
<p className="font-medium">既存 backend 設定について</p>
|
|
||||||
<p>
|
|
||||||
auto_fetch_title、auto_fetch_thumbnail、wiki_editor_mode は backend 側に
|
|
||||||
保持したままです.この共通設定画面にはまだ接続していません.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="button" onClick={onThemeSubmit}>
|
<Button type="button" onClick={onThemeSubmit}>
|
||||||
テーマを保存
|
テーマを保存
|
||||||
</Button>
|
</Button>
|
||||||
@@ -682,15 +656,40 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
<div className="mx-auto max-w-xl space-y-4 p-4 md:hidden">
|
<div className="mx-auto max-w-xl space-y-4 p-4 md:hidden">
|
||||||
<PageTitle>設定</PageTitle>
|
<PageTitle>設定</PageTitle>
|
||||||
<FieldError messages={settingsError ? [settingsError] : []}/>
|
<FieldError messages={settingsError ? [settingsError] : []}/>
|
||||||
|
<div
|
||||||
|
role="tablist"
|
||||||
|
aria-label="設定区分"
|
||||||
|
aria-orientation="horizontal"
|
||||||
|
className="-mx-4 flex gap-2 overflow-x-auto px-4 pb-1">
|
||||||
|
{tabs.map (tab => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
id={tabButtonId (tab.id)}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={activeTab === tab.id}
|
||||||
|
aria-controls={tabPanelId (tab.id)}
|
||||||
|
className={cn (
|
||||||
|
'shrink-0 rounded-full border px-3 py-2 text-sm font-medium',
|
||||||
|
(activeTab === tab.id
|
||||||
|
? ['border-slate-900 bg-slate-900 text-white',
|
||||||
|
'dark:border-slate-100 dark:bg-slate-100 dark:text-slate-900']
|
||||||
|
: ['border-border bg-background text-foreground']))}
|
||||||
|
onClick={() => setActiveTab (tab.id)}>
|
||||||
|
{tab.label}
|
||||||
|
{settingsTabErrors[tab.id] ? ' !' : ''}
|
||||||
|
</button>))}
|
||||||
|
</div>
|
||||||
{loading ? 'Loading...' : (
|
{loading ? 'Loading...' : (
|
||||||
<>
|
<div
|
||||||
{sectionProps && (
|
id={tabPanelId (activeTab)}
|
||||||
<>
|
role="tabpanel"
|
||||||
<AccountSection {...sectionProps}/>
|
aria-labelledby={tabButtonId (activeTab)}>
|
||||||
<ThemeSection {...sectionProps}/>
|
{sectionProps && activeTab === 'account' && <AccountSection {...sectionProps}/>}
|
||||||
<KeyboardSettingsSection sectionClassName={sectionClassName}/>
|
{sectionProps && activeTab === 'theme' && <ThemeSection {...sectionProps}/>}
|
||||||
</>)}
|
{activeTab === 'keyboard' && (
|
||||||
</>)}
|
<KeyboardSettingsSection sectionClassName={sectionClassName}/>)}
|
||||||
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden md:flex md:justify-center md:px-4">
|
<div className="hidden md:flex md:justify-center md:px-4">
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする