diff --git a/AGENTS.md b/AGENTS.md index fa48f25..94f69a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -271,6 +271,9 @@ const value = - Preserve the local TSX formatting style. - Do not normalize TSX to common Prettier-style React formatting unless explicitly asked. +- Treat TSX formatting rules as hard constraints, not preferences. Before + finishing a TSX edit, inspect the edited hunks for closing `)`, `]`, and `}` + placement and fix violations instead of relying on formatter defaults. - Prefer `const` arrow functions for TypeScript/TSX component and helper declarations. - Put two blank lines before and after top-level `const` function declarations, unless imports, exports, or file boundaries make that awkward. @@ -281,14 +284,29 @@ const value = - In TypeScript and TSX function declarations, including `const` arrow function declarations, when the parameter list spans multiple lines, always put the closing parenthesis at the beginning of its own line before the return - type or `=>`. + type or `=>`. Do not put that closing `)` at the end of the previous line. +- Do not write React component declarations in the common Prettier form + `const Component = ({ ... }) => (` when the destructured parameter spans + multiple lines. Use the project form with the opening `(` after `=`, the + destructured object as the argument, and the closing parameter `)` on its own + line before `=>`. - In TypeScript and TSX, never place a closing parenthesis at the beginning of a line except for a multi-line function declaration parameter list. - Never place a closing square bracket at the beginning of a line. -- For object literals and other associative-array-style braces, do not place - the closing brace at the beginning of a line. Function, lambda, callback, and - block closing braces are exempt and should stay on their own line when that - fits the local style. +- For object literals, type literals, import/export named bindings, destructuring + patterns, and other associative-array-style braces, do not place the closing + brace at the beginning of a line. Keep `}` on the same line as the final + property, binding, or specifier unless that would violate the line limit. + Function, lambda, callback, and block closing braces are exempt and should + stay on their own line when that fits the local style. When a function, + lambda, callback, or block body spans multiple lines, do not put its closing + `}` at the end of the previous line. +- For arrays and tuple-like lists, do not place the closing `]` at the + beginning of a line. Keep `]` on the same line as the final element unless + that would violate the line limit. +- For function and method calls, do not place the closing `)` at the beginning + of a line. The only TypeScript/TSX exception is the closing parameter `)` of a + multi-line function declaration. - When writing braces on a single line in TypeScript or TSX JavaScript context, put exactly one space inside the braces, as in `{ value }` or `{ key: value }`. diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 67f7b09..eb9c576 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -126,9 +126,38 @@ pass or the remaining failure is clearly blocked. - Preserve compact TSX expression shapes such as inline ternary branches and closing `)` forms when nearby code uses them. +- Treat TSX formatting rules as hard constraints, not preferences. Before + finishing a TSX edit, inspect the edited hunks for closing `)`, `]`, and `}` + placement and fix violations instead of relying on formatter defaults. - For long Tailwind `className` strings, wrap across lines only when needed. - Keep continuation indentation aligned with the 4-space logical indentation rule, using tabs only as leading 8-space compression. +- In TypeScript and TSX function declarations, including `const` arrow + function declarations, when the parameter list spans multiple lines, always + put the closing parenthesis at the beginning of its own line before the return + type or `=>`. Do not put that closing `)` at the end of the previous line. +- Do not write React component declarations in the common Prettier form + `const Component = ({ ... }) => (` when the destructured parameter spans + multiple lines. Use the project form with the opening `(` after `=`, the + destructured object as the argument, and the closing parameter `)` on its own + line before `=>`. +- In TypeScript and TSX, never place a closing parenthesis at the beginning of + a line except for a multi-line function declaration parameter list. +- Never place a closing square bracket at the beginning of a line. +- For object literals, type literals, import/export named bindings, destructuring + patterns, and other associative-array-style braces, do not place the closing + brace at the beginning of a line. Keep `}` on the same line as the final + property, binding, or specifier unless that would violate the line limit. + Function, lambda, callback, and block closing braces are exempt and should + stay on their own line when that fits the local style. When a function, + lambda, callback, or block body spans multiple lines, do not put its closing + `}` at the end of the previous line. +- For arrays and tuple-like lists, do not place the closing `]` at the + beginning of a line. Keep `]` on the same line as the final element unless + that would violate the line limit. +- For function and method calls, do not place the closing `)` at the beginning + of a line. The only TypeScript/TSX exception is the closing parameter `)` of a + multi-line function declaration. - Do not add braces around `if`, `else`, or `for` bodies when the body is a single physical line. - Always add braces around `if`, `else`, or `for` bodies when the body spans diff --git a/frontend/src/pages/users/SettingPage.tsx b/frontend/src/pages/users/SettingPage.tsx index 49294d9..1650178 100644 --- a/frontend/src/pages/users/SettingPage.tsx +++ b/frontend/src/pages/users/SettingPage.tsx @@ -1,5 +1,3 @@ -import type { CSSProperties, Dispatch, FC, KeyboardEvent, SetStateAction } from 'react' - import { useEffect, useMemo, useState } from 'react' import { Helmet } from 'react-helmet-async' import { useLocation, useNavigate } from 'react-router-dom' @@ -18,38 +16,35 @@ import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/use-toast' import { SITE_TITLE } from '@/config' import { apiPut } from '@/lib/api' -import { - applyThemeSelection, - BUILTIN_THEMES, - createCustomThemeFromBase, - defaultThemeTagColoursFor, - DEFAULT_USER_SETTINGS, - deriveUserThemeModeFromSelection, - fetchUserSettings, - hasStoredClientThemeSelection, - getClientActiveThemeId, - getClientCustomThemes, - getResolvedThemeFromSelection, - setClientAppearanceThemes, - updateUserSettings, -} from '@/lib/settings' +import { applyThemeSelection, + BUILTIN_THEMES, + createCustomThemeFromBase, + defaultThemeTagColoursFor, + DEFAULT_USER_SETTINGS, + deriveUserThemeModeFromSelection, + fetchUserSettings, + hasStoredClientThemeSelection, + getClientActiveThemeId, + getClientCustomThemes, + getResolvedThemeFromSelection, + setClientAppearanceThemes, + updateUserSettings } from '@/lib/settings' import { useKeyboardShortcutSettings } from '@/lib/useKeyboardShortcuts' import { cn, inputClass } from '@/lib/utils' import { useValidationErrors } from '@/lib/useValidationErrors' +import type { CSSProperties, Dispatch, FC, KeyboardEvent, SetStateAction } from 'react' + import type { Category, Tag, User } from '@/types' -import type { - ActiveThemeId, - ClientTheme, - CustomClientTheme, - ThemeTagColours, - UserSettings, -} from '@/lib/settings' +import type { ActiveThemeId, + ClientTheme, + CustomClientTheme, + ThemeTagColours, + UserSettings } from '@/lib/settings' type Props = { user: User | null - setUser: Dispatch> -} + setUser: Dispatch> } type UserFormField = 'name' type SettingsFormField = 'theme' @@ -79,14 +74,12 @@ type SharedSectionProps = { onResetTagColour: (category: Category) => void onThemeSubmit: () => void settingsBaseErrors: string[] - settingsFieldErrors: Partial> -} + settingsFieldErrors: Partial> } const tabs: TabSpec[] = [ { id: 'account', label: 'アカウント' }, { id: 'theme', label: 'テーマ' }, - { id: 'keyboard', label: 'キーボード' }, -] + { id: 'keyboard', label: 'キーボード' } ] const sectionClassName = 'space-y-4 rounded-xl border border-border bg-background/80 p-4' @@ -94,13 +87,12 @@ const sectionClassName = const themeLabel: Record = { system: 'システム設定に従う', light: 'ライト・モード', - dark: 'ダーク・モード', -} + dark: 'ダーク・モード' } const builtinThemeLabel: Record<'light' | 'dark', string> = { light: 'ライト・モード', - dark: 'ダーク・モード', -} + dark: 'ダーク・モード' } + const previewTag = ( id: number, @@ -122,8 +114,7 @@ const previewTag = ( hasWiki, materialId: null, hasDeerjikists: options?.hasDeerjikists ?? false, - children: [], -}) + children: [] }) const previewTags: Record = { deerjikist: previewTag (1, 'にじらー', 'deerjikist', 12, true, { hasDeerjikists: true }), @@ -132,15 +123,14 @@ const previewTags: Record = { general: previewTag (4, '一般', 'general', 78, true), material: previewTag (5, '素材', 'material', 90, false), meta: previewTag (6, 'メタ', 'meta', 21, true), - nico: previewTag (7, 'nico:sm9', 'nico', 43, false), -} + nico: previewTag (7, 'nico:sm9', 'nico', 43, false) } const parseTab = (search: string): SettingsTab => { const tab = new URLSearchParams (search).get ('tab') - return tabs.some (candidate => candidate.id === tab) - ? tab as SettingsTab - : 'account' + return (tabs.some (candidate => candidate.id === tab) + ? (tab as SettingsTab) + : 'account') } @@ -152,9 +142,7 @@ const themePreviewStyle = (tagColours: ThemeTagColours): CSSProperties => Object.fromEntries ( CATEGORIES.flatMap (category => [ [`--tag-colour-${ category }`, tagColours[category]], - [`--tag-colour-${ category }-hover`, tagColours[category]], - ]), - ) as CSSProperties + [`--tag-colour-${ category }-hover`, tagColours[category]]])) as CSSProperties const themeLabelById = ( @@ -169,16 +157,16 @@ const themeLabelById = ( } -const AccountSection: FC = ({ - name, - onInheritOpen, - onNameChange, - onUserCodeOpen, - onUserSubmit, - user, - userBaseErrors, - userFieldErrors, -}) => ( +const AccountSection: FC = ( + { name, + onInheritOpen, + onNameChange, + onUserCodeOpen, + onUserSubmit, + user, + userBaseErrors, + userFieldErrors }, +) => (

アカウント

@@ -229,20 +217,20 @@ const AccountSection: FC = ({
) -const ThemeSection: FC = ({ - draftActiveThemeId, - draftCustomThemes, - selectedTheme, - draftTagColours, - onEnsureEditableTheme, - onResetAllTagColours, - onResetTagColour, - onTagColourChange, - onThemeChange, - onThemeSubmit, - settingsBaseErrors, - settingsFieldErrors, -}) => ( +const ThemeSection: FC = ( + { draftActiveThemeId, + draftCustomThemes, + selectedTheme, + draftTagColours, + onEnsureEditableTheme, + onResetAllTagColours, + onResetTagColour, + onTagColourChange, + onThemeChange, + onThemeSubmit, + settingsBaseErrors, + settingsFieldErrors }, +) => (

テーマ

@@ -389,24 +377,20 @@ const SettingPage: FC = ({ user, setUser }) => { useState> (getClientCustomThemes ()) const [userCodeVsbl, setUserCodeVsbl] = useState (false) const { conflictActionIds } = useKeyboardShortcutSettings () - const { - baseErrors: userBaseErrors, - fieldErrors: userFieldErrors, - clearValidationErrors: clearUserValidationErrors, - applyValidationError: applyUserValidationError, - } = useValidationErrors () - const { - baseErrors: settingsBaseErrors, - fieldErrors: settingsFieldErrors, - clearValidationErrors: clearSettingsValidationErrors, - applyValidationError: applySettingsValidationError, - } = useValidationErrors () + const { baseErrors: userBaseErrors, + fieldErrors: userFieldErrors, + clearValidationErrors: clearUserValidationErrors, + applyValidationError: applyUserValidationError } = useValidationErrors () + const { baseErrors: settingsBaseErrors, + fieldErrors: settingsFieldErrors, + clearValidationErrors: clearSettingsValidationErrors, + applyValidationError: applySettingsValidationError } = + useValidationErrors () const activeTab = useMemo (() => parseTab (location.search), [location.search]) const selectedTheme = useMemo ( () => getResolvedThemeFromSelection (draftActiveThemeId, draftCustomThemes), - [draftActiveThemeId, draftCustomThemes], - ) + [draftActiveThemeId, draftCustomThemes]) const draftTagColours = selectedTheme.tokens.tagColours const setActiveTab = (tab: SettingsTab) => { @@ -414,15 +398,15 @@ const SettingPage: FC = ({ user, setUser }) => { params.set ('tab', tab) navigate ( `${ location.pathname }?${ params.toString () }`, - { replace: true }, - ) + { replace: true }) } - const settingsTabErrors = useMemo> (() => ({ - account: Boolean (userFieldErrors.name?.length), - theme: Boolean (settingsFieldErrors.theme?.length), - keyboard: conflictActionIds.size > 0, - }), [conflictActionIds.size, settingsFieldErrors.theme, userFieldErrors.name]) + const settingsTabErrors = useMemo> ( + () => ( + { account: Boolean (userFieldErrors.name?.length), + theme: Boolean (settingsFieldErrors.theme?.length), + keyboard: conflictActionIds.size > 0 }), + [conflictActionIds.size, settingsFieldErrors.theme, userFieldErrors.name]) const applyDraftThemeSelection = ( activeThemeId: ActiveThemeId, @@ -432,52 +416,40 @@ const SettingPage: FC = ({ user, setUser }) => { setDraftCustomThemes (customThemes) setDraftSettings (current => ({ ...current, - theme: deriveUserThemeModeFromSelection (activeThemeId, customThemes), - })) + theme: deriveUserThemeModeFromSelection (activeThemeId, customThemes) })) applyThemeSelection (activeThemeId, customThemes) } const ensureEditableCustomTheme = (): { activeThemeId: ActiveThemeId - customThemes: Record - } => { + customThemes: Record } => { if (draftActiveThemeId !== 'system' && draftCustomThemes[draftActiveThemeId] != null) { - return { - activeThemeId: draftActiveThemeId, - customThemes: draftCustomThemes, - } + return { activeThemeId: draftActiveThemeId, + customThemes: draftCustomThemes } } const customTheme = createCustomThemeFromBase ( selectedTheme.baseTheme, - { - ...selectedTheme.tokens, - tagColours: { ...selectedTheme.tokens.tagColours }, - }, - ) + { ...selectedTheme.tokens, + tagColours: { ...selectedTheme.tokens.tagColours } }) const nextCustomThemes = { ...draftCustomThemes, - [customTheme.id]: customTheme, - } + [customTheme.id]: customTheme } return { activeThemeId: customTheme.id, - customThemes: nextCustomThemes, - } + customThemes: nextCustomThemes } } const handleEnsureEditableTheme = (): { activeThemeId: ActiveThemeId - customThemes: Record - } => { + customThemes: Record } => { const editable = ensureEditableCustomTheme () if (editable.activeThemeId !== draftActiveThemeId || editable.customThemes !== draftCustomThemes) - { - applyDraftThemeSelection (editable.activeThemeId, editable.customThemes) - } + applyDraftThemeSelection (editable.activeThemeId, editable.customThemes) return editable } @@ -580,9 +552,9 @@ const SettingPage: FC = ({ user, setUser }) => { return const customThemes = getClientCustomThemes () - const activeThemeId = hasStoredClientThemeSelection () - ? getClientActiveThemeId () - : data.theme + const activeThemeId = (hasStoredClientThemeSelection () + ? getClientActiveThemeId () + : data.theme) const themeMode = deriveUserThemeModeFromSelection (activeThemeId, customThemes) setDraftSettings ({ ...data, theme: themeMode }) @@ -629,71 +601,62 @@ const SettingPage: FC = ({ user, setUser }) => { } }, [draftActiveThemeId, draftCustomThemes, savedActiveThemeId, savedCustomThemes]) - const sectionProps: SharedSectionProps | null = user ? { - user, - name, - onNameChange: setName, - onInheritOpen: () => setInheritVsbl (true), - onUserCodeOpen: () => setUserCodeVsbl (true), - onUserSubmit: handleUserSubmit, - userBaseErrors, - userFieldErrors, - draftActiveThemeId, - draftCustomThemes, - selectedTheme, - draftTagColours, - onThemeChange: themeId => { - applyDraftThemeSelection (themeId, draftCustomThemes) - }, - onEnsureEditableTheme: () => { - handleEnsureEditableTheme () - }, - onTagColourChange: (category, colour) => { - const editable = handleEnsureEditableTheme () - const editableTheme = editable.customThemes[editable.activeThemeId] - const nextCustomThemes = { - ...editable.customThemes, - [editable.activeThemeId]: { - ...editableTheme, - tokens: { - ...editableTheme.tokens, - tagColours: { - ...editableTheme.tokens.tagColours, - [category]: colour, - }, - }, - }, - } - applyDraftThemeSelection (editable.activeThemeId, nextCustomThemes) - }, - onResetAllTagColours: () => { - if (selectedTheme.builtin || draftActiveThemeId === 'system') - return + const sectionProps: SharedSectionProps | null = + user + ? { user, + name, + onNameChange: setName, + onInheritOpen: () => setInheritVsbl (true), + onUserCodeOpen: () => setUserCodeVsbl (true), + onUserSubmit: handleUserSubmit, + userBaseErrors, + userFieldErrors, + draftActiveThemeId, + draftCustomThemes, + selectedTheme, + draftTagColours, + onThemeChange: themeId => { + applyDraftThemeSelection (themeId, draftCustomThemes) + }, + onEnsureEditableTheme: () => { + handleEnsureEditableTheme () + }, + onTagColourChange: (category, colour) => { + const editable = handleEnsureEditableTheme () + const editableTheme = editable.customThemes[editable.activeThemeId] + const nextCustomThemes = { + ...editable.customThemes, + [editable.activeThemeId]: { + ...editableTheme, + tokens: { + ...editableTheme.tokens, + tagColours: { + ...editableTheme.tokens.tagColours, + [category]: colour } } } } + applyDraftThemeSelection (editable.activeThemeId, nextCustomThemes) + }, + onResetAllTagColours: () => { + if (selectedTheme.builtin || draftActiveThemeId === 'system') + return - applyDraftThemeSelection ( - draftActiveThemeId, - { - ...draftCustomThemes, - [draftActiveThemeId]: { - ...draftCustomThemes[draftActiveThemeId], - tokens: { - ...draftCustomThemes[draftActiveThemeId].tokens, - tagColours: { - ...defaultThemeTagColoursFor (selectedTheme.baseTheme), - }, - }, - }, - }, - ) - }, - onResetTagColour: category => { - if (selectedTheme.builtin || draftActiveThemeId === 'system') - return + applyDraftThemeSelection ( + draftActiveThemeId, + { + ...draftCustomThemes, + [draftActiveThemeId]: { + ...draftCustomThemes[draftActiveThemeId], + tokens: { + ...draftCustomThemes[draftActiveThemeId].tokens, + tagColours: { + ...defaultThemeTagColoursFor (selectedTheme.baseTheme) } } } }) + }, + onResetTagColour: category => { + if (selectedTheme.builtin || draftActiveThemeId === 'system') + return - applyDraftThemeSelection ( - draftActiveThemeId, - { - ...draftCustomThemes, + applyDraftThemeSelection ( + draftActiveThemeId, + { ...draftCustomThemes, [draftActiveThemeId]: { ...draftCustomThemes[draftActiveThemeId], tokens: { @@ -701,17 +664,13 @@ const SettingPage: FC = ({ user, setUser }) => { tagColours: { ...draftCustomThemes[draftActiveThemeId].tokens.tagColours, [category]: - defaultThemeTagColoursFor (selectedTheme.baseTheme)[category], - }, - }, - }, - }, - ) - }, - onThemeSubmit: handleThemeSubmit, - settingsBaseErrors, - settingsFieldErrors, - } : null + defaultThemeTagColoursFor ( + selectedTheme.baseTheme)[category] } } } }) + }, + onThemeSubmit: handleThemeSubmit, + settingsBaseErrors, + settingsFieldErrors } + : null return ( @@ -752,12 +711,11 @@ const SettingPage: FC = ({ user, setUser }) => { 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'], - )} + (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} @@ -778,8 +736,7 @@ const SettingPage: FC = ({ user, setUser }) => { {activeTab === 'account' && } {activeTab === 'theme' && } {activeTab === 'keyboard' && ( - - )} + )} )} )} @@ -799,4 +756,5 @@ const SettingPage: FC = ({ user, setUser }) => { ) } + export default SettingPage