このコミットが含まれているのは:
@@ -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}>
|
||||
|
||||
新しい課題から参照
ユーザをブロックする