このコミットが含まれているのは:
@@ -7,7 +7,7 @@ import { useOverlayStore } from '@/components/RouteBlockerOverlay'
|
||||
import { prefetchForURL } from '@/lib/prefetchers'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { AnchorHTMLAttributes, KeyboardEvent, MouseEvent, TouchEvent } from 'react'
|
||||
import type { AnchorHTMLAttributes, MouseEvent, TouchEvent } from 'react'
|
||||
import type { To } from 'react-router-dom'
|
||||
|
||||
type Props = AnchorHTMLAttributes<HTMLAnchorElement> & {
|
||||
@@ -25,7 +25,6 @@ export default forwardRef<HTMLAnchorElement, Props> (({
|
||||
state,
|
||||
onMouseEnter,
|
||||
onTouchStart,
|
||||
onKeyDown,
|
||||
onClick,
|
||||
cancelOnError = false,
|
||||
...rest }, ref) => {
|
||||
@@ -98,22 +97,11 @@ export default forwardRef<HTMLAnchorElement, Props> (({
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (ev: KeyboardEvent<HTMLAnchorElement>) => {
|
||||
onKeyDown?.(ev)
|
||||
|
||||
if (ev.defaultPrevented)
|
||||
return
|
||||
|
||||
if (ev.key === ' ' || ev.key === 'Spacebar')
|
||||
ev.preventDefault ()
|
||||
}
|
||||
|
||||
return (
|
||||
<a ref={ref}
|
||||
href={typeof to === 'string' ? to : createPath (to)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onTouchStart={handleTouchStart}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={handleClick}
|
||||
className={cn ('cursor-pointer', className)}
|
||||
{...rest}/>)
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import FormField from '@/components/common/FormField'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import {
|
||||
getClientBehaviorSettings,
|
||||
setClientBehaviorSettings,
|
||||
} from '@/lib/settings'
|
||||
import { inputClass } from '@/lib/utils'
|
||||
|
||||
import type {
|
||||
ClientAnimationMode,
|
||||
ClientBehaviorSettings,
|
||||
ClientEmbedAutoLoadMode,
|
||||
ClientThumbnailMode,
|
||||
} from '@/lib/settings'
|
||||
import type { FC } from 'react'
|
||||
|
||||
type Props = {
|
||||
sectionClassName: string
|
||||
}
|
||||
|
||||
|
||||
const BehaviorSettingsSection: FC<Props> = ({ sectionClassName }) => {
|
||||
const [savedSettings, setSavedSettings] =
|
||||
useState<ClientBehaviorSettings> (() => getClientBehaviorSettings ())
|
||||
const [draftSettings, setDraftSettings] =
|
||||
useState<ClientBehaviorSettings> (() => getClientBehaviorSettings ())
|
||||
|
||||
useEffect (() => {
|
||||
const current = getClientBehaviorSettings ()
|
||||
setSavedSettings (current)
|
||||
setDraftSettings (current)
|
||||
}, [])
|
||||
|
||||
const hasUnsavedChanges = useMemo (
|
||||
() => JSON.stringify (draftSettings) !== JSON.stringify (savedSettings),
|
||||
[draftSettings, savedSettings],
|
||||
)
|
||||
|
||||
const updateDraft = <Key extends keyof ClientBehaviorSettings,> (
|
||||
key: Key,
|
||||
value: ClientBehaviorSettings[Key],
|
||||
) => {
|
||||
setDraftSettings (current => ({ ...current, [key]: value }))
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
const next = setClientBehaviorSettings (draftSettings)
|
||||
setSavedSettings (next)
|
||||
setDraftSettings (next)
|
||||
toast ({ title: '動作設定を保存しました' })
|
||||
}
|
||||
|
||||
const handleDiscard = () => {
|
||||
setDraftSettings (savedSettings)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={sectionClassName}>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-xl font-bold">動作</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
この設定は現在のブラウザに保存されます。
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
重い端末や通信量を抑えたい場合に調整します。
|
||||
</p>
|
||||
{hasUnsavedChanges && (
|
||||
<p className="text-sm text-amber-700 dark:text-amber-300">
|
||||
未保存の変更があります
|
||||
</p>)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={!(hasUnsavedChanges)}
|
||||
onClick={handleDiscard}>
|
||||
変更を破棄
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!(hasUnsavedChanges)}
|
||||
onClick={handleSave}>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<FormField label="アニメーション">
|
||||
{() => (
|
||||
<select
|
||||
className={inputClass (false)}
|
||||
value={draftSettings.animation ?? 'normal'}
|
||||
onChange={ev => updateDraft (
|
||||
'animation',
|
||||
ev.target.value as ClientAnimationMode,
|
||||
)}>
|
||||
<option value="normal">通常</option>
|
||||
<option value="reduced">控えめ</option>
|
||||
<option value="off">なし</option>
|
||||
</select>)}
|
||||
</FormField>
|
||||
|
||||
<FormField label="埋め込み自動読込">
|
||||
{() => (
|
||||
<select
|
||||
className={inputClass (false)}
|
||||
value={draftSettings.embedAutoLoad ?? 'auto'}
|
||||
onChange={ev => updateDraft (
|
||||
'embedAutoLoad',
|
||||
ev.target.value as ClientEmbedAutoLoadMode,
|
||||
)}>
|
||||
<option value="auto">自動</option>
|
||||
<option value="manual">クリック時</option>
|
||||
<option value="off">読み込まない</option>
|
||||
</select>)}
|
||||
</FormField>
|
||||
|
||||
<FormField label="サムネイル表示">
|
||||
{() => (
|
||||
<select
|
||||
className={inputClass (false)}
|
||||
value={draftSettings.thumbnailMode ?? 'normal'}
|
||||
onChange={ev => updateDraft (
|
||||
'thumbnailMode',
|
||||
ev.target.value as ClientThumbnailMode,
|
||||
)}>
|
||||
<option value="normal">通常</option>
|
||||
<option value="light">軽量</option>
|
||||
<option value="off">非表示</option>
|
||||
</select>)}
|
||||
</FormField>
|
||||
</div>
|
||||
</section>)
|
||||
}
|
||||
|
||||
export default BehaviorSettingsSection
|
||||
@@ -24,14 +24,24 @@ type Props = {
|
||||
activeScope: ShortcutScope
|
||||
effectiveBindings: Record<ShortcutActionId, KeyBinding | null>
|
||||
conflictActionIds: Set<ShortcutActionId>
|
||||
availableActionIds: Set<ShortcutActionId>
|
||||
}
|
||||
|
||||
|
||||
const ShortcutHelpDialog: FC<Props> = (
|
||||
{ open, onOpenChange, activeScope, effectiveBindings, conflictActionIds },
|
||||
{
|
||||
open,
|
||||
onOpenChange,
|
||||
activeScope,
|
||||
effectiveBindings,
|
||||
conflictActionIds,
|
||||
availableActionIds,
|
||||
},
|
||||
) => {
|
||||
const rows = SHORTCUT_DEFINITIONS.filter (definition =>
|
||||
definition.scope === 'global' || definition.scope === activeScope)
|
||||
const rows = SHORTCUT_DEFINITIONS
|
||||
.filter (definition =>
|
||||
definition.scope === 'global' || definition.scope === activeScope)
|
||||
.filter (definition => availableActionIds.has (definition.id))
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
|
||||
@@ -134,6 +134,21 @@
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
:root[data-animation='reduced'] *,
|
||||
:root[data-animation='off'] *
|
||||
{
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
|
||||
:root[data-animation='off'] *,
|
||||
:root[data-animation='off'] *::before,
|
||||
:root[data-animation='off'] *::after
|
||||
{
|
||||
animation-duration: 0.001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.001ms !important;
|
||||
}
|
||||
|
||||
a
|
||||
{
|
||||
font-weight: 500;
|
||||
|
||||
@@ -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 },
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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>)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { HelmetProvider } from 'react-helmet-async'
|
||||
|
||||
import '@/index.css'
|
||||
import App from '@/App'
|
||||
import { applyClientAnimationMode, getClientAnimationMode } from '@/lib/settings'
|
||||
|
||||
const helmetContext = { }
|
||||
|
||||
@@ -13,6 +14,8 @@ const client = new QueryClient ({
|
||||
gcTime: 30 * 60 * 1000,
|
||||
retry: 1 } } })
|
||||
|
||||
applyClientAnimationMode (getClientAnimationMode ())
|
||||
|
||||
createRoot (document.getElementById ('root')!).render (
|
||||
<HelmetProvider context={helmetContext}>
|
||||
<QueryClientProvider client={client}>
|
||||
|
||||
@@ -8,6 +8,7 @@ import Label from '@/components/common/Label'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import TagLink from '@/components/TagLink'
|
||||
import BehaviorSettingsSection from '@/components/users/BehaviorSettingsSection'
|
||||
import KeyboardSettingsSection from '@/components/users/KeyboardSettingsSection'
|
||||
import InheritDialogue from '@/components/users/InheritDialogue'
|
||||
import UserCodeDialogue from '@/components/users/UserCodeDialogue'
|
||||
@@ -48,7 +49,7 @@ type Props = {
|
||||
|
||||
type UserFormField = 'name'
|
||||
type SettingsFormField = 'theme'
|
||||
type SettingsTab = 'account' | 'theme' | 'keyboard'
|
||||
type SettingsTab = 'account' | 'theme' | 'keyboard' | 'behavior'
|
||||
|
||||
type TabSpec = {
|
||||
id: SettingsTab
|
||||
@@ -79,7 +80,8 @@ type SharedSectionProps = {
|
||||
const tabs: TabSpec[] = [
|
||||
{ id: 'account', label: 'アカウント' },
|
||||
{ id: 'theme', label: 'テーマ' },
|
||||
{ id: 'keyboard', label: 'キーボード' } ]
|
||||
{ id: 'keyboard', label: 'キーボード' },
|
||||
{ id: 'behavior', label: '動作' } ]
|
||||
|
||||
const sectionClassName =
|
||||
'space-y-4 rounded-xl border border-border bg-background/80 p-4'
|
||||
@@ -379,7 +381,8 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
() => (
|
||||
{ account: Boolean (userFieldErrors.name?.length),
|
||||
theme: Boolean (settingsFieldErrors.theme?.length),
|
||||
keyboard: conflictActionIds.size > 0 }),
|
||||
keyboard: conflictActionIds.size > 0,
|
||||
behavior: false }),
|
||||
[conflictActionIds.size, settingsFieldErrors.theme, userFieldErrors.name])
|
||||
|
||||
const applyDraftThemeSelection = (
|
||||
@@ -689,6 +692,8 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
{sectionProps && activeTab === 'theme' && <ThemeSection {...sectionProps}/>}
|
||||
{activeTab === 'keyboard' && (
|
||||
<KeyboardSettingsSection sectionClassName={sectionClassName}/>)}
|
||||
{activeTab === 'behavior' && (
|
||||
<BehaviorSettingsSection sectionClassName={sectionClassName}/>)}
|
||||
</div>)}
|
||||
</div>
|
||||
|
||||
@@ -736,6 +741,8 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
{activeTab === 'theme' && <ThemeSection {...sectionProps}/>}
|
||||
{activeTab === 'keyboard' && (
|
||||
<KeyboardSettingsSection sectionClassName={sectionClassName}/>)}
|
||||
{activeTab === 'behavior' && (
|
||||
<BehaviorSettingsSection sectionClassName={sectionClassName}/>)}
|
||||
</>)}
|
||||
</div>)}
|
||||
</div>
|
||||
|
||||
新しい課題から参照
ユーザをブロックする