このコミットが含まれているのは:
2026-07-04 21:47:16 +09:00
コミット 518c5fa0f2
16個のファイルの変更840行の追加598行の削除
+387 -390
ファイルの表示
@@ -1,11 +1,15 @@
import type { Dispatch, FC, KeyboardEvent, SetStateAction } from 'react'
import type {
Dispatch,
FC,
KeyboardEvent,
SetStateAction,
} from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import FieldError from '@/components/common/FieldError'
import Form from '@/components/common/Form'
import FormField from '@/components/common/FormField'
import Label from '@/components/common/Label'
import PageTitle from '@/components/common/PageTitle'
@@ -18,16 +22,11 @@ import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import { apiPut } from '@/lib/api'
import {
AUTO_FETCH_OPTIONS,
DEFAULT_USER_SETTINGS,
DISPLAY_DENSITY_OPTIONS,
FONT_SIZE_OPTIONS,
POST_LIST_LIMIT_OPTIONS,
POST_LIST_ORDER_OPTIONS,
THEME_OPTIONS,
VIEWED_POST_DISPLAY_OPTIONS,
WIKI_EDITOR_MODE_OPTIONS,
updateUserSettings,
AUTO_FETCH_OPTIONS,
DEFAULT_USER_SETTINGS,
THEME_OPTIONS,
WIKI_EDITOR_MODE_OPTIONS,
updateUserSettings,
} from '@/lib/settings'
import { cn, inputClass } from '@/lib/utils'
import { useValidationErrors } from '@/lib/useValidationErrors'
@@ -41,41 +40,55 @@ type Props = { user: User | null
type UserFormField = 'name'
type SettingsFormField =
| 'theme'
| 'displayDensity'
| 'fontSize'
| 'postListLimit'
| 'postListOrder'
| 'viewedPostDisplay'
| 'tagAutocompleteNico'
| 'autoFetchTitle'
| 'autoFetchThumbnail'
| 'wikiEditorMode'
type SettingsTab = 'account' | 'display' | 'posts' | 'editing' | 'wiki'
type SettingsTab = 'account' | 'theme' | 'keyboard' | 'editing' | 'wiki'
type TabSpec = {
id: SettingsTab
label: string }
type NameFieldErrors = Partial<Record<UserFormField, string[]>>
type SettingsFieldErrors = Partial<Record<SettingsFormField, string[]>>
type SharedSectionProps = {
draftSettings: UserSettings
name: string
nameBaseErrors: string[]
nameFieldErrors: NameFieldErrors
onDraftSettingsChange: Dispatch<SetStateAction<UserSettings>>
onInheritOpen: () => void
onNameChange: (value: string) => void
onSettingsSubmit: () => void
onUserCodeOpen: () => void
onUserSubmit: () => void
settingsBaseErrors: string[]
settingsFieldErrors: SettingsFieldErrors
user: User }
const sectionClassName =
'space-y-4 rounded-xl border border-border bg-background/80 p-4'
const desktopTabRailClassName = [
'hidden md:flex md:justify-center',
].join (' ')
const desktopTabRailInnerClassName = [
'grid w-fit grid-cols-[12rem_minmax(0,36rem)] items-start gap-6',
].join (' ')
const tabs: TabSpec[] = [
{ id: 'account', label: 'アカウント' },
{ id: 'display', label: '表示' },
{ id: 'posts', label: '投稿' },
{ id: 'theme', label: 'テーマ' },
{ id: 'keyboard', label: 'キーボード' },
{ id: 'editing', label: '編輯支援' },
{ id: 'wiki', label: 'Wiki' },
]
const settingsTabByField: Record<SettingsFormField, SettingsTab> = {
theme: 'display',
displayDensity: 'display',
fontSize: 'display',
postListLimit: 'posts',
postListOrder: 'posts',
viewedPostDisplay: 'posts',
tagAutocompleteNico: 'editing',
theme: 'theme',
autoFetchTitle: 'editing',
autoFetchThumbnail: 'editing',
wikiEditorMode: 'wiki',
@@ -83,12 +96,6 @@ const settingsTabByField: Record<SettingsFormField, SettingsTab> = {
const settingsFieldOrder: SettingsFormField[] = [
'theme',
'displayDensity',
'fontSize',
'postListLimit',
'postListOrder',
'viewedPostDisplay',
'tagAutocompleteNico',
'autoFetchTitle',
'autoFetchThumbnail',
'wikiEditorMode',
@@ -96,43 +103,44 @@ const settingsFieldOrder: SettingsFormField[] = [
const themeLabel: Record<UserSettings['theme'], string> = {
system: 'システム設定に従う',
light: '',
dark: '暗' }
const densityLabel: Record<UserSettings['displayDensity'], string> = {
comfortable: '標準',
compact: '詰める' }
const fontSizeLabel: Record<UserSettings['fontSize'], string> = {
small: '小',
normal: '標準',
large: '大' }
const postListOrderLabel: Record<UserSettings['postListOrder'], string> = {
title_asc: 'タイトル昇順',
title_desc: 'タイトル降順',
url_asc: 'URL 昇順',
url_desc: 'URL 降順',
original_created_at_asc: 'オリジナル投稿日時 昇順',
original_created_at_desc: 'オリジナル投稿日時 降順',
created_at_asc: '投稿日 昇順',
created_at_desc: '投稿日 降順',
updated_at_asc: '更新日 昇順',
updated_at_desc: '更新日 降順' }
const viewedPostDisplayLabel: Record<UserSettings['viewedPostDisplay'], string> = {
show: '通常表示',
dim: '薄く表示' }
light: 'ライト・モード',
dark: 'ダーク・モード',
}
const autoFetchLabel: Record<UserSettings['autoFetchTitle'], string> = {
auto: '自動',
manual: '手動',
off: '無効' }
off: '無効',
}
const wikiEditorModeLabel: Record<UserSettings['wikiEditorMode'], string> = {
split: '左右表示',
write: '本文のみ',
preview: 'プレビューのみ' }
preview: 'プレビューのみ',
}
const keyboardShortcutRows = [
{ action: '投稿詳細を開く', key: 'Enter', scope: '一覧・検索結果', conflict: 'なし' },
{ action: '候補タグを選ぶ', key: '↑ / ↓ / Enter', scope: 'タグ入力', conflict: 'なし' },
{ action: '候補タグを閉ぢる', key: 'Escape', scope: 'タグ入力', conflict: 'なし' },
]
const themeColorTargets = [
'背景色',
'文字色',
'枠線色',
'リンク色',
'強調色',
'通常タグ',
'キャラタグ',
'素材タグ',
'Nico タグ',
'廃止タグ',
'Wiki ありタグ',
'Wiki 未作成タグ',
'選択中タグ',
'ホバー中タグ',
]
const parseTab = (search: string): SettingsTab => {
@@ -147,6 +155,243 @@ const tabButtonId = (tab: SettingsTab): string => `settings-tab-${ tab }`
const tabPanelId = (tab: SettingsTab): string => `settings-panel-${ tab }`
const AccountSection: FC<SharedSectionProps> = ({
name,
nameBaseErrors,
nameFieldErrors,
onInheritOpen,
onNameChange,
onUserCodeOpen,
onUserSubmit,
user,
}) => (
<section className={sectionClassName}>
<h2 className="text-xl font-bold"></h2>
<FieldError messages={nameBaseErrors}/>
<FormField label="表示名" messages={nameFieldErrors.name}>
{({ describedBy, invalid }) => (
<>
<input
type="text"
aria-describedby={describedBy}
aria-invalid={invalid}
className={inputClass (invalid)}
value={name}
placeholder="名もなきニジラー"
onChange={ev => onNameChange (ev.target.value)}/>
{!(user.name) && (
<p className="mt-1 text-sm text-red-500">
30 !!!!
</p>)}
</>)}
</FormField>
<div className="flex flex-wrap gap-2">
<Button type="button" onClick={onUserSubmit}>
</Button>
</div>
<div className="space-y-2">
<Label></Label>
<div className="flex flex-wrap gap-2">
<Button
type="button"
onClick={onUserCodeOpen}
className="bg-gray-600 text-white"
disabled={!(user)}>
</Button>
<Button
type="button"
onClick={onInheritOpen}
className="bg-red-600 text-white"
disabled={!(user)}>
</Button>
</div>
</div>
</section>)
const ThemeSection: FC<SharedSectionProps> = ({
draftSettings,
onDraftSettingsChange,
onSettingsSubmit,
settingsBaseErrors,
settingsFieldErrors,
}) => (
<section className={sectionClassName}>
<h2 className="text-xl font-bold"></h2>
<FieldError messages={settingsBaseErrors}/>
<FormField label="基本テーマ" messages={settingsFieldErrors.theme}>
{() => (
<select
className={inputClass (Boolean (settingsFieldErrors.theme?.length))}
value={draftSettings.theme}
onChange={ev => onDraftSettingsChange (current => ({
...current,
theme: ev.target.value as UserSettings['theme'],
}))}>
{THEME_OPTIONS.map (value => (
<option key={value} value={value}>
{themeLabel[value]}
</option>))}
</select>)}
</FormField>
<div className="space-y-2 rounded-lg border border-border/70 bg-muted/30 p-3 text-sm">
<p className="font-medium"></p>
<p>
`themes` `user_themes`
</p>
<p></p>
<div className="flex flex-wrap gap-2">
{themeColorTargets.map (target => (
<span
key={target}
className="rounded-full border border-border/70 px-2 py-1">
{target}
</span>))}
</div>
</div>
<Button type="button" onClick={onSettingsSubmit}>
</Button>
</section>)
const KeyboardSection: FC = () => (
<section className={sectionClassName}>
<h2 className="text-xl font-bold"></h2>
<div className="space-y-2 rounded-lg border border-border/70 bg-muted/30 p-3 text-sm">
<p>
`action / key / scope / conflict`
key bind
</p>
</div>
<div className="overflow-x-auto">
<table className="w-full min-w-[28rem] 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>
</tr>
</thead>
<tbody>
{keyboardShortcutRows.map (row => (
<tr key={row.action} className="border-b border-border/60 last:border-b-0">
<td className="p-2">{row.action}</td>
<td className="p-2 font-mono">{row.key}</td>
<td className="p-2">{row.scope}</td>
<td className="p-2">{row.conflict}</td>
</tr>))}
</tbody>
</table>
</div>
</section>)
const EditingSection: FC<SharedSectionProps> = ({
draftSettings,
onDraftSettingsChange,
onSettingsSubmit,
settingsBaseErrors,
settingsFieldErrors,
}) => (
<section className={sectionClassName}>
<h2 className="text-xl font-bold"></h2>
<FieldError messages={settingsBaseErrors}/>
<FormField label="タイトル自動取得" messages={settingsFieldErrors.autoFetchTitle}>
{() => (
<select
className={inputClass (
Boolean (settingsFieldErrors.autoFetchTitle?.length),
)}
value={draftSettings.autoFetchTitle}
onChange={ev => onDraftSettingsChange (current => ({
...current,
autoFetchTitle: ev.target.value as UserSettings['autoFetchTitle'],
}))}>
{AUTO_FETCH_OPTIONS.map (value => (
<option key={value} value={value}>
{autoFetchLabel[value]}
</option>))}
</select>)}
</FormField>
<FormField
label="サムネール自動取得"
messages={settingsFieldErrors.autoFetchThumbnail}>
{() => (
<select
className={inputClass (
Boolean (settingsFieldErrors.autoFetchThumbnail?.length),
)}
value={draftSettings.autoFetchThumbnail}
onChange={ev => onDraftSettingsChange (current => ({
...current,
autoFetchThumbnail:
ev.target.value as UserSettings['autoFetchThumbnail'],
}))}>
{AUTO_FETCH_OPTIONS.map (value => (
<option key={value} value={value}>
{autoFetchLabel[value]}
</option>))}
</select>)}
</FormField>
<Button type="button" onClick={onSettingsSubmit}>
</Button>
</section>)
const WikiSection: FC<SharedSectionProps> = ({
draftSettings,
onDraftSettingsChange,
onSettingsSubmit,
settingsBaseErrors,
settingsFieldErrors,
}) => (
<section className={sectionClassName}>
<h2 className="text-xl font-bold">Wiki</h2>
<FieldError messages={settingsBaseErrors}/>
<FormField label="エディタ表示" messages={settingsFieldErrors.wikiEditorMode}>
{() => (
<select
className={inputClass (
Boolean (settingsFieldErrors.wikiEditorMode?.length),
)}
value={draftSettings.wikiEditorMode}
onChange={ev => onDraftSettingsChange (current => ({
...current,
wikiEditorMode: ev.target.value as UserSettings['wikiEditorMode'],
}))}>
{WIKI_EDITOR_MODE_OPTIONS.map (value => (
<option key={value} value={value}>
{wikiEditorModeLabel[value]}
</option>))}
</select>)}
</FormField>
<Button type="button" onClick={onSettingsSubmit}>
</Button>
</section>)
const SettingPage: FC<Props> = ({ user, setUser }) => {
const location = useLocation ()
const navigate = useNavigate ()
@@ -182,19 +427,10 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
const settingsTabErrors = useMemo<Record<SettingsTab, boolean>> (() => ({
account: Boolean (nameFieldErrors.name?.length),
display: (
Boolean (settingsFieldErrors.theme?.length)
|| Boolean (settingsFieldErrors.displayDensity?.length)
|| Boolean (settingsFieldErrors.fontSize?.length)
),
posts: (
Boolean (settingsFieldErrors.postListLimit?.length)
|| Boolean (settingsFieldErrors.postListOrder?.length)
|| Boolean (settingsFieldErrors.viewedPostDisplay?.length)
),
theme: Boolean (settingsFieldErrors.theme?.length),
keyboard: false,
editing: (
Boolean (settingsFieldErrors.tagAutocompleteNico?.length)
|| Boolean (settingsFieldErrors.autoFetchTitle?.length)
Boolean (settingsFieldErrors.autoFetchTitle?.length)
|| Boolean (settingsFieldErrors.autoFetchThumbnail?.length)
),
wiki: Boolean (settingsFieldErrors.wikiEditorMode?.length),
@@ -260,12 +496,6 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
{
const data = await updateUserSettings ({
theme: draftSettings.theme,
display_density: draftSettings.displayDensity,
font_size: draftSettings.fontSize,
post_list_limit: draftSettings.postListLimit,
post_list_order: draftSettings.postListOrder,
viewed_post_display: draftSettings.viewedPostDisplay,
tag_autocomplete_nico: draftSettings.tagAutocompleteNico,
auto_fetch_title: draftSettings.autoFetchTitle,
auto_fetch_thumbnail: draftSettings.autoFetchThumbnail,
wiki_editor_mode: draftSettings.wikiEditorMode,
@@ -305,6 +535,22 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
setActiveTab (settingsTabByField[fieldWithError])
}, [setActiveTab, settingsFieldErrors])
const sectionProps: SharedSectionProps | null = user ? {
draftSettings,
name,
nameBaseErrors,
nameFieldErrors,
onDraftSettingsChange: setDraftSettings,
onInheritOpen: () => setInheritVsbl (true),
onNameChange: setName,
onSettingsSubmit: handleSettingsSubmit,
onUserCodeOpen: () => setUserCodeVsbl (true),
onUserSubmit: handleUserSubmit,
settingsBaseErrors,
settingsFieldErrors,
user,
} : null
return (
<MainArea>
<Helmet>
@@ -312,322 +558,73 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
<title> | {SITE_TITLE}</title>
</Helmet>
<Form>
<PageTitle></PageTitle>
<FieldError messages={error ? [error] : []}/>
<div className="space-y-4 p-4">
<div className="mx-auto max-w-xl space-y-4">
<PageTitle></PageTitle>
<FieldError messages={error ? [error] : []}/>
</div>
{user && loaded ? (
<>
<div
role="tablist"
aria-label="設定区分"
className="mb-4 flex flex-wrap gap-2">
{tabs.map (tab => (
<button
key={tab.id}
id={tabButtonId (tab.id)}
type="button"
role="tab"
aria-selected={activeTab === tab.id}
aria-controls={tabPanelId (tab.id)}
tabIndex={activeTab === tab.id ? 0 : -1}
className={cn (
'rounded-full border px-4 py-2 text-sm font-medium',
activeTab === tab.id
? ['border-slate-900 bg-slate-900 text-white',
'dark:border-slate-100 dark:bg-slate-100 dark:text-slate-900']
: ['border-slate-300 bg-white text-slate-700',
'dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100'],
)}
onClick={() => setActiveTab (tab.id)}
onKeyDown={handleTabKeyDown}>
{tab.label}
{settingsTabErrors[tab.id] ? ' ⚠' : ''}
</button>))}
<div className="mx-auto max-w-xl space-y-6 md:hidden">
{sectionProps && (
<>
<AccountSection {...sectionProps}/>
<ThemeSection {...sectionProps}/>
<KeyboardSection/>
<EditingSection {...sectionProps}/>
<WikiSection {...sectionProps}/>
</>)}
</div>
<section
id={tabPanelId (activeTab)}
role="tabpanel"
aria-labelledby={tabButtonId (activeTab)}
className={sectionClassName}>
{activeTab === 'account' && (
<>
<h2 className="text-xl font-bold"></h2>
<FieldError messages={nameBaseErrors}/>
<div className={desktopTabRailClassName}>
<div className={desktopTabRailInnerClassName}>
<div
role="tablist"
aria-label="設定区分"
aria-orientation="vertical"
className="sticky top-4 flex h-fit flex-col gap-2">
{tabs.map (tab => (
<button
key={tab.id}
id={tabButtonId (tab.id)}
type="button"
role="tab"
aria-selected={activeTab === tab.id}
aria-controls={tabPanelId (tab.id)}
tabIndex={activeTab === tab.id ? 0 : -1}
className={cn (
'rounded-xl border px-4 py-3 text-left text-sm font-medium',
activeTab === tab.id
? ['border-slate-900 bg-slate-900 text-white',
'dark:border-slate-100 dark:bg-slate-100 dark:text-slate-900']
: ['border-slate-300 bg-white text-slate-700',
'dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100'],
)}
onClick={() => setActiveTab (tab.id)}
onKeyDown={handleTabKeyDown}>
{tab.label}
{settingsTabErrors[tab.id] ? ' ⚠' : ''}
</button>))}
</div>
<FormField label="表示名" messages={nameFieldErrors.name}>
{({ describedBy, invalid }) => (
<>
<input
type="text"
aria-describedby={describedBy}
aria-invalid={invalid}
className={inputClass (invalid)}
value={name}
placeholder="名もなきニジラー"
onChange={ev => setName (ev.target.value)}/>
{!(user.name) && (
<p className="mt-1 text-sm text-red-500">
30 !!!!
</p>)}
</>)}
</FormField>
<div className="flex flex-wrap gap-2">
<Button type="button" onClick={handleUserSubmit}>
</Button>
</div>
<div className="space-y-2">
<Label></Label>
<div className="flex flex-wrap gap-2">
<Button
type="button"
onClick={() => setUserCodeVsbl (true)}
className="bg-gray-600 text-white"
disabled={!(user)}>
</Button>
<Button
type="button"
onClick={() => setInheritVsbl (true)}
className="bg-red-600 text-white"
disabled={!(user)}>
</Button>
</div>
</div>
</>)}
{activeTab === 'display' && (
<>
<h2 className="text-xl font-bold"></h2>
<FieldError messages={settingsBaseErrors}/>
<FormField label="テーマ" messages={settingsFieldErrors.theme}>
{() => (
<select
className={inputClass (Boolean (settingsFieldErrors.theme?.length))}
value={draftSettings.theme}
onChange={ev => setDraftSettings (current => ({
...current,
theme: ev.target.value as UserSettings['theme'],
}))}>
{THEME_OPTIONS.map (value => (
<option key={value} value={value}>
{themeLabel[value]}
</option>))}
</select>)}
</FormField>
<FormField label="表示密度" messages={settingsFieldErrors.displayDensity}>
{() => (
<select
className={inputClass (
Boolean (settingsFieldErrors.displayDensity?.length),
)}
value={draftSettings.displayDensity}
onChange={ev => setDraftSettings (current => ({
...current,
displayDensity:
ev.target.value as UserSettings['displayDensity'],
}))}>
{DISPLAY_DENSITY_OPTIONS.map (value => (
<option key={value} value={value}>
{densityLabel[value]}
</option>))}
</select>)}
</FormField>
<FormField label="文字サイズ" messages={settingsFieldErrors.fontSize}>
{() => (
<select
className={inputClass (
Boolean (settingsFieldErrors.fontSize?.length),
)}
value={draftSettings.fontSize}
onChange={ev => setDraftSettings (current => ({
...current,
fontSize: ev.target.value as UserSettings['fontSize'],
}))}>
{FONT_SIZE_OPTIONS.map (value => (
<option key={value} value={value}>
{fontSizeLabel[value]}
</option>))}
</select>)}
</FormField>
<Button type="button" onClick={handleSettingsSubmit}>
</Button>
</>)}
{activeTab === 'posts' && (
<>
<h2 className="text-xl font-bold">稿</h2>
<FieldError messages={settingsBaseErrors}/>
<FormField label="既定の件数" messages={settingsFieldErrors.postListLimit}>
{() => (
<select
className={inputClass (
Boolean (settingsFieldErrors.postListLimit?.length),
)}
value={draftSettings.postListLimit}
onChange={ev => setDraftSettings (current => ({
...current,
postListLimit:
Number (ev.target.value) as UserSettings['postListLimit'],
}))}>
{POST_LIST_LIMIT_OPTIONS.map (value => (
<option key={value} value={value}>
{value}
</option>))}
</select>)}
</FormField>
<FormField label="既定の並び順" messages={settingsFieldErrors.postListOrder}>
{() => (
<select
className={inputClass (
Boolean (settingsFieldErrors.postListOrder?.length),
)}
value={draftSettings.postListOrder}
onChange={ev => setDraftSettings (current => ({
...current,
postListOrder:
ev.target.value as UserSettings['postListOrder'],
}))}>
{POST_LIST_ORDER_OPTIONS.map (value => (
<option key={value} value={value}>
{postListOrderLabel[value]}
</option>))}
</select>)}
</FormField>
<FormField label="既読投稿の表示" messages={settingsFieldErrors.viewedPostDisplay}>
{() => (
<select
className={inputClass (
Boolean (settingsFieldErrors.viewedPostDisplay?.length),
)}
value={draftSettings.viewedPostDisplay}
onChange={ev => setDraftSettings (current => ({
...current,
viewedPostDisplay:
ev.target.value as UserSettings['viewedPostDisplay'],
}))}>
{VIEWED_POST_DISPLAY_OPTIONS.map (value => (
<option key={value} value={value}>
{viewedPostDisplayLabel[value]}
</option>))}
</select>)}
</FormField>
<Button type="button" onClick={handleSettingsSubmit}>
</Button>
</>)}
{activeTab === 'editing' && (
<>
<h2 className="text-xl font-bold"></h2>
<FieldError messages={settingsBaseErrors}/>
<FormField
label="ニコニコタグを補完に含める"
messages={settingsFieldErrors.tagAutocompleteNico}>
{() => (
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={draftSettings.tagAutocompleteNico}
onChange={ev => setDraftSettings (current => ({
...current,
tagAutocompleteNico: ev.target.checked,
}))}/>
<span></span>
</label>)}
</FormField>
<FormField label="タイトル自動取得" messages={settingsFieldErrors.autoFetchTitle}>
{() => (
<select
className={inputClass (
Boolean (settingsFieldErrors.autoFetchTitle?.length),
)}
value={draftSettings.autoFetchTitle}
onChange={ev => setDraftSettings (current => ({
...current,
autoFetchTitle:
ev.target.value as UserSettings['autoFetchTitle'],
}))}>
{AUTO_FETCH_OPTIONS.map (value => (
<option key={value} value={value}>
{autoFetchLabel[value]}
</option>))}
</select>)}
</FormField>
<FormField
label="サムネール自動取得"
messages={settingsFieldErrors.autoFetchThumbnail}>
{() => (
<select
className={inputClass (
Boolean (settingsFieldErrors.autoFetchThumbnail?.length),
)}
value={draftSettings.autoFetchThumbnail}
onChange={ev => setDraftSettings (current => ({
...current,
autoFetchThumbnail:
ev.target.value as UserSettings['autoFetchThumbnail'],
}))}>
{AUTO_FETCH_OPTIONS.map (value => (
<option key={value} value={value}>
{autoFetchLabel[value]}
</option>))}
</select>)}
</FormField>
<Button type="button" onClick={handleSettingsSubmit}>
</Button>
</>)}
{activeTab === 'wiki' && (
<>
<h2 className="text-xl font-bold">Wiki</h2>
<FieldError messages={settingsBaseErrors}/>
<FormField label="エディタ表示" messages={settingsFieldErrors.wikiEditorMode}>
{() => (
<select
className={inputClass (
Boolean (settingsFieldErrors.wikiEditorMode?.length),
)}
value={draftSettings.wikiEditorMode}
onChange={ev => setDraftSettings (current => ({
...current,
wikiEditorMode:
ev.target.value as UserSettings['wikiEditorMode'],
}))}>
{WIKI_EDITOR_MODE_OPTIONS.map (value => (
<option key={value} value={value}>
{wikiEditorModeLabel[value]}
</option>))}
</select>)}
</FormField>
<Button type="button" onClick={handleSettingsSubmit}>
</Button>
</>)}
</section>
<div className="w-[36rem] max-w-full">
{sectionProps && (
<div
id={tabPanelId (activeTab)}
role="tabpanel"
aria-labelledby={tabButtonId (activeTab)}>
{activeTab === 'account' && <AccountSection {...sectionProps}/>}
{activeTab === 'theme' && <ThemeSection {...sectionProps}/>}
{activeTab === 'keyboard' && <KeyboardSection/>}
{activeTab === 'editing' && <EditingSection {...sectionProps}/>}
{activeTab === 'wiki' && <WikiSection {...sectionProps}/>}
</div>)}
</div>
</div>
</div>
</>) : 'Loading...'}
</Form>
</div>
<UserCodeDialogue
visible={userCodeVsbl}