このコミットが含まれているのは:
@@ -142,23 +142,34 @@ const TopNav: FC<Props> = ({ user, locales }) => {
|
||||
const layoutRef = useRef<HTMLDivElement | null> (null)
|
||||
const barRef = useRef<HTMLElement | null> (null)
|
||||
const submenuContentRef = useRef<HTMLDivElement | null> (null)
|
||||
const highlightTargetRef = useRef (-1)
|
||||
|
||||
const measure = (idx: number) => {
|
||||
const nav = navRef.current
|
||||
const el = itemsRef.current[idx < 0 ? visibleMenu.length : idx]
|
||||
|
||||
if (!(nav) || !(el))
|
||||
{
|
||||
setHL ({ left: 0, width: 0, visible: true })
|
||||
return
|
||||
}
|
||||
if (nav == null || el == null || nav.offsetWidth === 0
|
||||
|| layoutRef.current?.dataset.desktop !== 'true')
|
||||
{
|
||||
setHL (current => current.visible
|
||||
? { left: 0, width: 0, visible: false } : current)
|
||||
return
|
||||
}
|
||||
|
||||
const navRect = nav.getBoundingClientRect ()
|
||||
const elRect = el.getBoundingClientRect ()
|
||||
|
||||
setHL ({ left: elRect.left - navRect.left,
|
||||
width: elRect.width,
|
||||
visible: true })
|
||||
// Layout offsets exclude Framer Motion's temporary projection transforms.
|
||||
let left = 0
|
||||
let node: HTMLElement | null = el
|
||||
while (node != null && node !== nav)
|
||||
{
|
||||
left += node.offsetLeft
|
||||
const parent: Element | null = node.offsetParent
|
||||
node = parent instanceof HTMLElement ? parent : null
|
||||
}
|
||||
const width = node === nav ? el.offsetWidth : 0
|
||||
const visible = width > 0
|
||||
setHL (current =>
|
||||
(current.left === left && current.width === width && current.visible === visible)
|
||||
? current : { left, width, visible })
|
||||
}
|
||||
|
||||
const [hl, setHL] = useState<{ left: number; width: number; visible: boolean }> ({
|
||||
@@ -200,11 +211,14 @@ const TopNav: FC<Props> = ({ user, locales }) => {
|
||||
|
||||
const menu = menuOutline ({ tag, material, wikiId, user, pathName: location.pathname })
|
||||
const visibleMenu = menu.filter ((item): item is MenuVisibleItem => item.visible ?? true)
|
||||
const menuLabels = visibleMenu.map (item => item.name).join ('\u0000')
|
||||
const moreMenu = menu.filter (item =>
|
||||
!(item.visible ?? true)
|
||||
|| item.subMenu.filter (subItem => subItem.visible ?? true).length > 0)
|
||||
const activeIdx =
|
||||
visibleMenu.findIndex (item => pathname.startsWith (item.base || item.to))
|
||||
highlightTargetRef.current = moreVsbl ? -1 : activeIdx
|
||||
const activeSubmenuId = `submenu-${ menu.indexOf (visibleMenu[activeIdx]) }`
|
||||
const topNavTransition =
|
||||
animationsOff
|
||||
? { duration: 0 }
|
||||
@@ -300,6 +314,9 @@ const TopNav: FC<Props> = ({ user, locales }) => {
|
||||
typeof ResizeObserver === 'undefined' ? null : new ResizeObserver (schedule)
|
||||
observer?.observe (bar)
|
||||
observer?.observe (primary)
|
||||
for (const item of itemsRef.current)
|
||||
if (item != null)
|
||||
observer?.observe (item)
|
||||
if (account != null)
|
||||
observer?.observe (account)
|
||||
update ()
|
||||
@@ -314,7 +331,13 @@ const TopNav: FC<Props> = ({ user, locales }) => {
|
||||
removeEventListener ('resize', schedule)
|
||||
document.fonts?.removeEventListener ('loadingdone', schedule)
|
||||
}
|
||||
}, [activeIdx, moreVsbl, desktop, density, user?.name, locale, locales])
|
||||
}, [activeIdx, moreVsbl, desktop, density, user?.name, locale, locales, menuLabels])
|
||||
|
||||
useLayoutEffect (() => {
|
||||
measure (moreVsbl ? -1 : activeIdx)
|
||||
if (!(desktop))
|
||||
setMoreVsbl (false)
|
||||
}, [activeIdx, moreVsbl, desktop, density, locale, animationMode, menuLabels])
|
||||
|
||||
useEffect (() => {
|
||||
const unsubscribe = WikiIdBus.subscribe (setWikiId)
|
||||
@@ -403,7 +426,8 @@ const TopNav: FC<Props> = ({ user, locales }) => {
|
||||
className="shrink-0 whitespace-nowrap"
|
||||
{...(animationsOff
|
||||
? { }
|
||||
: { layoutId: `menu-${ item.name }` })}
|
||||
: { layoutId: `menu-${ menu.indexOf (item) }` })}
|
||||
onLayoutAnimationComplete={() => measure (highlightTargetRef.current)}
|
||||
animate={{ opacity: moreVsbl ? 0 : 1 }}
|
||||
transition={{ opacity: opacityTransition,
|
||||
layout: topNavTransition }}
|
||||
@@ -472,7 +496,10 @@ const TopNav: FC<Props> = ({ user, locales }) => {
|
||||
}}
|
||||
transition={{ layout: topNavTransition }}
|
||||
onAnimationComplete={() => {
|
||||
measure (moreVsbl ? -1 : activeIdx)
|
||||
measure (highlightTargetRef.current)
|
||||
}}
|
||||
onLayoutAnimationComplete={() => {
|
||||
measure (highlightTargetRef.current)
|
||||
}}>
|
||||
<div ref={submenuContentRef} className={cn (moreVsbl && 'top-nav-more-menu')}>
|
||||
{moreVsbl
|
||||
@@ -484,10 +511,11 @@ const TopNav: FC<Props> = ({ user, locales }) => {
|
||||
{...(animationsOff
|
||||
? { }
|
||||
: ((item.visible ?? true)
|
||||
? { layoutId: `menu-${ item.name }` }
|
||||
? { layoutId: `menu-${ menu.indexOf (item) }` }
|
||||
: { initial: { x: 40, y: -40, opacity: 0 },
|
||||
animate: { x: 0, y: 0, opacity: 1 },
|
||||
exit: { x: 40, y: -40, opacity: 0 } }))}
|
||||
onLayoutAnimationComplete={() => measure (highlightTargetRef.current)}
|
||||
className="z-10 min-w-0 px-3 py-2 font-bold">
|
||||
<h2>
|
||||
<LocalisedText language={item.nameLanguage ?? 'ja'}>
|
||||
@@ -507,9 +535,8 @@ const TopNav: FC<Props> = ({ user, locales }) => {
|
||||
transition={topNavTransition}
|
||||
{...(animationsOff
|
||||
? { }
|
||||
: ((visibleMenu[activeIdx]?.name
|
||||
=== item.name)
|
||||
? { layoutId: `submenu-${ item.name }-${ j }` }
|
||||
: ((visibleMenu[activeIdx] === item)
|
||||
? { layoutId: `submenu-${ menu.indexOf (item) }-${ j }` }
|
||||
: { initial: { y: -40, opacity: 0 },
|
||||
animate: { y: 0, opacity: 1 },
|
||||
exit: { y: -40, opacity: 0 } }))}>
|
||||
@@ -522,9 +549,8 @@ const TopNav: FC<Props> = ({ user, locales }) => {
|
||||
transition={topNavTransition}
|
||||
{...(animationsOff
|
||||
? { }
|
||||
: ((visibleMenu[activeIdx]?.name
|
||||
=== item.name)
|
||||
? { layoutId: `submenu-${ item.name }-${ j }` }
|
||||
: ((visibleMenu[activeIdx] === item)
|
||||
? { layoutId: `submenu-${ menu.indexOf (item) }-${ j }` }
|
||||
: { initial: { y: -40, opacity: 0 },
|
||||
animate: { y: 0, opacity: 1 },
|
||||
exit: { y: -40, opacity: 0 } }))}>
|
||||
@@ -591,7 +617,7 @@ const TopNav: FC<Props> = ({ user, locales }) => {
|
||||
key={`c-${ i }`}
|
||||
className="min-w-0 max-w-full"
|
||||
transition={{ layout: topNavTransition }}
|
||||
layoutId={`submenu-${ visibleMenu[activeIdx].name }-${ i }`}>
|
||||
layoutId={`${ activeSubmenuId }-${ i }`}>
|
||||
{item.component}
|
||||
</motion.div>)
|
||||
: (
|
||||
@@ -599,7 +625,7 @@ const TopNav: FC<Props> = ({ user, locales }) => {
|
||||
key={`l-${ i }`}
|
||||
className="min-w-0 max-w-full"
|
||||
transition={{ layout: topNavTransition }}
|
||||
layoutId={`submenu-${ visibleMenu[activeIdx].name }-${ i }`}>
|
||||
layoutId={`${ activeSubmenuId }-${ i }`}>
|
||||
<PrefetchLink
|
||||
to={item.to}
|
||||
target={item.to.slice (0, 2) === '//'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FC } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
|
||||
type Props = { id?: string
|
||||
messages?: string[] }
|
||||
messages?: ReactNode[] }
|
||||
|
||||
|
||||
export const FieldError: FC<Props> = ({ id, messages }: Props) => {
|
||||
|
||||
@@ -179,6 +179,7 @@ const DialogueProvider: FC<Props> = ({ children }) => {
|
||||
closeRequest (active.id, active.kind !== 'confirm' && null)
|
||||
}}>
|
||||
<DialogContent
|
||||
closeLabel={active.kind === 'confirm' ? active.options.closeLabel : undefined}
|
||||
className={
|
||||
active.kind === 'form'
|
||||
? `flex max-h-[calc(100dvh-1rem)] ${ active.options.size === 'large'
|
||||
@@ -323,7 +324,8 @@ const DialogueProvider: FC<Props> = ({ children }) => {
|
||||
if (!(open))
|
||||
closeNestedConfirm (false)
|
||||
}}>
|
||||
<DialogContent className="px-6 pb-6 pt-7">
|
||||
<DialogContent className="px-6 pb-6 pt-7"
|
||||
closeLabel={nestedConfirm.options.closeLabel}>
|
||||
<DialogHeader className="pl-8">
|
||||
<DialogTitle>{nestedConfirm.options.title}</DialogTitle>
|
||||
|
||||
|
||||
@@ -32,8 +32,9 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
& { closeLabel?: React.ReactNode }
|
||||
>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
({ className, children, closeLabel = '閉ぢる', ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
@@ -59,7 +60,7 @@ const DialogContent = React.forwardRef<
|
||||
'dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-50',
|
||||
'focus:outline-none focus:ring-2 focus:ring-slate-400')}>
|
||||
<X className="h-4 w-4"/>
|
||||
<span className="sr-only">閉ぢる</span>
|
||||
<span className="sr-only">{closeLabel}</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>))
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { getClientBehaviorSettings, setClientBehaviorSettings } from '@/lib/settings'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { FC, ReactNode } from 'react'
|
||||
@@ -22,7 +23,7 @@ type Props = {
|
||||
|
||||
type SegmentedOption<T extends string> = {
|
||||
value: T
|
||||
label: string }
|
||||
label: ReactNode }
|
||||
|
||||
type SegmentedControlProps<T extends string> = {
|
||||
value: T
|
||||
@@ -30,27 +31,18 @@ type SegmentedControlProps<T extends string> = {
|
||||
onChange: (value: T) => void }
|
||||
|
||||
type SettingBlockProps = {
|
||||
title: string
|
||||
description?: string
|
||||
title: ReactNode
|
||||
description?: ReactNode
|
||||
children: ReactNode }
|
||||
|
||||
const animationOptions: SegmentedOption<ClientAnimationMode>[] = [
|
||||
{ value: 'off', label: 'なし' },
|
||||
{ value: 'reduced', label: '控えめ' },
|
||||
{ value: 'normal', label: '通常' }]
|
||||
const animationOptions: ClientAnimationMode[] = ['off', 'reduced', 'normal']
|
||||
|
||||
const embedAutoLoadOptions: SegmentedOption<ClientEmbedAutoLoadMode>[] = [
|
||||
{ value: 'off', label: '読み込まない' },
|
||||
{ value: 'manual', label: 'クリック時' },
|
||||
{ value: 'auto', label: '自動' }]
|
||||
const embedAutoLoadOptions: ClientEmbedAutoLoadMode[] = ['off', 'manual', 'auto']
|
||||
|
||||
const linkPreloadOptions: SegmentedOption<ClientLinkPreloadMode>[] = [
|
||||
{ value: 'off', label: 'しない' },
|
||||
{ value: 'intent', label: 'する' }]
|
||||
const linkPreloadOptions: ClientLinkPreloadMode[] = ['off', 'intent']
|
||||
|
||||
const tagRelationDisplayOptions: ClientTagRelationDisplayMode[] = ['flat', 'grouped']
|
||||
|
||||
const tagRelationDisplayOptions: SegmentedOption<ClientTagRelationDisplayMode>[] = [
|
||||
{ value: 'flat', label: 'まとめない' },
|
||||
{ value: 'grouped', label: 'まとめる' }]
|
||||
|
||||
const SegmentedControl = <T extends string,> (
|
||||
{ value, options, onChange }: SegmentedControlProps<T>,
|
||||
@@ -92,10 +84,9 @@ const SettingBlock: FC<SettingBlockProps> = (
|
||||
</div>)
|
||||
|
||||
|
||||
const BehaviourSettingsSection: FC<Props> = (
|
||||
{ sectionClassName,
|
||||
onDirtyStateChange },
|
||||
) => {
|
||||
const BehaviourSettingsSection: FC<Props> = props => {
|
||||
const { sectionClassName, onDirtyStateChange } = props
|
||||
const { label } = useUITranslation ('settings')
|
||||
const [savedSettings, setSavedSettings] =
|
||||
useState<ClientBehaviorSettings> (() => getClientBehaviorSettings ())
|
||||
const [draftSettings, setDraftSettings] =
|
||||
@@ -122,7 +113,7 @@ const BehaviourSettingsSection: FC<Props> = (
|
||||
const next = setClientBehaviorSettings (draftSettings)
|
||||
setSavedSettings (next)
|
||||
setDraftSettings (next)
|
||||
toast ({ title: '動作設定を保存しました' })
|
||||
toast ({ title: label ('behavior.saved') })
|
||||
}
|
||||
|
||||
const handleDiscard = () => {
|
||||
@@ -141,7 +132,7 @@ const BehaviourSettingsSection: FC<Props> = (
|
||||
<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>
|
||||
<h2 className="text-xl font-bold">{label ('tabs.behavior')}</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -150,45 +141,49 @@ const BehaviourSettingsSection: FC<Props> = (
|
||||
variant="destructive"
|
||||
disabled={hasUnsavedChanges === false}
|
||||
onClick={handleDiscard}>
|
||||
変更を破棄
|
||||
{label ('actions.discard')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={hasUnsavedChanges === false}
|
||||
onClick={handleSave}>
|
||||
保存
|
||||
{label ('actions.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingBlock title="アニメーション">
|
||||
<SettingBlock title={label ('behavior.animation')}>
|
||||
<SegmentedControl
|
||||
value={draftSettings.animation ?? 'normal'}
|
||||
options={animationOptions}
|
||||
options={animationOptions.map (value => ({ value,
|
||||
label: label (`behavior.animationOptions.${value}`) }))}
|
||||
onChange={value => updateDraft ('animation', value)}/>
|
||||
</SettingBlock>
|
||||
|
||||
<SettingBlock
|
||||
title="リンク先の先読み"
|
||||
description="マウスを置いた時点でリンク先のデータを取得します。">
|
||||
title={label ('behavior.preload')}
|
||||
description={label ('behavior.preloadDescription')}>
|
||||
<SegmentedControl
|
||||
value={draftSettings.linkPreload ?? 'intent'}
|
||||
options={linkPreloadOptions}
|
||||
options={linkPreloadOptions.map (value => ({ value,
|
||||
label: label (`behavior.preloadOptions.${value}`) }))}
|
||||
onChange={value => updateDraft ('linkPreload', value)}/>
|
||||
</SettingBlock>
|
||||
|
||||
<SettingBlock title="タグの親子関係表示">
|
||||
<SettingBlock title={label ('behavior.tagRelations')}>
|
||||
<SegmentedControl
|
||||
value={draftSettings.tagRelationDisplay ?? 'grouped'}
|
||||
options={tagRelationDisplayOptions}
|
||||
options={tagRelationDisplayOptions.map (value => ({ value,
|
||||
label: label (`behavior.relationOptions.${value}`) }))}
|
||||
onChange={value => updateDraft ('tagRelationDisplay', value)}/>
|
||||
</SettingBlock>
|
||||
|
||||
<SettingBlock title="埋め込み自動読込">
|
||||
<SettingBlock title={label ('behavior.embeds')}>
|
||||
<SegmentedControl
|
||||
value={draftSettings.embedAutoLoad ?? 'auto'}
|
||||
options={embedAutoLoadOptions}
|
||||
options={embedAutoLoadOptions.map (value => ({ value,
|
||||
label: label (`behavior.embedOptions.${value}`) }))}
|
||||
onChange={value => updateDraft ('embedAutoLoad', value)}/>
|
||||
</SettingBlock>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { FC } from 'react'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useDialogue } from '@/components/dialogues/DialogueProvider'
|
||||
import { useDialogue } from '@/lib/dialogues/useDialogue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog,
|
||||
DialogContent,
|
||||
@@ -12,6 +10,9 @@ import { Dialog,
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { apiPost } from '@/lib/api'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
import type { User } from '@/types'
|
||||
|
||||
@@ -22,14 +23,17 @@ type Props = { visible: boolean
|
||||
|
||||
const InheritDialogue: FC<Props> = ({ visible, onVisibleChange, setUser }) => {
|
||||
const dialogue = useDialogue ()
|
||||
const { message, label } = useUITranslation ('settings')
|
||||
|
||||
const [inputCode, setInputCode] = useState ('')
|
||||
|
||||
const handleTransfer = async () => {
|
||||
if (!(await dialogue.confirm ({
|
||||
title: '引継ぎを行ってもよろしいですか?',
|
||||
description: '現在のアカウントからはログアウトされます.',
|
||||
confirmText: '引継ぐ',
|
||||
title: label ('transfer.confirm'),
|
||||
description: label ('transfer.logoutWarning'),
|
||||
confirmText: label ('transfer.submit'),
|
||||
cancelText: label ('actions.cancel'),
|
||||
closeLabel: label ('actions.close'),
|
||||
variant: 'danger' })))
|
||||
return
|
||||
|
||||
@@ -41,29 +45,31 @@ const InheritDialogue: FC<Props> = ({ visible, onVisibleChange, setUser }) => {
|
||||
{
|
||||
localStorage.setItem ('user_code', inputCode)
|
||||
setUser (data.user)
|
||||
toast ({ title: '引継ぎ成功!' })
|
||||
toast ({ title: label ('transfer.success') })
|
||||
onVisibleChange (false)
|
||||
}
|
||||
else
|
||||
toast ({ title: '認証失敗', description: 'そのコードは使へません.' })
|
||||
toast ({ title: label ('transfer.authFailed'), description: label ('transfer.invalidCode') })
|
||||
}
|
||||
catch
|
||||
{
|
||||
toast ({ title: '通信エラー', description: 'またあとで試してね.' })
|
||||
toast ({ title: label ('transfer.networkError'), description: label ('transfer.retryLater') })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={onVisibleChange}>
|
||||
<DialogContent className="px-6 pp-6 pt-7">
|
||||
<DialogContent className="px-6 pp-6 pt-7" closeLabel={label ('actions.close')}>
|
||||
<DialogHeader className="pl-8">
|
||||
<DialogTitle>ほかのブラウザから引継ぐ</DialogTitle>
|
||||
<DialogTitle>{label ('account.inherit')}</DialogTitle>
|
||||
<DialogDescription asChild>
|
||||
<div className="flex gap-2">
|
||||
<Input placeholder="引継ぎコードを入力"
|
||||
<Input placeholder={message ('transfer.placeholder').text}
|
||||
aria-label={message ('transfer.placeholder').text}
|
||||
lang={message ('transfer.placeholder').language}
|
||||
value={inputCode}
|
||||
onChange={ev => setInputCode (ev.target.value)}/>
|
||||
<Button onClick={handleTransfer}>引継ぐ</Button>
|
||||
<Button onClick={handleTransfer}>{label ('transfer.submit')}</Button>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -8,14 +8,15 @@ import {
|
||||
isSpaceBinding,
|
||||
keyBindingFromKeyboardEvent,
|
||||
SHORTCUT_DEFINITIONS,
|
||||
SHORTCUT_SCOPE_LABELS,
|
||||
} from '@/lib/keyboardShortcuts'
|
||||
import { getEffectiveKeyBindings } from '@/lib/settings'
|
||||
import { useKeyboardShortcutSettings } from '@/lib/useKeyboardShortcuts'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
import type { ShortcutActionId } from '@/lib/keyboardShortcuts'
|
||||
import type { ClientKeyboardSettings } from '@/lib/settings'
|
||||
import type { FC } from 'react'
|
||||
|
||||
type Props = {
|
||||
sectionClassName: string
|
||||
@@ -26,10 +27,9 @@ type Props = {
|
||||
}
|
||||
|
||||
|
||||
const KeyboardSettingsSection: FC<Props> = (
|
||||
{ sectionClassName,
|
||||
onDirtyStateChange },
|
||||
) => {
|
||||
const KeyboardSettingsSection: FC<Props> = props => {
|
||||
const { sectionClassName, onDirtyStateChange } = props
|
||||
const { label } = useUITranslation ('settings')
|
||||
const {
|
||||
keyboardSettings,
|
||||
saveKeyboardSettings,
|
||||
@@ -92,7 +92,7 @@ const KeyboardSettingsSection: FC<Props> = (
|
||||
|
||||
if (isSpaceBinding (binding))
|
||||
{
|
||||
toast ({ title: 'Space は今は割り当てられません.' })
|
||||
toast ({ title: label ('shortcuts.spaceUnavailable') })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ const KeyboardSettingsSection: FC<Props> = (
|
||||
|
||||
document.addEventListener ('keydown', onKeyDown, true)
|
||||
return () => document.removeEventListener ('keydown', onKeyDown, true)
|
||||
}, [capturingActionId])
|
||||
}, [capturingActionId, label])
|
||||
|
||||
const rows = useMemo (() => SHORTCUT_DEFINITIONS.map (definition => ({
|
||||
...definition,
|
||||
@@ -126,7 +126,7 @@ const KeyboardSettingsSection: FC<Props> = (
|
||||
|
||||
const handleSave = () => {
|
||||
saveKeyboardSettings (draftKeyboardSettings)
|
||||
toast ({ title: 'キーボード設定を保存しました' })
|
||||
toast ({ title: label ('shortcuts.saved') })
|
||||
}
|
||||
|
||||
const handleDiscard = () => {
|
||||
@@ -146,7 +146,7 @@ const KeyboardSettingsSection: FC<Props> = (
|
||||
<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>
|
||||
<h2 className="text-xl font-bold">{label ('tabs.keyboard')}</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -159,20 +159,20 @@ const KeyboardSettingsSection: FC<Props> = (
|
||||
bindings: { },
|
||||
}))
|
||||
}}>
|
||||
すべて既定に戻す
|
||||
{label ('actions.resetAll')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
disabled={!(hasUnsavedChanges)}
|
||||
onClick={handleDiscard}>
|
||||
変更を破棄
|
||||
{label ('actions.discard')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!(hasUnsavedChanges) || draftConflictActionIds.size > 0}
|
||||
onClick={handleSave}>
|
||||
保存
|
||||
{label ('actions.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -187,33 +187,35 @@ const KeyboardSettingsSection: FC<Props> = (
|
||||
enabled: event.target.checked,
|
||||
}))
|
||||
}}/>
|
||||
<span>キーボード・ショートカットを有効にする</span>
|
||||
<span>{label ('shortcuts.enable')}</span>
|
||||
</label>
|
||||
|
||||
{capturingActionId != null && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
キー入力待ちです.Escape で取消し,Backspace / Delete で未割当にできます.
|
||||
{label ('shortcuts.captureHelp')}
|
||||
</p>)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[52rem] 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">{label ('shortcuts.operation')}</th>
|
||||
<th className="p-2 text-left">{label ('shortcuts.scope')}</th>
|
||||
<th className="p-2 text-left">{label ('shortcuts.currentKey')}</th>
|
||||
<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">{label ('shortcuts.conflictState')}</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">{label (`shortcuts.actions.${row.id}`)}</td>
|
||||
<td className="p-2">{label (`shortcuts.scopes.${row.scope}`)}</td>
|
||||
<td className="p-2 font-mono">
|
||||
{formatKeyBinding (row.currentBinding)}
|
||||
{row.currentBinding == null
|
||||
? label ('shortcuts.unassigned')
|
||||
: <span lang="en">{formatKeyBinding (row.currentBinding)}</span>}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<Button
|
||||
@@ -225,7 +227,8 @@ const KeyboardSettingsSection: FC<Props> = (
|
||||
capturingActionId === row.id ? null : row.id,
|
||||
)
|
||||
}}>
|
||||
{capturingActionId === row.id ? '入力待ち…' : '変更'}
|
||||
{label (capturingActionId === row.id
|
||||
? 'shortcuts.capturing' : 'actions.change')}
|
||||
</Button>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
@@ -244,13 +247,15 @@ const KeyboardSettingsSection: FC<Props> = (
|
||||
}
|
||||
})
|
||||
}}>
|
||||
既定に戻す
|
||||
{label ('actions.reset')}
|
||||
</Button>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{row.hasConflict
|
||||
? <span className="text-red-600 dark:text-red-400">競合あり</span>
|
||||
: 'なし'}
|
||||
? <span className="text-red-600 dark:text-red-400">
|
||||
{label ('shortcuts.conflicting')}
|
||||
</span>
|
||||
: label ('shortcuts.none')}
|
||||
</td>
|
||||
</tr>))}
|
||||
</tbody>
|
||||
|
||||
@@ -5,19 +5,13 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
formatKeyBinding,
|
||||
SHORTCUT_DEFINITIONS,
|
||||
SHORTCUT_SCOPE_LABELS,
|
||||
} from '@/lib/keyboardShortcuts'
|
||||
import { formatKeyBinding, SHORTCUT_DEFINITIONS } from '@/lib/keyboardShortcuts'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
|
||||
import type {
|
||||
KeyBinding,
|
||||
ShortcutActionId,
|
||||
ShortcutScope,
|
||||
} from '@/lib/keyboardShortcuts'
|
||||
import type { FC } from 'react'
|
||||
|
||||
import type { KeyBinding, ShortcutActionId, ShortcutScope } from '@/lib/keyboardShortcuts'
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
@@ -28,16 +22,10 @@ type Props = {
|
||||
}
|
||||
|
||||
|
||||
const ShortcutHelpDialog: FC<Props> = (
|
||||
{
|
||||
open,
|
||||
onOpenChange,
|
||||
activeScope,
|
||||
effectiveBindings,
|
||||
conflictActionIds,
|
||||
availableActionIds,
|
||||
},
|
||||
) => {
|
||||
const ShortcutHelpDialog: FC<Props> = props => {
|
||||
const { open, onOpenChange, activeScope, effectiveBindings,
|
||||
conflictActionIds, availableActionIds } = props
|
||||
const { label } = useUITranslation ('settings')
|
||||
const rows = SHORTCUT_DEFINITIONS
|
||||
.filter (definition =>
|
||||
definition.scope === 'global' || definition.scope === activeScope)
|
||||
@@ -45,11 +33,12 @@ const ShortcutHelpDialog: FC<Props> = (
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl px-6 pb-6 pt-7">
|
||||
<DialogContent className="max-w-3xl px-6 pb-6 pt-7"
|
||||
closeLabel={label ('actions.close')}>
|
||||
<DialogHeader className="pl-8">
|
||||
<DialogTitle>キーボード・ショートカット</DialogTitle>
|
||||
<DialogTitle>{label ('shortcuts.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
この画面で利用できるショートカットを表示します。
|
||||
{label ('shortcuts.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -57,24 +46,28 @@ const ShortcutHelpDialog: FC<Props> = (
|
||||
<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>
|
||||
<th className="p-2 text-left">{label ('shortcuts.operation')}</th>
|
||||
<th className="p-2 text-left">{label ('shortcuts.scope')}</th>
|
||||
<th className="p-2 text-left">{label ('shortcuts.key')}</th>
|
||||
<th className="p-2 text-left">{label ('shortcuts.conflict')}</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">{label (`shortcuts.actions.${row.id}`)}</td>
|
||||
<td className="p-2">{label (`shortcuts.scopes.${row.scope}`)}</td>
|
||||
<td className="p-2 font-mono">
|
||||
{formatKeyBinding (effectiveBindings[row.id])}
|
||||
{effectiveBindings[row.id] == null
|
||||
? label ('shortcuts.unassigned')
|
||||
: <span lang="en">{formatKeyBinding (effectiveBindings[row.id])}</span>}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{conflictActionIds.has (row.id)
|
||||
? <span className="text-red-600 dark:text-red-400">競合あり</span>
|
||||
: 'なし'}
|
||||
? <span className="text-red-600 dark:text-red-400">
|
||||
{label ('shortcuts.conflicting')}
|
||||
</span>
|
||||
: label ('shortcuts.none')}
|
||||
</td>
|
||||
</tr>))}
|
||||
</tbody>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { FC } from 'react'
|
||||
|
||||
import { useDialogue } from '@/components/dialogues/DialogueProvider'
|
||||
import { useDialogue } from '@/lib/dialogues/useDialogue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog,
|
||||
DialogContent,
|
||||
@@ -10,6 +8,9 @@ import { Dialog,
|
||||
DialogTitle } from '@/components/ui/dialog'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { apiPost } from '@/lib/api'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
import type { User } from '@/types'
|
||||
|
||||
@@ -21,18 +22,21 @@ type Props = { visible: boolean
|
||||
|
||||
const UserCodeDialogue: FC<Props> = ({ visible, onVisibleChange, user, setUser }) => {
|
||||
const dialogue = useDialogue ()
|
||||
const { label } = useUITranslation ('settings')
|
||||
|
||||
const handleChange = async () => {
|
||||
if (!(user))
|
||||
return
|
||||
|
||||
if (!(await dialogue.confirm ({
|
||||
title: '引継ぎコードを再発行しますか?',
|
||||
title: label ('userCode.confirm'),
|
||||
description: (
|
||||
<div>
|
||||
<p>再発行するとほかのブラウザからはログアウトされます.</p>
|
||||
<p>{label ('userCode.logoutWarning')}</p>
|
||||
</div>),
|
||||
confirmText: '再発行',
|
||||
confirmText: label ('userCode.confirmRenew'),
|
||||
cancelText: label ('actions.cancel'),
|
||||
closeLabel: label ('actions.close'),
|
||||
variant: 'danger' })))
|
||||
return
|
||||
|
||||
@@ -42,22 +46,22 @@ const UserCodeDialogue: FC<Props> = ({ visible, onVisibleChange, user, setUser }
|
||||
{
|
||||
localStorage.setItem ('user_code', data.code)
|
||||
setUser (user => ({ ...user, inheritanceCode: data.code } as User))
|
||||
toast ({ title: '再発行しました.' })
|
||||
toast ({ title: label ('userCode.renewed') })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={onVisibleChange}>
|
||||
<DialogContent className="px-6 pb-6 pt-7">
|
||||
<DialogContent className="px-6 pb-6 pt-7" closeLabel={label ('actions.close')}>
|
||||
<DialogHeader className="pl-8">
|
||||
<DialogTitle>引継ぎコード</DialogTitle>
|
||||
<DialogTitle>{label ('userCode.title')}</DialogTitle>
|
||||
|
||||
<DialogDescription asChild>
|
||||
<div>
|
||||
<p>あなたの引継ぎコードはこちらです:</p>
|
||||
<p>{label ('userCode.description')}</p>
|
||||
<div className="m-2">{user?.inheritanceCode}</div>
|
||||
<p className="mt-1 text-sm text-destructive">
|
||||
このコードはほかの人には教えないでください!
|
||||
{label ('userCode.secretWarning')}
|
||||
</p>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
@@ -65,7 +69,7 @@ const UserCodeDialogue: FC<Props> = ({ visible, onVisibleChange, user, setUser }
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={handleChange} variant="destructive">
|
||||
引継ぎコード再発行
|
||||
{label ('userCode.renew')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする