このコミットが含まれているのは:
2026-10-18 01:39:03 +09:00
コミット eda81cfa89
30個のファイルの変更、965行の追加、473行の削除
+49 -23
ファイルの表示
@@ -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) === '//'
+2 -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) => {
+3 -1
ファイルの表示
@@ -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>
+3 -2
ファイルの表示
@@ -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>))
+29 -34
ファイルの表示
@@ -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>
+19 -13
ファイルの表示
@@ -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>
+31 -26
ファイルの表示
@@ -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>
+25 -32
ファイルの表示
@@ -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>
+16 -12
ファイルの表示
@@ -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>