このコミットが含まれているのは:
2026-07-05 20:18:16 +09:00
コミット a0c1b8ba31
5個のファイルの変更578行の追加339行の削除
+348 -249
ファイルの表示
@@ -20,20 +20,20 @@ import { SITE_TITLE } from '@/config'
import { apiPut } from '@/lib/api'
import {
applyClientAppearance,
applyThemeSelection,
applyThemeAppearanceState,
DEFAULT_USER_SETTINGS,
deriveUserThemeModeFromSlotSelection,
fetchUserSettings,
fetchUserThemeSlots,
getClientActiveThemeSlot,
getClientActiveDarkThemeSlotNo,
getClientActiveLightThemeSlotNo,
getClientThemeMode,
getDefaultUserThemeSlot,
hasStoredClientThemeSelection,
isUserThemeSlotSelection,
normaliseThemeSlotSelectionAgainstSlots,
getUserThemeSlotSelection,
normaliseUserThemeSlots,
resolveDisplayedTheme,
setCachedUserThemeSlots,
setClientActiveThemeSlot,
resolveThemeFromSlotSelection,
setClientThemeAppearance,
updateUserSettings,
upsertUserThemeSlot,
USER_THEME_SLOT_SELECTIONS,
@@ -47,8 +47,8 @@ import type { CSSProperties, Dispatch, FC, KeyboardEvent, SetStateAction } from
import type { Category, Tag, User } from '@/types'
import type {
BuiltinThemeId,
ClientThemeMode,
ClientTheme,
ThemeSlotSelection,
ThemeTokens,
ThemeTagColours,
UserSettings,
@@ -80,12 +80,15 @@ type AccountSectionProps = {
userBaseErrors: string[]
userFieldErrors: Partial<Record<UserFormField, string[]>> }
type ThemeModeItem = {
id: ClientThemeMode
label: string }
type ThemeSelectionItem = {
id: ThemeSlotSelection
label: string
editable: boolean
baseTheme?: BuiltinThemeId
slotNo?: UserThemeSlotNo }
id: UserThemeSlotSelection
label: string
baseTheme: BuiltinThemeId
slotNo: UserThemeSlotNo }
type ThemeTokenFieldSpec = {
key: ThemeTokenKey
@@ -96,17 +99,20 @@ type ThemeTokenGroupSpec = {
fields: ThemeTokenFieldSpec[] }
type ThemeSectionProps = {
draftActiveThemeSlot: ThemeSlotSelection
draftThemeSlots: UserThemeSlotMap
editingThemeSlot: UserThemeSlotSelection | null
draftThemeMode: ClientThemeMode
draftActiveLightThemeSlotNo: UserThemeSlotNo
draftActiveDarkThemeSlotNo: UserThemeSlotNo
editingThemeSlot: UserThemeSlotSelection
selectedTheme: ClientTheme
selectedThemeLabel: string
displayedThemeLabel: string
systemThemeLabel: string
hasUnsavedChanges: boolean
settingsBaseErrors: string[]
settingsFieldErrors: Partial<Record<SettingsFormField, string[]>>
onSelectThemeSlot: (selection: ThemeSlotSelection) => void
onEditThemeSlot: (selection: UserThemeSlotSelection) => void
onEnsureEditableTheme: () => void
onSelectThemeMode: (themeMode: ClientThemeMode) => void
onSelectLightThemeSlotNo: (slotNo: UserThemeSlotNo) => void
onSelectDarkThemeSlotNo: (slotNo: UserThemeSlotNo) => void
onSelectEditingThemeSlot: (selection: UserThemeSlotSelection) => void
onThemeTokenChange: (key: ThemeTokenKey, value: string) => void
onTagColourChange: (category: Category, colour: string) => void
onResetThemeSlot: (selection: UserThemeSlotSelection) => void
@@ -117,25 +123,34 @@ type SettingsTabDirtyState = {
dirty: boolean
discard: () => void | Promise<void> }
type ThemeDraftState = {
themeMode: ClientThemeMode
activeLightThemeSlotNo: UserThemeSlotNo
activeDarkThemeSlotNo: UserThemeSlotNo
themeSlots: UserThemeSlotMap
editingThemeSlot: UserThemeSlotSelection }
const tabs: TabSpec[] = [
{ id: 'account', label: 'アカウント' },
{ id: 'behavior', label: '動作' },
{ id: 'theme', label: 'テーマ' },
{ id: 'keyboard', label: 'キーボード' } ]
const themeModeSelections: ThemeModeItem[] = [
{ id: 'system', label: 'システム設定に従う' },
{ id: 'light', label: 'ライト' },
{ id: 'dark', label: 'ダーク' } ]
const sectionClassName =
'space-y-4 rounded-xl border border-border bg-background/80 p-4'
const themeSelections: ThemeSelectionItem[] = [
{ id: 'system', label: 'システム設定', editable: false },
{ id: 'builtin:light', label: '標準ライト', editable: false },
{ id: 'builtin:dark', label: '標準ダーク', editable: false },
{ id: 'user:light:1', label: 'ライト 1', editable: true, baseTheme: 'light', slotNo: 1 },
{ id: 'user:light:2', label: 'ライト 2', editable: true, baseTheme: 'light', slotNo: 2 },
{ id: 'user:light:3', label: 'ライト 3', editable: true, baseTheme: 'light', slotNo: 3 },
{ id: 'user:dark:1', label: 'ダーク 1', editable: true, baseTheme: 'dark', slotNo: 1 },
{ id: 'user:dark:2', label: 'ダーク 2', editable: true, baseTheme: 'dark', slotNo: 2 },
{ id: 'user:dark:3', label: 'ダーク 3', editable: true, baseTheme: 'dark', slotNo: 3 },
{ id: 'user:light:1', label: 'ライト 1', baseTheme: 'light', slotNo: 1 },
{ id: 'user:light:2', label: 'ライト 2', baseTheme: 'light', slotNo: 2 },
{ id: 'user:light:3', label: 'ライト 3', baseTheme: 'light', slotNo: 3 },
{ id: 'user:dark:1', label: 'ダーク 1', baseTheme: 'dark', slotNo: 1 },
{ id: 'user:dark:2', label: 'ダーク 2', baseTheme: 'dark', slotNo: 2 },
{ id: 'user:dark:3', label: 'ダーク 3', baseTheme: 'dark', slotNo: 3 },
]
const themeTokenGroups: ThemeTokenGroupSpec[] = [
@@ -159,12 +174,13 @@ const themeTokenGroups: ThemeTokenGroupSpec[] = [
] },
{ title: '上部ナビ',
fields: [
{ key: 'topNavRootBackgroundMobile', label: '通常背景' },
{ key: 'topNavRootBackgroundMobile', label: 'スマホ背景' },
{ key: 'topNavRootBackgroundDesktop', label: 'PC 背景' },
{ key: 'topNavHighlightBackground', label: '選択中背景' },
{ key: 'topNavActiveBackground', label: '選択中背景' },
{ key: 'topNavSubmenuBackground', label: 'サブメニュー背景' },
{ key: 'topNavMobileActiveBackground', label: 'スマホ選択中背景' },
{ key: 'topNavLink', label: 'リンク' },
{ key: 'topNavBrandLink', label: 'ロゴ色' },
{ key: 'topNavMenuLink', label: 'メニュー文字色' },
] },
]
@@ -215,8 +231,8 @@ const themePreviewStyle = (tagColours: ThemeTagColours): CSSProperties =>
[`--tag-colour-${ category }`, tagColours[category]],
[`--tag-colour-${ category }-hover`, tagColours[category]]])) as CSSProperties
const themeLabelBySelection = (selection: ThemeSlotSelection): string =>
themeSelections.find (item => item.id === selection)?.label ?? 'システム設定'
const themeLabelBySelection = (selection: UserThemeSlotSelection): string =>
themeSelections.find (item => item.id === selection)?.label ?? 'ライト 1'
const ensureCompleteThemeSlots = (
themeSlots: UserThemeSlotMap,
@@ -236,31 +252,21 @@ const ensureCompleteThemeSlots = (
)
const serialiseThemeDraft = (
activeThemeSlot: ThemeSlotSelection,
themeSlots: UserThemeSlotMap,
themeMode: ClientThemeMode,
activeLightThemeSlotNo: UserThemeSlotNo,
activeDarkThemeSlotNo: UserThemeSlotNo,
themeSlots: UserThemeSlotMap,
): string =>
JSON.stringify ({
activeThemeSlot,
themeMode,
activeLightThemeSlotNo,
activeDarkThemeSlotNo,
themeSlots: USER_THEME_SLOT_SELECTIONS.map (selection => ({
selection,
tokens: themeSlots[selection]?.tokens,
})),
})
const themeToSelection = (
theme: UserSettings['theme'],
): ThemeSlotSelection =>
theme === 'system'
? 'system'
: `builtin:${ theme }`
const themeRowBaseLabel = (
selection: ThemeSlotSelection,
): string =>
selection === 'system'
? '端末設定に従います'
: themeLabelBySelection (selection)
const isTopNavThemeTokenKey = (key: ThemeTokenKey): boolean =>
key.startsWith ('topNav')
@@ -447,32 +453,27 @@ const AccountSection: FC<AccountSectionProps> = (
</section>)
const ThemeSection: FC<ThemeSectionProps> = (
{ draftActiveThemeSlot,
draftThemeSlots,
{ draftThemeMode,
draftActiveLightThemeSlotNo,
draftActiveDarkThemeSlotNo,
editingThemeSlot,
selectedTheme,
selectedThemeLabel,
displayedThemeLabel,
systemThemeLabel,
hasUnsavedChanges,
settingsBaseErrors,
settingsFieldErrors,
onSelectThemeSlot,
onEditThemeSlot,
onEnsureEditableTheme,
onSelectThemeMode,
onSelectLightThemeSlotNo,
onSelectDarkThemeSlotNo,
onSelectEditingThemeSlot,
onThemeTokenChange,
onTagColourChange,
onResetThemeSlot,
onThemeSubmit,
onThemeDiscard },
) => {
const currentSelection =
themeSelections.find (item => item.id === draftActiveThemeSlot)
?? themeSelections[0]
const currentEditableSelection =
editingThemeSlot
?? (isUserThemeSlotSelection (draftActiveThemeSlot)
? draftActiveThemeSlot
: null)
const canEditTheme = currentEditableSelection != null
const currentEditableSelection = editingThemeSlot
return (
<section className={sectionClassName}>
@@ -480,12 +481,15 @@ const ThemeSection: FC<ThemeSectionProps> = (
<div className="space-y-1">
<h2 className="text-xl font-bold"></h2>
<p className="text-sm text-muted-foreground">
: {selectedThemeLabel}
: {displayedThemeLabel}
</p>
{editingThemeSlot && (
{draftThemeMode === 'system' && (
<p className="text-sm text-muted-foreground">
: {themeLabelBySelection (editingThemeSlot)}
: {systemThemeLabel}
</p>)}
<p className="text-sm text-muted-foreground">
: {themeLabelBySelection (currentEditableSelection)}
</p>
{hasUnsavedChanges && (
<p className="text-sm text-amber-700 dark:text-amber-300">
@@ -514,13 +518,60 @@ const ThemeSection: FC<ThemeSectionProps> = (
<div className="space-y-3 rounded-xl border border-border/70 bg-background/70 p-4">
<div className="space-y-2">
<Label></Label>
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
<Label></Label>
<select
className={inputClass (false)}
value={draftThemeMode}
onChange={ev =>
onSelectThemeMode (ev.target.value as ClientThemeMode)}>
{themeModeSelections.map (selection => (
<option key={selection.id} value={selection.id}>
{selection.label}
</option>))}
</select>
</div>
<div className="space-y-2">
<Label></Label>
<select
className={inputClass (false)}
value={draftActiveLightThemeSlotNo}
onChange={ev =>
onSelectLightThemeSlotNo (Number (ev.target.value) as UserThemeSlotNo)}>
{themeSelections
.filter (selection => selection.baseTheme === 'light')
.map (selection => (
<option key={selection.id} value={selection.slotNo}>
{selection.label}
</option>))}
</select>
</div>
<div className="space-y-2">
<Label></Label>
<select
className={inputClass (false)}
value={draftActiveDarkThemeSlotNo}
onChange={ev =>
onSelectDarkThemeSlotNo (Number (ev.target.value) as UserThemeSlotNo)}>
{themeSelections
.filter (selection => selection.baseTheme === 'dark')
.map (selection => (
<option key={selection.id} value={selection.slotNo}>
{selection.label}
</option>))}
</select>
</div>
<div className="space-y-2">
<Label htmlFor="theme-slot-select"></Label>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<select
id="theme-slot-select"
className={cn (inputClass (false), 'w-full min-w-0 sm:w-64')}
value={draftActiveThemeSlot}
value={currentEditableSelection}
onChange={ev =>
onSelectThemeSlot (ev.target.value as ThemeSlotSelection)}>
onSelectEditingThemeSlot (ev.target.value as UserThemeSlotSelection)}>
{themeSelections.map (selection => (
<option key={selection.id} value={selection.id}>
{selection.label}
@@ -529,43 +580,15 @@ const ThemeSection: FC<ThemeSectionProps> = (
<Button
type="button"
variant="outline"
onClick={() => {
if (isUserThemeSlotSelection (draftActiveThemeSlot))
{
onEditThemeSlot (draftActiveThemeSlot)
return
}
onEnsureEditableTheme ()
}}>
{canEditTheme ? 'この枠を編輯' : '編輯する枠を選ぶ'}
variant="ghost"
onClick={() => onResetThemeSlot (currentEditableSelection)}>
</Button>
{currentEditableSelection && (
<Button
type="button"
variant="ghost"
onClick={() => onResetThemeSlot (currentEditableSelection)}>
</Button>)}
</div>
</div>
<div className="space-y-1">
<p className="text-sm font-semibold">{currentSelection.label}</p>
<p className="text-sm text-muted-foreground">
{themeRowBaseLabel (currentSelection.id)}
</p>
{canEditTheme
? (
<p className="text-sm text-muted-foreground">
</p>)
: (
<p className="text-sm text-muted-foreground">
</p>)}
<div className="space-y-1 text-sm text-muted-foreground">
<p></p>
</div>
<div style={themePreviewStyle (selectedTheme.tokens.tagColours)}>
@@ -593,10 +616,9 @@ const ThemeSection: FC<ThemeSectionProps> = (
<p className="text-sm font-medium">{field.label}</p>
</div>
<div className="flex flex-wrap items-center gap-3">
<input
<input
type="color"
value={themeTokenToHex (field.key, selectedTheme.tokens[field.key])}
disabled={!(canEditTheme)}
onChange={ev =>
onThemeTokenChange (field.key, hexToThemeToken (field.key, ev.target.value))}
className="h-10 w-16 cursor-pointer rounded border border-border
@@ -635,7 +657,6 @@ const ThemeSection: FC<ThemeSectionProps> = (
<input
type="color"
value={selectedTheme.tokens.tagColours[category]}
disabled={!(canEditTheme)}
onChange={ev => onTagColourChange (category, ev.target.value)}
className="h-10 w-16 cursor-pointer rounded border border-border
bg-transparent p-1 disabled:cursor-not-allowed
@@ -659,16 +680,26 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
const [name, setName] = useState ('')
const [settingsError, setSettingsError] = useState<string | null> (null)
const [, setDraftSettings] = useState<UserSettings> (DEFAULT_USER_SETTINGS)
const [savedActiveThemeSlot, setSavedActiveThemeSlot] =
useState<ThemeSlotSelection> (getClientActiveThemeSlot ())
const [draftActiveThemeSlot, setDraftActiveThemeSlot] =
useState<ThemeSlotSelection> (getClientActiveThemeSlot ())
const [savedThemeMode, setSavedThemeMode] =
useState<ClientThemeMode> (getClientThemeMode ())
const [draftThemeMode, setDraftThemeMode] =
useState<ClientThemeMode> (getClientThemeMode ())
const [savedActiveLightThemeSlotNo, setSavedActiveLightThemeSlotNo] =
useState<UserThemeSlotNo> (getClientActiveLightThemeSlotNo ())
const [draftActiveLightThemeSlotNo, setDraftActiveLightThemeSlotNo] =
useState<UserThemeSlotNo> (getClientActiveLightThemeSlotNo ())
const [savedActiveDarkThemeSlotNo, setSavedActiveDarkThemeSlotNo] =
useState<UserThemeSlotNo> (getClientActiveDarkThemeSlotNo ())
const [draftActiveDarkThemeSlotNo, setDraftActiveDarkThemeSlotNo] =
useState<UserThemeSlotNo> (getClientActiveDarkThemeSlotNo ())
const [savedThemeSlots, setSavedThemeSlots] =
useState<UserThemeSlotMap> (() => ensureCompleteThemeSlots ({ }))
const [draftThemeSlots, setDraftThemeSlots] =
useState<UserThemeSlotMap> (() => ensureCompleteThemeSlots ({ }))
const [savedEditingThemeSlot, setSavedEditingThemeSlot] =
useState<UserThemeSlotSelection> ('user:light:1')
const [editingThemeSlot, setEditingThemeSlot] =
useState<UserThemeSlotSelection | null> (null)
useState<UserThemeSlotSelection> ('user:light:1')
const [dirtyStates, setDirtyStates] =
useState<Partial<Record<SettingsTab, SettingsTabDirtyState>>> ({ })
const [userCodeVsbl, setUserCodeVsbl] = useState (false)
@@ -684,25 +715,59 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
useValidationErrors<SettingsFormField> ()
const activeTab = useMemo<SettingsTab> (() => parseTab (location.search), [location.search])
const displayedThemeSelection = useMemo (
() => resolveDisplayedTheme ({
themeMode: draftThemeMode,
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo,
}, draftThemeSlots),
[draftActiveDarkThemeSlotNo,
draftActiveLightThemeSlotNo,
draftThemeMode,
draftThemeSlots],
)
const selectedTheme = useMemo<ClientTheme> (
() => {
const resolvedTheme =
resolveThemeFromSlotSelection (draftActiveThemeSlot, draftThemeSlots)
const currentSelection =
themeSelections.find (selection => selection.id === editingThemeSlot)
?? themeSelections[0]
const themeSlot =
draftThemeSlots[currentSelection.id]
?? getDefaultUserThemeSlot (currentSelection.baseTheme, currentSelection.slotNo)
return {
id: resolvedTheme.baseTheme,
name: themeLabelBySelection (draftActiveThemeSlot),
builtin: resolvedTheme.builtin,
baseTheme: resolvedTheme.baseTheme,
tokens: resolvedTheme.tokens,
id: currentSelection.baseTheme,
name: themeLabelBySelection (currentSelection.id),
builtin: false,
baseTheme: currentSelection.baseTheme,
tokens: themeSlot.tokens,
}
},
[draftActiveThemeSlot, draftThemeSlots],
[draftThemeSlots, editingThemeSlot],
)
const hasUnsavedThemeChanges = useMemo (
() =>
serialiseThemeDraft (draftActiveThemeSlot, draftThemeSlots)
!== serialiseThemeDraft (savedActiveThemeSlot, savedThemeSlots),
[draftActiveThemeSlot, draftThemeSlots, savedActiveThemeSlot, savedThemeSlots],
serialiseThemeDraft (
draftThemeMode,
draftActiveLightThemeSlotNo,
draftActiveDarkThemeSlotNo,
draftThemeSlots,
)
!== serialiseThemeDraft (
savedThemeMode,
savedActiveLightThemeSlotNo,
savedActiveDarkThemeSlotNo,
savedThemeSlots,
),
[draftActiveDarkThemeSlotNo,
draftActiveLightThemeSlotNo,
draftThemeMode,
draftThemeSlots,
editingThemeSlot,
savedActiveDarkThemeSlotNo,
savedActiveLightThemeSlotNo,
savedEditingThemeSlot,
savedThemeMode,
savedThemeSlots],
)
const applyActiveTab = (tab: SettingsTab) => {
@@ -753,25 +818,63 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
[conflictActionIds.size, settingsFieldErrors.theme, userFieldErrors.name],
)
const applyDraftThemeSelection = (
activeThemeSlot: ThemeSlotSelection,
themeSlots: UserThemeSlotMap,
const applyDraftThemeState = (
{ themeMode = draftThemeMode,
activeLightThemeSlotNo = draftActiveLightThemeSlotNo,
activeDarkThemeSlotNo = draftActiveDarkThemeSlotNo,
themeSlots = draftThemeSlots,
nextEditingThemeSlot = editingThemeSlot }: {
themeMode?: ClientThemeMode
activeLightThemeSlotNo?: UserThemeSlotNo
activeDarkThemeSlotNo?: UserThemeSlotNo
themeSlots?: UserThemeSlotMap
nextEditingThemeSlot?: UserThemeSlotSelection },
) => {
setDraftActiveThemeSlot (activeThemeSlot)
setDraftThemeMode (themeMode)
setDraftActiveLightThemeSlotNo (activeLightThemeSlotNo)
setDraftActiveDarkThemeSlotNo (activeDarkThemeSlotNo)
setDraftThemeSlots (themeSlots)
setEditingThemeSlot (nextEditingThemeSlot)
setDraftSettings (current => ({
...current,
theme: deriveUserThemeModeFromSlotSelection (activeThemeSlot),
theme: themeMode,
}))
applyThemeSelection (activeThemeSlot, themeSlots)
setCachedUserThemeSlots (themeSlots)
applyThemeAppearanceState ({
themeMode,
activeLightThemeSlotNo,
activeDarkThemeSlotNo,
}, themeSlots)
}
const currentThemeDraftState = (): ThemeDraftState => ({
themeMode: draftThemeMode,
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo,
themeSlots: draftThemeSlots,
editingThemeSlot,
})
const savedThemeDraftState = (): ThemeDraftState => ({
themeMode: savedThemeMode,
activeLightThemeSlotNo: savedActiveLightThemeSlotNo,
activeDarkThemeSlotNo: savedActiveDarkThemeSlotNo,
themeSlots: savedThemeSlots,
editingThemeSlot: savedEditingThemeSlot,
})
const discardThemeChanges = () => {
setDraftActiveThemeSlot (savedActiveThemeSlot)
setDraftThemeMode (savedThemeMode)
setDraftActiveLightThemeSlotNo (savedActiveLightThemeSlotNo)
setDraftActiveDarkThemeSlotNo (savedActiveDarkThemeSlotNo)
setDraftThemeSlots (savedThemeSlots)
setEditingThemeSlot (null)
setEditingThemeSlot (savedEditingThemeSlot)
setCachedUserThemeSlots (savedThemeSlots)
setClientActiveThemeSlot (savedActiveThemeSlot)
setClientThemeAppearance ({
themeMode: savedThemeMode,
activeLightThemeSlotNo: savedActiveLightThemeSlotNo,
activeDarkThemeSlotNo: savedActiveDarkThemeSlotNo,
})
applyClientAppearance (savedThemeSlots)
}
@@ -800,66 +903,42 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
applyActiveTab (nextTab)
}
const requestThemeSlotSelection = async (
selection: ThemeSlotSelection,
editingSlot: UserThemeSlotSelection | null,
const requestThemeStateChange = async (
buildNextState: (baseState: ThemeDraftState) => ThemeDraftState,
description = 'このまま切り替えると、保存していない変更は失われます。',
cancelText = 'このまま編集する',
confirmText = '変更を破棄して切り替える',
): Promise<void> => {
if (
selection === draftActiveThemeSlot
&& editingSlot === editingThemeSlot
)
return
const baseState =
hasUnsavedThemeChanges
? savedThemeDraftState ()
: currentThemeDraftState ()
if (hasUnsavedThemeChanges)
{
const confirmed = await confirmDiscardChanges (
'このまま切り替えると、保存していない変更は失われます。',
'この枠に残る',
'変更を破棄して切り替える',
description,
cancelText,
confirmText,
)
if (!(confirmed))
return
discardThemeChanges ()
}
setEditingThemeSlot (editingSlot)
applyDraftThemeSelection (selection, savedThemeSlots)
}
const nextState = buildNextState (baseState)
const ensureEditableThemeSlot = async (): Promise<UserThemeSlotSelection | null> => {
if (editingThemeSlot != null)
return editingThemeSlot
if (isUserThemeSlotSelection (draftActiveThemeSlot))
{
setEditingThemeSlot (draftActiveThemeSlot)
return draftActiveThemeSlot
}
const choice = await dialogue.choice<UserThemeSlotSelection> ({
title: '保存先を選択してください',
description: '色を変更する前に、保存先のテーマ枠を選択してください。',
cancelText: '取消',
choices: themeSelections
.filter (selection => selection.editable)
.map (selection => ({
value: selection.id as UserThemeSlotSelection,
label: selection.label,
})),
applyDraftThemeState ({
themeMode: nextState.themeMode,
activeLightThemeSlotNo: nextState.activeLightThemeSlotNo,
activeDarkThemeSlotNo: nextState.activeDarkThemeSlotNo,
themeSlots: nextState.themeSlots,
nextEditingThemeSlot: nextState.editingThemeSlot,
})
if (choice == null)
return null
setEditingThemeSlot (choice)
applyDraftThemeSelection (choice, savedThemeSlots)
return choice
}
const updateDraftThemeSlot = (
selection: UserThemeSlotSelection,
updater: (tokens: ThemeTokens) => ThemeTokens,
options?: { activeThemeSlot?: ThemeSlotSelection },
) => {
const currentThemeSlot =
draftThemeSlots[selection]
@@ -872,10 +951,10 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
},
}
applyDraftThemeSelection (
options?.activeThemeSlot ?? draftActiveThemeSlot,
nextThemeSlots,
)
applyDraftThemeState ({
themeSlots: nextThemeSlots,
nextEditingThemeSlot: selection,
})
}
const handleTabKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
@@ -944,8 +1023,7 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
const draftThemeSlot = draftThemeSlots[selection]
const savedThemeSlot = savedThemeSlots[selection]
const requiresPersist =
selection === draftActiveThemeSlot
&& isUserThemeSlotSelection (draftActiveThemeSlot)
selection === editingThemeSlot
&& savedThemeSlot?.createdAt == null
if (
@@ -969,21 +1047,22 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
}
}
const themeMode =
deriveUserThemeModeFromSlotSelection (draftActiveThemeSlot)
const data = await updateUserSettings ({ theme: themeMode })
const data = await updateUserSettings ({ theme: draftThemeMode })
const completeThemeSlots = ensureCompleteThemeSlots (nextSavedThemeSlots)
setDraftSettings ({ ...data, theme: themeMode })
setSavedActiveThemeSlot (draftActiveThemeSlot)
setDraftSettings ({ ...data, theme: draftThemeMode })
setSavedThemeMode (draftThemeMode)
setSavedActiveLightThemeSlotNo (draftActiveLightThemeSlotNo)
setSavedActiveDarkThemeSlotNo (draftActiveDarkThemeSlotNo)
setSavedThemeSlots (completeThemeSlots)
setDraftThemeSlots (completeThemeSlots)
setEditingThemeSlot (
isUserThemeSlotSelection (draftActiveThemeSlot)
? draftActiveThemeSlot
: null,
)
setClientActiveThemeSlot (draftActiveThemeSlot)
setSavedEditingThemeSlot (editingThemeSlot)
setEditingThemeSlot (editingThemeSlot)
setClientThemeAppearance ({
themeMode: draftThemeMode,
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo,
})
setCachedUserThemeSlots (completeThemeSlots)
applyClientAppearance (completeThemeSlots)
toast ({ title: 'テーマ設定を保存しました.' })
@@ -1026,31 +1105,35 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
const storedThemeSlots = normaliseUserThemeSlots (themeSlots)
const completeThemeSlots = ensureCompleteThemeSlots (storedThemeSlots)
const initialSelection = (
const themeMode =
hasStoredClientThemeSelection ()
? getClientActiveThemeSlot ()
: themeToSelection (settings.theme)
)
const nextSelection = normaliseThemeSlotSelectionAgainstSlots (
initialSelection,
storedThemeSlots,
)
const themeMode = deriveUserThemeModeFromSlotSelection (nextSelection)
? getClientThemeMode ()
: settings.theme
const activeLightThemeSlotNo = getClientActiveLightThemeSlotNo ()
const activeDarkThemeSlotNo = getClientActiveDarkThemeSlotNo ()
const initialEditingThemeSlot = resolveDisplayedTheme ({
themeMode,
activeLightThemeSlotNo,
activeDarkThemeSlotNo,
}, completeThemeSlots).selection
setDraftSettings ({ ...settings, theme: themeMode })
setSavedActiveThemeSlot (nextSelection)
setDraftActiveThemeSlot (nextSelection)
setSavedThemeMode (themeMode)
setDraftThemeMode (themeMode)
setSavedActiveLightThemeSlotNo (activeLightThemeSlotNo)
setDraftActiveLightThemeSlotNo (activeLightThemeSlotNo)
setSavedActiveDarkThemeSlotNo (activeDarkThemeSlotNo)
setDraftActiveDarkThemeSlotNo (activeDarkThemeSlotNo)
setSavedThemeSlots (completeThemeSlots)
setDraftThemeSlots (completeThemeSlots)
setEditingThemeSlot (
isUserThemeSlotSelection (nextSelection)
? nextSelection
: null,
)
setSavedEditingThemeSlot (initialEditingThemeSlot)
setEditingThemeSlot (initialEditingThemeSlot)
setCachedUserThemeSlots (completeThemeSlots)
applyThemeSelection (nextSelection, completeThemeSlots)
if (nextSelection !== initialSelection)
setClientActiveThemeSlot (nextSelection)
applyThemeAppearanceState ({
themeMode,
activeLightThemeSlotNo,
activeDarkThemeSlotNo,
}, completeThemeSlots)
setSettingsError (null)
}
catch
@@ -1083,12 +1166,21 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
}, [location.pathname, location.search, settingsFieldErrors.theme])
useEffect (() => {
applyThemeSelection (draftActiveThemeSlot, draftThemeSlots)
applyThemeAppearanceState ({
themeMode: draftThemeMode,
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo,
}, draftThemeSlots)
return () => {
applyThemeSelection (savedActiveThemeSlot, savedThemeSlots)
setCachedUserThemeSlots (savedThemeSlots)
applyClientAppearance (savedThemeSlots)
}
}, [draftActiveThemeSlot, draftThemeSlots, savedActiveThemeSlot, savedThemeSlots])
}, [draftActiveDarkThemeSlotNo,
draftActiveLightThemeSlotNo,
draftThemeMode,
draftThemeSlots,
savedThemeSlots])
const accountSectionProps: AccountSectionProps | null =
user
@@ -1109,7 +1201,7 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
dirty: hasUnsavedThemeChanges,
discard: handleThemeDiscard,
})
}, [hasUnsavedThemeChanges, savedActiveThemeSlot, savedThemeSlots])
}, [hasUnsavedThemeChanges, savedThemeSlots])
useKeyboardShortcuts ({
'settings.account': () => {
@@ -1127,49 +1219,56 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
})
const themeSectionProps: ThemeSectionProps = {
draftActiveThemeSlot,
draftThemeSlots,
draftThemeMode,
draftActiveLightThemeSlotNo,
draftActiveDarkThemeSlotNo,
editingThemeSlot,
selectedTheme,
selectedThemeLabel: themeLabelBySelection (draftActiveThemeSlot),
displayedThemeLabel: themeLabelBySelection (displayedThemeSelection.selection),
systemThemeLabel: displayedThemeSelection.baseTheme === 'dark' ? 'ダーク' : 'ライト',
hasUnsavedChanges: hasUnsavedThemeChanges,
settingsBaseErrors,
settingsFieldErrors,
onSelectThemeSlot: selection => {
void requestThemeSlotSelection (selection, null)
onSelectThemeMode: themeMode => {
void requestThemeStateChange (baseState => ({
...baseState,
themeMode,
}))
},
onEditThemeSlot: selection => {
void requestThemeSlotSelection (selection, selection)
onSelectLightThemeSlotNo: slotNo => {
void requestThemeStateChange (baseState => ({
...baseState,
activeLightThemeSlotNo: slotNo,
editingThemeSlot: getUserThemeSlotSelection ('light', slotNo),
}))
},
onEnsureEditableTheme: () => {
void ensureEditableThemeSlot ()
onSelectDarkThemeSlotNo: slotNo => {
void requestThemeStateChange (baseState => ({
...baseState,
activeDarkThemeSlotNo: slotNo,
editingThemeSlot: getUserThemeSlotSelection ('dark', slotNo),
}))
},
onSelectEditingThemeSlot: selection => {
void requestThemeStateChange (baseState => ({
...baseState,
editingThemeSlot: selection,
}))
},
onThemeTokenChange: (key, value) => {
void (async () => {
const editableThemeSlot = await ensureEditableThemeSlot ()
if (editableThemeSlot == null)
return
updateDraftThemeSlot (editableThemeSlot, tokens => ({
...tokens,
[key]: value,
}), { activeThemeSlot: editableThemeSlot })
}) ()
updateDraftThemeSlot (editingThemeSlot, tokens => ({
...tokens,
[key]: value,
}))
},
onTagColourChange: (category, colour) => {
void (async () => {
const editableThemeSlot = await ensureEditableThemeSlot ()
if (editableThemeSlot == null)
return
updateDraftThemeSlot (editableThemeSlot, tokens => ({
...tokens,
tagColours: {
...tokens.tagColours,
[category]: colour,
},
}), { activeThemeSlot: editableThemeSlot })
}) ()
updateDraftThemeSlot (editingThemeSlot, tokens => ({
...tokens,
tagColours: {
...tokens.tagColours,
[category]: colour,
},
}))
},
onResetThemeSlot: selection => {
const baseTheme =