このコミットが含まれているのは:
2026-10-18 01:39:03 +09:00
コミット eda81cfa89
30個のファイルの変更、965行の追加、473行の削除
+4 -3
ファイルの表示
@@ -4,10 +4,11 @@ import type { ReactNode } from 'react'
type DialogueVariant = 'default' | 'danger'
type ConfirmOptions = { title: string
type ConfirmOptions = { title: ReactNode
description?: ReactNode
confirmText?: string
cancelText?: string
confirmText?: ReactNode
cancelText?: ReactNode
closeLabel?: ReactNode
variant?: DialogueVariant }
type AlertOptions = { title: string
+45 -20
ファイルの表示
@@ -1,4 +1,5 @@
import { unlocalisedPath } from '@/lib/locales'
import { i18n } from '@/lib/i18n'
export type ShortcutScope =
| 'global'
@@ -39,85 +40,109 @@ export type ShortcutDefinition = {
}
export const SHORTCUT_SCOPE_LABELS: Record<ShortcutScope, string> = {
global: '全画面',
posts: '広場',
postSearch: '広場検索',
tags: 'タグ',
settings: '設定',
wiki: 'Wiki',
materials: '素材',
get global () { return i18n.t ('settings:shortcuts.scopes.global') },
get posts () { return i18n.t ('settings:shortcuts.scopes.posts') },
get postSearch () { return i18n.t ('settings:shortcuts.scopes.postSearch') },
get tags () { return i18n.t ('settings:shortcuts.scopes.tags') },
get settings () { return i18n.t ('settings:shortcuts.scopes.settings') },
get wiki () { return i18n.t ('settings:shortcuts.scopes.wiki') },
get materials () { return i18n.t ('settings:shortcuts.scopes.materials') },
}
export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
{
id: 'global.openShortcutHelp',
label: 'ショートカット一覧を開く',
get label () {
return i18n.t ('settings:shortcuts.actions.global.openShortcutHelp')
},
scope: 'global',
defaultBinding: { key: '?', shift: true },
},
{
id: 'global.focusSearch',
label: '検索欄へ移動',
get label () {
return i18n.t ('settings:shortcuts.actions.global.focusSearch')
},
scope: 'global',
defaultBinding: { key: '/' },
},
{
id: 'navigation.posts',
label: '広場へ移動',
get label () {
return i18n.t ('settings:shortcuts.actions.navigation.posts')
},
scope: 'global',
defaultBinding: { key: 'p' },
},
{
id: 'navigation.tags',
label: 'タグへ移動',
get label () {
return i18n.t ('settings:shortcuts.actions.navigation.tags')
},
scope: 'global',
defaultBinding: { key: 't' },
},
{
id: 'navigation.materials',
label: '素材へ移動',
get label () {
return i18n.t ('settings:shortcuts.actions.navigation.materials')
},
scope: 'global',
defaultBinding: { key: 'm' },
},
{
id: 'navigation.wiki',
label: 'Wiki へ移動',
get label () {
return i18n.t ('settings:shortcuts.actions.navigation.wiki')
},
scope: 'global',
defaultBinding: { key: 'w' },
},
{
id: 'pagination.next',
label: '次のページへ移動',
get label () {
return i18n.t ('settings:shortcuts.actions.pagination.next')
},
scope: 'global',
defaultBinding: { key: 'ArrowRight' },
},
{
id: 'pagination.previous',
label: '前のページへ移動',
get label () {
return i18n.t ('settings:shortcuts.actions.pagination.previous')
},
scope: 'global',
defaultBinding: { key: 'ArrowLeft' },
},
{
id: 'settings.account',
label: '設定: アカウント',
get label () {
return i18n.t ('settings:shortcuts.actions.settings.account')
},
scope: 'settings',
defaultBinding: { key: '1' },
},
{
id: 'settings.behavior',
label: '設定: 動作',
get label () {
return i18n.t ('settings:shortcuts.actions.settings.behavior')
},
scope: 'settings',
defaultBinding: { key: '2' },
},
{
id: 'settings.theme',
label: '設定: テーマ',
get label () {
return i18n.t ('settings:shortcuts.actions.settings.theme')
},
scope: 'settings',
defaultBinding: { key: '3' },
},
{
id: 'settings.keyboard',
label: '設定: キーボード',
get label () {
return i18n.t ('settings:shortcuts.actions.settings.keyboard')
},
scope: 'settings',
defaultBinding: { key: '4' },
},
@@ -251,7 +276,7 @@ const keyLabel = (key: string): string => {
export const formatKeyBinding = (binding: KeyBinding | null): string => {
if (binding == null)
return '未割当'
return i18n.t ('settings:shortcuts.unassigned')
return [
binding.ctrl ? 'Ctrl' : null,
+3 -1
ファイルの表示
@@ -131,7 +131,9 @@ export const wikiKeys = {
title: string,
p: { version?: string },
locale: LocaleCode = 'ja',
) => ['wiki', locale, title, p] as const }
) => ['wiki', locale, title, p] as const,
changes: (p: { id?: string }, locale: LocaleCode = 'ja') =>
['wiki', locale, 'changes', 'index', p] as const }
export const helpKeys = {
root: ['help'] as const,
+7 -3
ファイルの表示
@@ -10,6 +10,7 @@ import {
import { useBlocker } from 'react-router-dom'
import { useDialogue } from '@/lib/dialogues/useDialogue'
import { useUITranslation } from '@/lib/useUITranslation'
import type { FC, PropsWithChildren } from 'react'
@@ -34,6 +35,7 @@ const UnsavedChangesGuardContext =
export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children }) => {
const dialogue = useDialogue ()
const { label } = useUITranslation ('settings')
const sourcesRef = useRef (new Map<symbol, UnsavedChangesSource> ())
const bypassNextNavigationRef = useRef<symbol | null> (null)
const [revision, setRevision] = useState (0)
@@ -99,8 +101,10 @@ export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children })
try
{
const confirmed = await dialogue.confirm ({
title: '変更が破棄してページ移動しますか?',
confirmText: '変更を破棄して移動',
title: label ('unsaved.leavePage'),
confirmText: label ('unsaved.discardAndMove'),
cancelText: label ('actions.cancel'),
closeLabel: label ('actions.close'),
variant: 'danger' })
if (!(confirmed))
return false
@@ -111,7 +115,7 @@ export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children })
{
dialogueOpenRef.current = false
}
}, [dialogue, hasUnsavedChanges])
}, [dialogue, hasUnsavedChanges, label])
useEffect (() => {
if (blocker.state !== 'blocked' || handlingBlockedTransitionRef.current)
+36 -15
ファイルの表示
@@ -1,6 +1,8 @@
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import { i18n } from '@/lib/i18n'
import type { ClassValue } from 'clsx'
import type { LocaleCode } from '@/lib/locales'
@@ -27,67 +29,86 @@ export const dateString = (
second: unknown == null ? 'numeric' : undefined })
export const localisedDateString = (value: string | Date, locale: LocaleCode): string => {
export const localisedDateString = (
value: string | Date,
locale: LocaleCode,
unknown: Parameters<typeof dateString>[1] = null,
): string => {
if (locale === 'ja')
return dateString (value)
return dateString (value, unknown)
const date = toDate (value)
if (Number.isNaN (date.getTime ()))
return dateString (value)
return dateString (value, unknown)
const parts = new Intl.DateTimeFormat ('en-CA', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hourCycle: 'h23' }).formatToParts (date)
const part = (type: string) => parts.find (entry => entry.type === type)?.value ?? ''
return `${ part ('year') }-${ part ('month') }-${ part ('day') }\u3000${
part ('hour') }:${ part ('minute') }:${ part ('second') }`
const omitDay = unknown != null && ['month', 'day'].includes (unknown)
const dateParts = [part ('year'),
...(unknown === 'month' ? [] : [part ('month')]),
...(omitDay ? [] : [part ('day')])]
const dateText = dateParts.join ('-')
if (unknown != null && ['month', 'day', 'hour'].includes (unknown))
return dateText
const timeParts = [part ('hour'),
...(unknown === 'minute' ? [] : [part ('minute')]),
...(unknown == null ? [part ('second')] : [])]
return `${dateText}\u3000${timeParts.join (':')}`
}
export const originalCreatedAtString = (
f: string | Date | null,
b: string | Date | null): string => {
b: string | Date | null,
locale: LocaleCode = 'ja',
): string => {
const from = f ? toDate (f) : null
const before = b ? toDate (b) : null
const format = (value: Date, unknown: Parameters<typeof dateString>[1]) =>
localisedDateString (value, locale, unknown)
const unknownLabel = (key: string) => i18n.t (`history:${key}`, { lng: locale })
const space = locale === 'ja' ? ' ' : '\u3000'
if (from && before)
{
const diff = before.getTime () - from.getTime ()
if (diff <= 60_000 /* 1 分 */)
return dateString (from, 'second')
return format (from, 'second')
if (from.getMinutes () === 0 && before.getMinutes () === 0)
{
if (Math.abs (diff - 3_600_000 /* 1 時間 */) < 60_000)
return dateString (from, 'minute') + ' (分不詳)'
return `${format (from, 'minute')}${space}(${unknownLabel ('unknownMinute')})`
if (from.getHours () === 0 && before.getHours () === 0)
{
if (Math.abs (diff - 86_400_000 /* 1 日 */) < 60_000)
return dateString (from, 'hour') + ' (時刻不詳)'
return `${format (from, 'hour')}${space}(${unknownLabel ('unknownTime')})`
if (from.getDate () === 1 && before.getDate () === 1)
{
if (2_332_800_000 /* 27 日 */ < diff && diff < 2_764_800_000 /* 32 日 */)
return dateString (from, 'day') + ' (日不詳)'
return `${format (from, 'day')}${space}(${unknownLabel ('unknownDay')})`
if (from.getMonth () === 0 && before.getMonth () === 0
&& (31_449_600_000 /* 364 日 */ <= diff
&& diff < 31_708_800_000 /* 367 日 */))
return dateString (from, 'month') + ' (月日不詳)'
return `${format (from, 'month')}${space}(${unknownLabel ('unknownMonthDay')})`
}
}
}
}
const rtn = ([from ? `${ dateString (from, 'second') }` : '',
const rtn = ([from ? format (from, 'second') : '',
'〜',
before ? `${ dateString (new Date (before.getTime () - 60_000), 'second') }` : '']
before ? format (new Date (before.getTime () - 60_000), 'second') : '']
.filter (Boolean)
.join (' '))
return rtn === '〜' ? '年月日不詳' : rtn
.join (space))
return rtn === '〜' ? unknownLabel ('unknownDate') : rtn
}
+7 -1
ファイルの表示
@@ -3,7 +3,13 @@ import { isAxiosError } from 'axios'
import { apiGet } from '@/lib/api'
import type { LocaleCode } from '@/lib/locales'
import type { WikiPage } from '@/types'
import type { WikiPage, WikiPageChange } from '@/types'
export const fetchWikiChanges = (
params: { id?: string },
locale: LocaleCode = 'ja',
): Promise<WikiPageChange[]> => apiGet ('/wiki/changes', { locale, params })
export const fetchWikiPages = async (