このコミットが含まれているのは:
2026-07-05 11:27:45 +09:00
コミット ca92c62049
11個のファイルの変更1137行の追加92行の削除
+9
ファイルの表示
@@ -19,6 +19,7 @@ export type ShortcutActionId =
| 'settings.account'
| 'settings.theme'
| 'settings.keyboard'
| 'settings.behavior'
export type KeyBinding = {
key: string
@@ -112,6 +113,12 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
scope: 'settings',
defaultBinding: { key: '3' },
},
{
id: 'settings.behavior',
label: '設定: 動作',
scope: 'settings',
defaultBinding: { key: '4' },
},
]
export const SHORTCUT_DEFINITIONS_BY_ID =
@@ -132,6 +139,7 @@ export const SHORTCUT_DEFINITIONS_BY_ID =
'settings.account': SHORTCUT_DEFINITIONS[8],
'settings.theme': SHORTCUT_DEFINITIONS[9],
'settings.keyboard': SHORTCUT_DEFINITIONS[10],
'settings.behavior': SHORTCUT_DEFINITIONS[11],
},
)
@@ -153,6 +161,7 @@ export const DEFAULT_KEY_BINDINGS =
'settings.account': null,
'settings.theme': null,
'settings.keyboard': null,
'settings.behavior': null,
},
)
+135 -1
ファイルの表示
@@ -51,6 +51,9 @@ export type CustomClientTheme =
export type TheatreLayoutMode = 'threeColumns' | 'tagsBottom' | 'commentsBottom'
export type TheatreTagFlow = 'vertical' | 'horizontal'
export type GekanatorBackgroundMotionMode = 'on' | 'calm' | 'off'
export type ClientAnimationMode = 'normal' | 'reduced' | 'off'
export type ClientEmbedAutoLoadMode = 'auto' | 'manual' | 'off'
export type ClientThumbnailMode = 'normal' | 'light' | 'off'
export type ClientPaneBreakpoint = 'desktop' | 'tablet'
export type ClientListKey = 'postList' | 'postSearch' | 'tagList'
export type ClientListLimit = 20 | 50 | 100
@@ -75,6 +78,12 @@ export type ClientAppearanceSettings = {
theme?: UserSettings['theme']
tagColours?: Partial<ThemeTagColours> }
export type ClientBehaviorSettings = {
animation?: ClientAnimationMode
embedAutoLoad?: ClientEmbedAutoLoadMode
thumbnailMode?: ClientThumbnailMode
}
// DB-backed settings. At present only `theme` is surfaced in `/users/settings`.
// The remaining fields are retained for existing backend work and are not wired
// to the common settings UI yet.
@@ -87,6 +96,7 @@ export const DEFAULT_USER_SETTINGS: UserSettings = {
// Browser-local settings only. These are device or screen specific.
export type ClientSettings = {
appearance?: ClientAppearanceSettings
behavior?: ClientBehaviorSettings
keyboard?: ClientKeyboardSettings
panes?: Record<string, ClientPaneSettings>
lists?: Partial<Record<ClientListKey, ClientListSettings>>
@@ -303,6 +313,25 @@ const validListLimit = (value: unknown): ClientListLimit | null =>
value === 20 || value === 50 || value === 100 ? value : null
const normaliseAnimationMode = (value: unknown): ClientAnimationMode | null => {
if (value === 'normal' || value === 'reduced' || value === 'off')
return value
if (value === 'true')
return 'reduced'
if (value === 'false')
return 'normal'
return null
}
const normaliseEmbedAutoLoadMode = (value: unknown): ClientEmbedAutoLoadMode | null =>
value === 'auto' || value === 'manual' || value === 'off' ? value : null
const normaliseThumbnailMode = (value: unknown): ClientThumbnailMode | null =>
value === 'normal' || value === 'light' || value === 'off' ? value : null
const normaliseKeyboardBindings = (
bindings: unknown,
): Partial<Record<ShortcutActionId, KeyBinding | null>> => {
@@ -341,6 +370,110 @@ export const getClientKeyboardSettings = (): ClientKeyboardSettings => {
}
const legacyAnimationMode = (): ClientAnimationMode | null =>
normaliseAnimationMode (loadClientSettings ().reducedMotion)
const legacyEmbedAutoLoadMode = (): ClientEmbedAutoLoadMode | null =>
normaliseEmbedAutoLoadMode (loadClientSettings ().embedAutoLoad)
const legacyThumbnailMode = (): ClientThumbnailMode | null =>
normaliseThumbnailMode (loadClientSettings ().thumbnailMode)
export const getClientAnimationMode = (): ClientAnimationMode =>
loadClientSettings ().behavior?.animation
?? legacyAnimationMode ()
?? 'normal'
export const applyClientAnimationMode = (mode: ClientAnimationMode): void => {
if (typeof document === 'undefined')
return
if (mode === 'normal')
{
delete document.documentElement.dataset.animation
return
}
document.documentElement.dataset.animation = mode
}
export const setClientAnimationMode = (
animation: ClientAnimationMode,
): ClientBehaviorSettings =>
setClientBehaviorSettings ({
...getClientBehaviorSettings (),
animation,
})
export const getClientEmbedAutoLoadMode = (): ClientEmbedAutoLoadMode =>
loadClientSettings ().behavior?.embedAutoLoad
?? legacyEmbedAutoLoadMode ()
?? 'auto'
export const setClientEmbedAutoLoadMode = (
embedAutoLoad: ClientEmbedAutoLoadMode,
): ClientBehaviorSettings =>
setClientBehaviorSettings ({
...getClientBehaviorSettings (),
embedAutoLoad,
})
export const getClientThumbnailMode = (): ClientThumbnailMode =>
loadClientSettings ().behavior?.thumbnailMode
?? legacyThumbnailMode ()
?? 'normal'
export const setClientThumbnailMode = (
thumbnailMode: ClientThumbnailMode,
): ClientBehaviorSettings =>
setClientBehaviorSettings ({
...getClientBehaviorSettings (),
thumbnailMode,
})
export const getClientBehaviorSettings = (): ClientBehaviorSettings => ({
animation: getClientAnimationMode (),
embedAutoLoad: getClientEmbedAutoLoadMode (),
thumbnailMode: getClientThumbnailMode (),
})
export const setClientBehaviorSettings = (
behavior: ClientBehaviorSettings,
): ClientBehaviorSettings => {
const next: ClientBehaviorSettings = {
animation: (
normaliseAnimationMode (behavior.animation)
?? getClientAnimationMode ()
),
embedAutoLoad: (
normaliseEmbedAutoLoadMode (behavior.embedAutoLoad)
?? getClientEmbedAutoLoadMode ()
),
thumbnailMode: (
normaliseThumbnailMode (behavior.thumbnailMode)
?? getClientThumbnailMode ()
),
}
updateClientSettings (settings => ({
...settings,
behavior: next,
}))
applyClientAnimationMode (next.animation ?? 'normal')
return next
}
export const setClientKeyboardSettings = (
keyboard: ClientKeyboardSettings,
): ClientKeyboardSettings => {
@@ -921,7 +1054,8 @@ export const setClientGekanatorBackgroundMotion = (
): void => {
updateClientSettings (settings => ({
...settings,
gekanator: { ...(settings.gekanator ?? { }), backgroundMotion } }))
gekanator: { ...(settings.gekanator ?? { }), backgroundMotion },
}))
}
+42 -66
ファイルの表示
@@ -23,9 +23,6 @@ import {
import {
getClientKeyboardSettings,
getEffectiveKeyBindings,
resetAllClientKeyBindings,
resetClientKeyBinding,
setClientKeyBinding,
setClientKeyboardSettings,
} from '@/lib/settings'
@@ -41,16 +38,12 @@ type ShortcutHandler = () => void
type ShortcutHandlers = Partial<Record<ShortcutActionId, ShortcutHandler>>
type KeyboardShortcutsContextValue = {
activeScope: ShortcutScope
keyboardSettings: ClientKeyboardSettings
effectiveBindings: Record<ShortcutActionId, KeyBinding | null>
conflictActionIds: Set<ShortcutActionId>
activeScope: ShortcutScope
keyboardSettings: ClientKeyboardSettings
effectiveBindings: Record<ShortcutActionId, KeyBinding | null>
conflictActionIds: Set<ShortcutActionId>
saveKeyboardSettings: (settings: ClientKeyboardSettings) => void
setEnabled: (enabled: boolean) => void
setBinding: (actionId: ShortcutActionId, binding: KeyBinding | null) => void
resetBinding: (actionId: ShortcutActionId) => void
resetAllBindings: () => void
registerHandlers: (handlers: ShortcutHandlers) => () => void
registerHandlers: (handlers: ShortcutHandlers) => () => void
}
const KeyboardShortcutsContext =
@@ -131,37 +124,11 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
[registeredHandlers],
)
const setEnabled = useCallback ((enabled: boolean) => {
const next = setClientKeyboardSettings ({
...keyboardSettings,
enabled,
})
setKeyboardSettingsState (next)
}, [keyboardSettings])
const saveKeyboardSettings = useCallback ((settings: ClientKeyboardSettings) => {
const next = setClientKeyboardSettings (settings)
setKeyboardSettingsState (next)
}, [])
const setBinding = useCallback ((
actionId: ShortcutActionId,
binding: KeyBinding | null,
) => {
const next = setClientKeyBinding (actionId, binding)
setKeyboardSettingsState (next)
}, [])
const resetBinding = useCallback ((actionId: ShortcutActionId) => {
const next = resetClientKeyBinding (actionId)
setKeyboardSettingsState (next)
}, [])
const resetAllBindings = useCallback (() => {
const next = resetAllClientKeyBindings ()
setKeyboardSettingsState (next)
}, [])
const registerHandlers = useCallback ((handlers: ShortcutHandlers) => {
const registrationId = ++registrationIdRef.current
@@ -179,6 +146,39 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
}
}, [])
const builtinHandlers = useMemo<ShortcutHandlers> (
() => ({
'global.openShortcutHelp': () => {
setShortcutHelpOpen (true)
},
'global.focusSearch': () => {
focusSearchTarget ()
},
'navigation.posts': () => navigate ('/posts'),
'navigation.tags': () => navigate ('/tags'),
'navigation.materials': () => navigate ('/materials'),
'navigation.wiki': () => navigate ('/wiki'),
'settings.account': () => navigate ('/users/settings?tab=account'),
'settings.theme': () => navigate ('/users/settings?tab=theme'),
'settings.keyboard': () => navigate ('/users/settings?tab=keyboard'),
'settings.behavior': () => navigate ('/users/settings?tab=behavior'),
}),
[navigate],
)
const availableActionIds = useMemo<Set<ShortcutActionId>> (
() => new Set (
SHORTCUT_DEFINITIONS
.filter (definition =>
definition.scope === 'global' || definition.scope === activeScope)
.filter (definition =>
builtinHandlers[definition.id] != null
|| mergedRegisteredHandlers[definition.id] != null)
.map (definition => definition.id),
),
[activeScope, builtinHandlers, mergedRegisteredHandlers],
)
useEffect (() => {
const onKeyDown = (event: KeyboardEvent) => {
if (
@@ -219,24 +219,7 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
return
const actionId = candidates[0].id
const builtinHandler = (
{
'global.openShortcutHelp': () => {
setShortcutHelpOpen (true)
},
'global.focusSearch': () => {
focusSearchTarget ()
},
'navigation.posts': () => navigate ('/posts'),
'navigation.tags': () => navigate ('/tags'),
'navigation.materials': () => navigate ('/materials'),
'navigation.wiki': () => navigate ('/wiki'),
'settings.account': () => navigate ('/users/settings?tab=account'),
'settings.theme': () => navigate ('/users/settings?tab=theme'),
'settings.keyboard': () => navigate ('/users/settings?tab=keyboard'),
} as ShortcutHandlers
)[actionId]
const handler = builtinHandler ?? mergedRegisteredHandlers[actionId]
const handler = builtinHandlers[actionId] ?? mergedRegisteredHandlers[actionId]
if (handler == null)
return
@@ -251,9 +234,9 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
activeScope,
conflictActionIds,
effectiveBindings,
builtinHandlers,
keyboardSettings.enabled,
mergedRegisteredHandlers,
navigate,
shortcutHelpOpen,
])
@@ -262,11 +245,7 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
keyboardSettings,
effectiveBindings,
conflictActionIds,
setEnabled,
saveKeyboardSettings,
setBinding,
resetBinding,
resetAllBindings,
registerHandlers,
}), [
activeScope,
@@ -274,11 +253,7 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
effectiveBindings,
keyboardSettings,
registerHandlers,
resetAllBindings,
resetBinding,
saveKeyboardSettings,
setBinding,
setEnabled,
])
return (
@@ -289,7 +264,8 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
onOpenChange={setShortcutHelpOpen}
activeScope={activeScope}
effectiveBindings={effectiveBindings}
conflictActionIds={conflictActionIds}/>
conflictActionIds={conflictActionIds}
availableActionIds={availableActionIds}/>
</KeyboardShortcutsContext.Provider>)
}