設定画面 (#34) #397
@@ -274,6 +274,30 @@ const value =
|
|||||||
- Treat TSX formatting rules as hard constraints, not preferences. Before
|
- Treat TSX formatting rules as hard constraints, not preferences. Before
|
||||||
finishing a TSX edit, inspect the edited hunks for closing `)`, `]`, and `}`
|
finishing a TSX edit, inspect the edited hunks for closing `)`, `]`, and `}`
|
||||||
placement and fix violations instead of relying on formatter defaults.
|
placement and fix violations instead of relying on formatter defaults.
|
||||||
|
- After every TSX edit, perform a style-only self-review of the edited hunks
|
||||||
|
before running verification or reporting completion. The task is not complete
|
||||||
|
while any edited TSX hunk violates these local formatting rules.
|
||||||
|
- The TSX self-review must classify every edited leading or trailing `)`, `]`,
|
||||||
|
and `}` by syntax role before deciding whether it is valid. Do not apply a
|
||||||
|
rule by glyph alone. A closing `)` for a function parameter list is different
|
||||||
|
from a closing `)` for a function call. A closing `}` for a block is different
|
||||||
|
from a closing `}` for an object, type literal, import list, or destructuring
|
||||||
|
pattern.
|
||||||
|
- The TSX self-review must confirm there are no common Prettier-style React
|
||||||
|
component declarations with a multi-line destructured parameter.
|
||||||
|
- The TSX self-review must confirm multi-line function declaration parameter
|
||||||
|
`)` placement follows the detailed parameter-list rules below.
|
||||||
|
- The TSX self-review must confirm call-expression `)` is never at the
|
||||||
|
beginning of a line.
|
||||||
|
- The TSX self-review must confirm object/type/import/destructuring `}` is not
|
||||||
|
at the beginning of a line.
|
||||||
|
- The TSX self-review must confirm multi-line function/lambda/callback/block
|
||||||
|
`}` is on its own line and never at the end of the previous line.
|
||||||
|
- The TSX self-review must confirm array `]` is not at the beginning of a line.
|
||||||
|
- The TSX self-review must confirm JSX closing markers and closing parentheses
|
||||||
|
keep the surrounding compact style.
|
||||||
|
- The TSX self-review must confirm leading indentation follows 4-space logical
|
||||||
|
indentation with tabs only as leading 8-space compression.
|
||||||
- Prefer `const` arrow functions for TypeScript/TSX component and helper declarations.
|
- Prefer `const` arrow functions for TypeScript/TSX component and helper declarations.
|
||||||
- Put two blank lines before and after top-level `const` function
|
- Put two blank lines before and after top-level `const` function
|
||||||
declarations, unless imports, exports, or file boundaries make that awkward.
|
declarations, unless imports, exports, or file boundaries make that awkward.
|
||||||
@@ -282,9 +306,15 @@ const value =
|
|||||||
character.
|
character.
|
||||||
- A leading tab is exactly equivalent to 8 leading spaces.
|
- A leading tab is exactly equivalent to 8 leading spaces.
|
||||||
- In TypeScript and TSX function declarations, including `const` arrow
|
- In TypeScript and TSX function declarations, including `const` arrow
|
||||||
function declarations, when the parameter list spans multiple lines, always
|
function declarations, classify the parameter list before placing the closing
|
||||||
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.
|
- If the parameter list itself is given its own multi-line block after the
|
||||||
|
function's opening `(`, put the closing parameter `)` at the beginning of its
|
||||||
|
own line before the return type or `=>`.
|
||||||
|
- If the only line break is inside a parameter's inline type, object type, or
|
||||||
|
destructuring shape, and the parameter list itself is not split as a separate
|
||||||
|
block, keep the closing parameter `)` on the same line as that parameter's
|
||||||
|
final `}`. In this case, moving `)` to a new line is wrong.
|
||||||
- Do not write React component declarations in the common Prettier form
|
- Do not write React component declarations in the common Prettier form
|
||||||
`const Component = ({ ... }) => (` when the destructured parameter spans
|
`const Component = ({ ... }) => (` when the destructured parameter spans
|
||||||
multiple lines. Use the project form with the opening `(` after `=`, the
|
multiple lines. Use the project form with the opening `(` after `=`, the
|
||||||
@@ -325,6 +355,356 @@ const value =
|
|||||||
- Do not use a leading semicolon for expression statements such as
|
- Do not use a leading semicolon for expression statements such as
|
||||||
`;([...]).forEach(...)`; rewrite the expression to avoid ASI hazards
|
`;([...]).forEach(...)`; rewrite the expression to avoid ASI hazards
|
||||||
explicitly, for example with `void`.
|
explicitly, for example with `void`.
|
||||||
|
- Spell British English identifiers correctly when the feature name uses
|
||||||
|
British English. Use `Behaviour`, not `Behavior`, for new component and file
|
||||||
|
names unless editing an already established American-English API.
|
||||||
|
|
||||||
|
### Frontend delimiter decision table
|
||||||
|
|
||||||
|
Use this table before accepting any edited TypeScript or TSX hunk. The table is
|
||||||
|
more authoritative than formatter habit.
|
||||||
|
|
||||||
|
#### Import and export named bindings
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
} from '@/components/ui'
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Button,
|
||||||
|
Card } from '@/components/ui'
|
||||||
|
```
|
||||||
|
|
||||||
|
Also acceptable when short:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Button, Card } from '@/components/ui'
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: named-binding `}` is associative-array-style syntax. It must not be alone
|
||||||
|
at the beginning of a line. Prefer keeping `{` with the first binding and `}`
|
||||||
|
with the final binding when this fits the line limit.
|
||||||
|
|
||||||
|
#### Type literals
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void }
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: type-literal `}` is not a block close. It must stay on the same line as
|
||||||
|
the final property unless that would break the hard line limit.
|
||||||
|
|
||||||
|
#### Object literals
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const value = {
|
||||||
|
open,
|
||||||
|
activeScope,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const value = {
|
||||||
|
open,
|
||||||
|
activeScope }
|
||||||
|
```
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const value = useMemo (() => ({
|
||||||
|
open,
|
||||||
|
activeScope,
|
||||||
|
}), [open, activeScope])
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const value = useMemo (() => ({
|
||||||
|
open,
|
||||||
|
activeScope }), [open, activeScope])
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: object-literal `}` is associative-array-style syntax. It must not be on a
|
||||||
|
line by itself. Keep it with the final property, and keep call `)` off the
|
||||||
|
beginning of a line.
|
||||||
|
|
||||||
|
#### Destructuring parameters
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const Component: FC<Props> = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}) => {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const Component: FC<Props> = (
|
||||||
|
{ open,
|
||||||
|
onOpenChange },
|
||||||
|
) => {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: when a React component or helper takes a destructured object parameter
|
||||||
|
that spans multiple lines, do not use the Prettier-style `= ({ ... }) =>`
|
||||||
|
shape. Put the function parameter list on its own lines. The destructuring `}`
|
||||||
|
stays with the final binding. The parameter-list `)` is then allowed and
|
||||||
|
required at the beginning of its own line.
|
||||||
|
|
||||||
|
#### Inline typed destructuring parameter
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const RouteTransitionWrapper = ({ user, setUser }: {
|
||||||
|
user: User | null
|
||||||
|
setUser: Dispatch<SetStateAction<User | null>> }) => {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const RouteTransitionWrapper = ({ user, setUser }: {
|
||||||
|
user: User | null
|
||||||
|
setUser: Dispatch<SetStateAction<User | null>>
|
||||||
|
}) => {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: this is not a separately split parameter-list block. The line break is
|
||||||
|
inside the inline type. Keep the type-literal `}` and parameter-list `)` on the
|
||||||
|
same line as the final type property.
|
||||||
|
|
||||||
|
#### Multi-line normal parameter list
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const updateDraft = <Key extends keyof Settings,> (
|
||||||
|
key: Key,
|
||||||
|
value: Settings[Key]) => {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const updateDraft = <Key extends keyof Settings,> (
|
||||||
|
key: Key,
|
||||||
|
value: Settings[Key],
|
||||||
|
) => {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: when the parameter list itself is split across multiple parameter lines,
|
||||||
|
the closing parameter `)` goes at the beginning of its own line before `=>` or
|
||||||
|
the return type.
|
||||||
|
|
||||||
|
#### Function and callback blocks
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleSave = () => { save ()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleSave = () => {
|
||||||
|
save () }
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleSave = () => {
|
||||||
|
save ()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: block `}` closes executable code, not associative data. Multi-line
|
||||||
|
function, lambda, callback, `if`, `for`, `switch`, and similar block braces
|
||||||
|
belong on their own line.
|
||||||
|
|
||||||
|
#### Function and method calls
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const value = compute (
|
||||||
|
first,
|
||||||
|
second
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const value = compute (
|
||||||
|
first,
|
||||||
|
second)
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: call-expression `)` must not be at the beginning of a line. The exception
|
||||||
|
for leading `)` applies only to function declaration parameter lists, never to
|
||||||
|
calls.
|
||||||
|
|
||||||
|
#### JSX closing markers
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSave}>
|
||||||
|
保存
|
||||||
|
</Button>)
|
||||||
|
```
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: keep `>` or `/>` with the final prop, and keep JSX closing parentheses in
|
||||||
|
the local compact form such as `</div>)`.
|
||||||
|
|
||||||
|
#### Arrays and tuples
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const items = [
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const items = [
|
||||||
|
first,
|
||||||
|
second]
|
||||||
|
```
|
||||||
|
|
||||||
|
Good when short:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const items = [first, second]
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: array and tuple `]` must not be at the beginning of a line. Keep it with
|
||||||
|
the final element unless that would break the hard line limit.
|
||||||
|
|
||||||
|
#### Single-line braces
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const next = { ...current, enabled: true }
|
||||||
|
const tag = { id, name }
|
||||||
|
```
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Component prop={ value }/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Component prop={value}/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: JavaScript object braces on one line get one inner space. JSX expression
|
||||||
|
braces do not get inner spaces.
|
||||||
|
|
||||||
|
#### Final TSX self-review checklist
|
||||||
|
|
||||||
|
Before reporting completion after a TypeScript or TSX edit, check the edited
|
||||||
|
hunks line by line:
|
||||||
|
|
||||||
|
1. No import/export/type/object/destructuring `}` appears alone at the beginning
|
||||||
|
of a line.
|
||||||
|
2. No call-expression `)` appears at the beginning of a line.
|
||||||
|
3. Any leading `)` is definitely closing a function declaration parameter list,
|
||||||
|
not a call.
|
||||||
|
4. Any parameter-list `)` at the end of a line is valid because the parameter
|
||||||
|
list itself was not split, only an inline type or destructuring shape was.
|
||||||
|
5. No array or tuple `]` appears at the beginning of a line.
|
||||||
|
6. Multi-line executable block `}` is on its own line.
|
||||||
|
7. JSX `>` and `/>` stay with the final prop unless nearby code proves
|
||||||
|
otherwise.
|
||||||
|
8. JSX closing parentheses keep the compact local style.
|
||||||
|
9. Leading indentation is 4-space logical indentation with tabs used only as
|
||||||
|
leading 8-space compression.
|
||||||
|
10. No line has trailing whitespace.
|
||||||
|
|
||||||
Preferred:
|
Preferred:
|
||||||
|
|
||||||
|
|||||||
+383
-3
@@ -129,13 +129,43 @@ pass or the remaining failure is clearly blocked.
|
|||||||
- Treat TSX formatting rules as hard constraints, not preferences. Before
|
- Treat TSX formatting rules as hard constraints, not preferences. Before
|
||||||
finishing a TSX edit, inspect the edited hunks for closing `)`, `]`, and `}`
|
finishing a TSX edit, inspect the edited hunks for closing `)`, `]`, and `}`
|
||||||
placement and fix violations instead of relying on formatter defaults.
|
placement and fix violations instead of relying on formatter defaults.
|
||||||
|
- After every TSX edit, perform a style-only self-review of the edited hunks
|
||||||
|
before running verification or reporting completion. The task is not complete
|
||||||
|
while any edited TSX hunk violates these local formatting rules.
|
||||||
|
- The TSX self-review must classify every edited leading or trailing `)`, `]`,
|
||||||
|
and `}` by syntax role before deciding whether it is valid. Do not apply a
|
||||||
|
rule by glyph alone. A closing `)` for a function parameter list is different
|
||||||
|
from a closing `)` for a function call. A closing `}` for a block is different
|
||||||
|
from a closing `}` for an object, type literal, import list, or destructuring
|
||||||
|
pattern.
|
||||||
|
- The TSX self-review must confirm there are no common Prettier-style React
|
||||||
|
component declarations with a multi-line destructured parameter.
|
||||||
|
- The TSX self-review must confirm multi-line function declaration parameter
|
||||||
|
`)` placement follows the detailed parameter-list rules below.
|
||||||
|
- The TSX self-review must confirm call-expression `)` is never at the
|
||||||
|
beginning of a line.
|
||||||
|
- The TSX self-review must confirm object/type/import/destructuring `}` is not
|
||||||
|
at the beginning of a line.
|
||||||
|
- The TSX self-review must confirm multi-line function/lambda/callback/block
|
||||||
|
`}` is on its own line and never at the end of the previous line.
|
||||||
|
- The TSX self-review must confirm array `]` is not at the beginning of a line.
|
||||||
|
- The TSX self-review must confirm JSX closing markers and closing parentheses
|
||||||
|
keep the surrounding compact style.
|
||||||
|
- The TSX self-review must confirm leading indentation follows 4-space logical
|
||||||
|
indentation with tabs only as leading 8-space compression.
|
||||||
- For long Tailwind `className` strings, wrap across lines only when needed.
|
- For long Tailwind `className` strings, wrap across lines only when needed.
|
||||||
- Keep continuation indentation aligned with the 4-space logical indentation
|
- Keep continuation indentation aligned with the 4-space logical indentation
|
||||||
rule, using tabs only as leading 8-space compression.
|
rule, using tabs only as leading 8-space compression.
|
||||||
- In TypeScript and TSX function declarations, including `const` arrow
|
- In TypeScript and TSX function declarations, including `const` arrow
|
||||||
function declarations, when the parameter list spans multiple lines, always
|
function declarations, classify the parameter list before placing the closing
|
||||||
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.
|
- If the parameter list itself is given its own multi-line block after the
|
||||||
|
function's opening `(`, put the closing parameter `)` at the beginning of its
|
||||||
|
own line before the return type or `=>`.
|
||||||
|
- If the only line break is inside a parameter's inline type, object type, or
|
||||||
|
destructuring shape, and the parameter list itself is not split as a separate
|
||||||
|
block, keep the closing parameter `)` on the same line as that parameter's
|
||||||
|
final `}`. In this case, moving `)` to a new line is wrong.
|
||||||
- Do not write React component declarations in the common Prettier form
|
- Do not write React component declarations in the common Prettier form
|
||||||
`const Component = ({ ... }) => (` when the destructured parameter spans
|
`const Component = ({ ... }) => (` when the destructured parameter spans
|
||||||
multiple lines. Use the project form with the opening `(` after `=`, the
|
multiple lines. Use the project form with the opening `(` after `=`, the
|
||||||
@@ -162,8 +192,358 @@ pass or the remaining failure is clearly blocked.
|
|||||||
single physical line.
|
single physical line.
|
||||||
- Always add braces around `if`, `else`, or `for` bodies when the body spans
|
- Always add braces around `if`, `else`, or `for` bodies when the body spans
|
||||||
two or more physical lines, even if it is one statement.
|
two or more physical lines, even if it is one statement.
|
||||||
|
- Spell British English identifiers correctly when the feature name uses
|
||||||
|
British English. Use `Behaviour`, not `Behavior`, for new component and file
|
||||||
|
names unless editing an already established American-English API.
|
||||||
- Avoid reformatting unrelated JSX.
|
- Avoid reformatting unrelated JSX.
|
||||||
|
|
||||||
|
### Delimiter decision table
|
||||||
|
|
||||||
|
Use this table before accepting any edited TypeScript or TSX hunk. The table is
|
||||||
|
more authoritative than formatter habit.
|
||||||
|
|
||||||
|
#### Import and export named bindings
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
} from '@/components/ui'
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Button,
|
||||||
|
Card } from '@/components/ui'
|
||||||
|
```
|
||||||
|
|
||||||
|
Also acceptable when short:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Button, Card } from '@/components/ui'
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: named-binding `}` is associative-array-style syntax. It must not be alone
|
||||||
|
at the beginning of a line. Prefer keeping `{` with the first binding and `}`
|
||||||
|
with the final binding when this fits the line limit.
|
||||||
|
|
||||||
|
#### Type literals
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void }
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: type-literal `}` is not a block close. It must stay on the same line as
|
||||||
|
the final property unless that would break the hard line limit.
|
||||||
|
|
||||||
|
#### Object literals
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const value = {
|
||||||
|
open,
|
||||||
|
activeScope,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const value = {
|
||||||
|
open,
|
||||||
|
activeScope }
|
||||||
|
```
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const value = useMemo (() => ({
|
||||||
|
open,
|
||||||
|
activeScope,
|
||||||
|
}), [open, activeScope])
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const value = useMemo (() => ({
|
||||||
|
open,
|
||||||
|
activeScope }), [open, activeScope])
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: object-literal `}` is associative-array-style syntax. It must not be on a
|
||||||
|
line by itself. Keep it with the final property, and keep call `)` off the
|
||||||
|
beginning of a line.
|
||||||
|
|
||||||
|
#### Destructuring parameters
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const Component: FC<Props> = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}) => {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const Component: FC<Props> = (
|
||||||
|
{ open,
|
||||||
|
onOpenChange },
|
||||||
|
) => {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: when a React component or helper takes a destructured object parameter
|
||||||
|
that spans multiple lines, do not use the Prettier-style `= ({ ... }) =>`
|
||||||
|
shape. Put the function parameter list on its own lines. The destructuring `}`
|
||||||
|
stays with the final binding. The parameter-list `)` is then allowed and
|
||||||
|
required at the beginning of its own line.
|
||||||
|
|
||||||
|
#### Inline typed destructuring parameter
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const RouteTransitionWrapper = ({ user, setUser }: {
|
||||||
|
user: User | null
|
||||||
|
setUser: Dispatch<SetStateAction<User | null>> }) => {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const RouteTransitionWrapper = ({ user, setUser }: {
|
||||||
|
user: User | null
|
||||||
|
setUser: Dispatch<SetStateAction<User | null>>
|
||||||
|
}) => {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: this is not a separately split parameter-list block. The line break is
|
||||||
|
inside the inline type. Keep the type-literal `}` and parameter-list `)` on the
|
||||||
|
same line as the final type property.
|
||||||
|
|
||||||
|
#### Multi-line normal parameter list
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const updateDraft = <Key extends keyof Settings,> (
|
||||||
|
key: Key,
|
||||||
|
value: Settings[Key]) => {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const updateDraft = <Key extends keyof Settings,> (
|
||||||
|
key: Key,
|
||||||
|
value: Settings[Key],
|
||||||
|
) => {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: when the parameter list itself is split across multiple parameter lines,
|
||||||
|
the closing parameter `)` goes at the beginning of its own line before `=>` or
|
||||||
|
the return type.
|
||||||
|
|
||||||
|
#### Function and callback blocks
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleSave = () => { save ()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleSave = () => {
|
||||||
|
save () }
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleSave = () => {
|
||||||
|
save ()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: block `}` closes executable code, not associative data. Multi-line
|
||||||
|
function, lambda, callback, `if`, `for`, `switch`, and similar block braces
|
||||||
|
belong on their own line.
|
||||||
|
|
||||||
|
#### Function and method calls
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const value = compute (
|
||||||
|
first,
|
||||||
|
second
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const value = compute (
|
||||||
|
first,
|
||||||
|
second)
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: call-expression `)` must not be at the beginning of a line. The exception
|
||||||
|
for leading `)` applies only to function declaration parameter lists, never to
|
||||||
|
calls.
|
||||||
|
|
||||||
|
#### JSX closing markers
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSave}>
|
||||||
|
保存
|
||||||
|
</Button>)
|
||||||
|
```
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: keep `>` or `/>` with the final prop, and keep JSX closing parentheses in
|
||||||
|
the local compact form such as `</div>)`.
|
||||||
|
|
||||||
|
#### Arrays and tuples
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const items = [
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const items = [
|
||||||
|
first,
|
||||||
|
second]
|
||||||
|
```
|
||||||
|
|
||||||
|
Good when short:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const items = [first, second]
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: array and tuple `]` must not be at the beginning of a line. Keep it with
|
||||||
|
the final element unless that would break the hard line limit.
|
||||||
|
|
||||||
|
#### Single-line braces
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const next = { ...current, enabled: true }
|
||||||
|
const tag = { id, name }
|
||||||
|
```
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Component prop={ value }/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Component prop={value}/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Rule: JavaScript object braces on one line get one inner space. JSX expression
|
||||||
|
braces do not get inner spaces.
|
||||||
|
|
||||||
|
#### Final TSX self-review checklist
|
||||||
|
|
||||||
|
Before reporting completion after a TypeScript or TSX edit, check the edited
|
||||||
|
hunks line by line:
|
||||||
|
|
||||||
|
1. No import/export/type/object/destructuring `}` appears alone at the beginning
|
||||||
|
of a line.
|
||||||
|
2. No call-expression `)` appears at the beginning of a line.
|
||||||
|
3. Any leading `)` is definitely closing a function declaration parameter list,
|
||||||
|
not a call.
|
||||||
|
4. Any parameter-list `)` at the end of a line is valid because the parameter
|
||||||
|
list itself was not split, only an inline type or destructuring shape was.
|
||||||
|
5. No array or tuple `]` appears at the beginning of a line.
|
||||||
|
6. Multi-line executable block `}` is on its own line.
|
||||||
|
7. JSX `>` and `/>` stay with the final prop unless nearby code proves
|
||||||
|
otherwise.
|
||||||
|
8. JSX closing parentheses keep the compact local style.
|
||||||
|
9. Leading indentation is 4-space logical indentation with tabs used only as
|
||||||
|
leading 8-space compression.
|
||||||
|
10. No line has trailing whitespace.
|
||||||
|
|
||||||
## Lint and build constraints
|
## Lint and build constraints
|
||||||
|
|
||||||
- ESLint uses `@eslint/js`, `typescript-eslint`, `eslint-plugin-react-hooks`,
|
- ESLint uses `@eslint/js`, `typescript-eslint`, `eslint-plugin-react-hooks`,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useOverlayStore } from '@/components/RouteBlockerOverlay'
|
|||||||
import { prefetchForURL } from '@/lib/prefetchers'
|
import { prefetchForURL } from '@/lib/prefetchers'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import type { AnchorHTMLAttributes, KeyboardEvent, MouseEvent, TouchEvent } from 'react'
|
import type { AnchorHTMLAttributes, MouseEvent, TouchEvent } from 'react'
|
||||||
import type { To } from 'react-router-dom'
|
import type { To } from 'react-router-dom'
|
||||||
|
|
||||||
type Props = AnchorHTMLAttributes<HTMLAnchorElement> & {
|
type Props = AnchorHTMLAttributes<HTMLAnchorElement> & {
|
||||||
@@ -25,7 +25,6 @@ export default forwardRef<HTMLAnchorElement, Props> (({
|
|||||||
state,
|
state,
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
onTouchStart,
|
onTouchStart,
|
||||||
onKeyDown,
|
|
||||||
onClick,
|
onClick,
|
||||||
cancelOnError = false,
|
cancelOnError = false,
|
||||||
...rest }, ref) => {
|
...rest }, ref) => {
|
||||||
@@ -98,22 +97,11 @@ export default forwardRef<HTMLAnchorElement, Props> (({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (ev: KeyboardEvent<HTMLAnchorElement>) => {
|
|
||||||
onKeyDown?.(ev)
|
|
||||||
|
|
||||||
if (ev.defaultPrevented)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (ev.key === ' ' || ev.key === 'Spacebar')
|
|
||||||
ev.preventDefault ()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a ref={ref}
|
<a ref={ref}
|
||||||
href={typeof to === 'string' ? to : createPath (to)}
|
href={typeof to === 'string' ? to : createPath (to)}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onTouchStart={handleTouchStart}
|
onTouchStart={handleTouchStart}
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={cn ('cursor-pointer', className)}
|
className={cn ('cursor-pointer', className)}
|
||||||
{...rest}/>)
|
{...rest}/>)
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
|
import FormField from '@/components/common/FormField'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { toast } from '@/components/ui/use-toast'
|
||||||
|
import {
|
||||||
|
getClientBehaviorSettings,
|
||||||
|
setClientBehaviorSettings,
|
||||||
|
} from '@/lib/settings'
|
||||||
|
import { inputClass } from '@/lib/utils'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ClientAnimationMode,
|
||||||
|
ClientBehaviorSettings,
|
||||||
|
ClientEmbedAutoLoadMode,
|
||||||
|
ClientThumbnailMode,
|
||||||
|
} from '@/lib/settings'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
sectionClassName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const BehaviorSettingsSection: FC<Props> = ({ sectionClassName }) => {
|
||||||
|
const [savedSettings, setSavedSettings] =
|
||||||
|
useState<ClientBehaviorSettings> (() => getClientBehaviorSettings ())
|
||||||
|
const [draftSettings, setDraftSettings] =
|
||||||
|
useState<ClientBehaviorSettings> (() => getClientBehaviorSettings ())
|
||||||
|
|
||||||
|
useEffect (() => {
|
||||||
|
const current = getClientBehaviorSettings ()
|
||||||
|
setSavedSettings (current)
|
||||||
|
setDraftSettings (current)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const hasUnsavedChanges = useMemo (
|
||||||
|
() => JSON.stringify (draftSettings) !== JSON.stringify (savedSettings),
|
||||||
|
[draftSettings, savedSettings],
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateDraft = <Key extends keyof ClientBehaviorSettings,> (
|
||||||
|
key: Key,
|
||||||
|
value: ClientBehaviorSettings[Key],
|
||||||
|
) => {
|
||||||
|
setDraftSettings (current => ({ ...current, [key]: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const next = setClientBehaviorSettings (draftSettings)
|
||||||
|
setSavedSettings (next)
|
||||||
|
setDraftSettings (next)
|
||||||
|
toast ({ title: '動作設定を保存しました' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDiscard = () => {
|
||||||
|
setDraftSettings (savedSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
この設定は現在のブラウザに保存されます。
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
重い端末や通信量を抑えたい場合に調整します。
|
||||||
|
</p>
|
||||||
|
{hasUnsavedChanges && (
|
||||||
|
<p className="text-sm text-amber-700 dark:text-amber-300">
|
||||||
|
未保存の変更があります
|
||||||
|
</p>)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
disabled={!(hasUnsavedChanges)}
|
||||||
|
onClick={handleDiscard}>
|
||||||
|
変更を破棄
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
disabled={!(hasUnsavedChanges)}
|
||||||
|
onClick={handleSave}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<FormField label="アニメーション">
|
||||||
|
{() => (
|
||||||
|
<select
|
||||||
|
className={inputClass (false)}
|
||||||
|
value={draftSettings.animation ?? 'normal'}
|
||||||
|
onChange={ev => updateDraft (
|
||||||
|
'animation',
|
||||||
|
ev.target.value as ClientAnimationMode,
|
||||||
|
)}>
|
||||||
|
<option value="normal">通常</option>
|
||||||
|
<option value="reduced">控えめ</option>
|
||||||
|
<option value="off">なし</option>
|
||||||
|
</select>)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="埋め込み自動読込">
|
||||||
|
{() => (
|
||||||
|
<select
|
||||||
|
className={inputClass (false)}
|
||||||
|
value={draftSettings.embedAutoLoad ?? 'auto'}
|
||||||
|
onChange={ev => updateDraft (
|
||||||
|
'embedAutoLoad',
|
||||||
|
ev.target.value as ClientEmbedAutoLoadMode,
|
||||||
|
)}>
|
||||||
|
<option value="auto">自動</option>
|
||||||
|
<option value="manual">クリック時</option>
|
||||||
|
<option value="off">読み込まない</option>
|
||||||
|
</select>)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="サムネイル表示">
|
||||||
|
{() => (
|
||||||
|
<select
|
||||||
|
className={inputClass (false)}
|
||||||
|
value={draftSettings.thumbnailMode ?? 'normal'}
|
||||||
|
onChange={ev => updateDraft (
|
||||||
|
'thumbnailMode',
|
||||||
|
ev.target.value as ClientThumbnailMode,
|
||||||
|
)}>
|
||||||
|
<option value="normal">通常</option>
|
||||||
|
<option value="light">軽量</option>
|
||||||
|
<option value="off">非表示</option>
|
||||||
|
</select>)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</section>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BehaviorSettingsSection
|
||||||
@@ -24,14 +24,24 @@ type Props = {
|
|||||||
activeScope: ShortcutScope
|
activeScope: ShortcutScope
|
||||||
effectiveBindings: Record<ShortcutActionId, KeyBinding | null>
|
effectiveBindings: Record<ShortcutActionId, KeyBinding | null>
|
||||||
conflictActionIds: Set<ShortcutActionId>
|
conflictActionIds: Set<ShortcutActionId>
|
||||||
|
availableActionIds: Set<ShortcutActionId>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const ShortcutHelpDialog: FC<Props> = (
|
const ShortcutHelpDialog: FC<Props> = (
|
||||||
{ open, onOpenChange, activeScope, effectiveBindings, conflictActionIds },
|
{
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
activeScope,
|
||||||
|
effectiveBindings,
|
||||||
|
conflictActionIds,
|
||||||
|
availableActionIds,
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
const rows = SHORTCUT_DEFINITIONS.filter (definition =>
|
const rows = SHORTCUT_DEFINITIONS
|
||||||
|
.filter (definition =>
|
||||||
definition.scope === 'global' || definition.scope === activeScope)
|
definition.scope === 'global' || definition.scope === activeScope)
|
||||||
|
.filter (definition => availableActionIds.has (definition.id))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
|||||||
@@ -134,6 +134,21 @@
|
|||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-animation='reduced'] *,
|
||||||
|
:root[data-animation='off'] *
|
||||||
|
{
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-animation='off'] *,
|
||||||
|
:root[data-animation='off'] *::before,
|
||||||
|
:root[data-animation='off'] *::after
|
||||||
|
{
|
||||||
|
animation-duration: 0.001ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.001ms !important;
|
||||||
|
}
|
||||||
|
|
||||||
a
|
a
|
||||||
{
|
{
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type ShortcutActionId =
|
|||||||
| 'settings.account'
|
| 'settings.account'
|
||||||
| 'settings.theme'
|
| 'settings.theme'
|
||||||
| 'settings.keyboard'
|
| 'settings.keyboard'
|
||||||
|
| 'settings.behavior'
|
||||||
|
|
||||||
export type KeyBinding = {
|
export type KeyBinding = {
|
||||||
key: string
|
key: string
|
||||||
@@ -112,6 +113,12 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
|
|||||||
scope: 'settings',
|
scope: 'settings',
|
||||||
defaultBinding: { key: '3' },
|
defaultBinding: { key: '3' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'settings.behavior',
|
||||||
|
label: '設定: 動作',
|
||||||
|
scope: 'settings',
|
||||||
|
defaultBinding: { key: '4' },
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const SHORTCUT_DEFINITIONS_BY_ID =
|
export const SHORTCUT_DEFINITIONS_BY_ID =
|
||||||
@@ -132,6 +139,7 @@ export const SHORTCUT_DEFINITIONS_BY_ID =
|
|||||||
'settings.account': SHORTCUT_DEFINITIONS[8],
|
'settings.account': SHORTCUT_DEFINITIONS[8],
|
||||||
'settings.theme': SHORTCUT_DEFINITIONS[9],
|
'settings.theme': SHORTCUT_DEFINITIONS[9],
|
||||||
'settings.keyboard': SHORTCUT_DEFINITIONS[10],
|
'settings.keyboard': SHORTCUT_DEFINITIONS[10],
|
||||||
|
'settings.behavior': SHORTCUT_DEFINITIONS[11],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -153,6 +161,7 @@ export const DEFAULT_KEY_BINDINGS =
|
|||||||
'settings.account': null,
|
'settings.account': null,
|
||||||
'settings.theme': null,
|
'settings.theme': null,
|
||||||
'settings.keyboard': null,
|
'settings.keyboard': null,
|
||||||
|
'settings.behavior': null,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+135
-1
@@ -51,6 +51,9 @@ export type CustomClientTheme =
|
|||||||
export type TheatreLayoutMode = 'threeColumns' | 'tagsBottom' | 'commentsBottom'
|
export type TheatreLayoutMode = 'threeColumns' | 'tagsBottom' | 'commentsBottom'
|
||||||
export type TheatreTagFlow = 'vertical' | 'horizontal'
|
export type TheatreTagFlow = 'vertical' | 'horizontal'
|
||||||
export type GekanatorBackgroundMotionMode = 'on' | 'calm' | 'off'
|
export type GekanatorBackgroundMotionMode = 'on' | 'calm' | 'off'
|
||||||
|
export type ClientAnimationMode = 'normal' | 'reduced' | 'off'
|
||||||
|
export type ClientEmbedAutoLoadMode = 'auto' | 'manual' | 'off'
|
||||||
|
export type ClientThumbnailMode = 'normal' | 'light' | 'off'
|
||||||
export type ClientPaneBreakpoint = 'desktop' | 'tablet'
|
export type ClientPaneBreakpoint = 'desktop' | 'tablet'
|
||||||
export type ClientListKey = 'postList' | 'postSearch' | 'tagList'
|
export type ClientListKey = 'postList' | 'postSearch' | 'tagList'
|
||||||
export type ClientListLimit = 20 | 50 | 100
|
export type ClientListLimit = 20 | 50 | 100
|
||||||
@@ -75,6 +78,12 @@ export type ClientAppearanceSettings = {
|
|||||||
theme?: UserSettings['theme']
|
theme?: UserSettings['theme']
|
||||||
tagColours?: Partial<ThemeTagColours> }
|
tagColours?: Partial<ThemeTagColours> }
|
||||||
|
|
||||||
|
export type ClientBehaviorSettings = {
|
||||||
|
animation?: ClientAnimationMode
|
||||||
|
embedAutoLoad?: ClientEmbedAutoLoadMode
|
||||||
|
thumbnailMode?: ClientThumbnailMode
|
||||||
|
}
|
||||||
|
|
||||||
// DB-backed settings. At present only `theme` is surfaced in `/users/settings`.
|
// DB-backed settings. At present only `theme` is surfaced in `/users/settings`.
|
||||||
// The remaining fields are retained for existing backend work and are not wired
|
// The remaining fields are retained for existing backend work and are not wired
|
||||||
// to the common settings UI yet.
|
// to the common settings UI yet.
|
||||||
@@ -87,6 +96,7 @@ export const DEFAULT_USER_SETTINGS: UserSettings = {
|
|||||||
// Browser-local settings only. These are device or screen specific.
|
// Browser-local settings only. These are device or screen specific.
|
||||||
export type ClientSettings = {
|
export type ClientSettings = {
|
||||||
appearance?: ClientAppearanceSettings
|
appearance?: ClientAppearanceSettings
|
||||||
|
behavior?: ClientBehaviorSettings
|
||||||
keyboard?: ClientKeyboardSettings
|
keyboard?: ClientKeyboardSettings
|
||||||
panes?: Record<string, ClientPaneSettings>
|
panes?: Record<string, ClientPaneSettings>
|
||||||
lists?: Partial<Record<ClientListKey, ClientListSettings>>
|
lists?: Partial<Record<ClientListKey, ClientListSettings>>
|
||||||
@@ -303,6 +313,25 @@ const validListLimit = (value: unknown): ClientListLimit | null =>
|
|||||||
value === 20 || value === 50 || value === 100 ? value : null
|
value === 20 || value === 50 || value === 100 ? value : null
|
||||||
|
|
||||||
|
|
||||||
|
const normaliseAnimationMode = (value: unknown): ClientAnimationMode | null => {
|
||||||
|
if (value === 'normal' || value === 'reduced' || value === 'off')
|
||||||
|
return value
|
||||||
|
if (value === 'true')
|
||||||
|
return 'reduced'
|
||||||
|
if (value === 'false')
|
||||||
|
return 'normal'
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const normaliseEmbedAutoLoadMode = (value: unknown): ClientEmbedAutoLoadMode | null =>
|
||||||
|
value === 'auto' || value === 'manual' || value === 'off' ? value : null
|
||||||
|
|
||||||
|
|
||||||
|
const normaliseThumbnailMode = (value: unknown): ClientThumbnailMode | null =>
|
||||||
|
value === 'normal' || value === 'light' || value === 'off' ? value : null
|
||||||
|
|
||||||
|
|
||||||
const normaliseKeyboardBindings = (
|
const normaliseKeyboardBindings = (
|
||||||
bindings: unknown,
|
bindings: unknown,
|
||||||
): Partial<Record<ShortcutActionId, KeyBinding | null>> => {
|
): Partial<Record<ShortcutActionId, KeyBinding | null>> => {
|
||||||
@@ -341,6 +370,110 @@ export const getClientKeyboardSettings = (): ClientKeyboardSettings => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const legacyAnimationMode = (): ClientAnimationMode | null =>
|
||||||
|
normaliseAnimationMode (loadClientSettings ().reducedMotion)
|
||||||
|
|
||||||
|
|
||||||
|
const legacyEmbedAutoLoadMode = (): ClientEmbedAutoLoadMode | null =>
|
||||||
|
normaliseEmbedAutoLoadMode (loadClientSettings ().embedAutoLoad)
|
||||||
|
|
||||||
|
|
||||||
|
const legacyThumbnailMode = (): ClientThumbnailMode | null =>
|
||||||
|
normaliseThumbnailMode (loadClientSettings ().thumbnailMode)
|
||||||
|
|
||||||
|
|
||||||
|
export const getClientAnimationMode = (): ClientAnimationMode =>
|
||||||
|
loadClientSettings ().behavior?.animation
|
||||||
|
?? legacyAnimationMode ()
|
||||||
|
?? 'normal'
|
||||||
|
|
||||||
|
|
||||||
|
export const applyClientAnimationMode = (mode: ClientAnimationMode): void => {
|
||||||
|
if (typeof document === 'undefined')
|
||||||
|
return
|
||||||
|
|
||||||
|
if (mode === 'normal')
|
||||||
|
{
|
||||||
|
delete document.documentElement.dataset.animation
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
document.documentElement.dataset.animation = mode
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const setClientAnimationMode = (
|
||||||
|
animation: ClientAnimationMode,
|
||||||
|
): ClientBehaviorSettings =>
|
||||||
|
setClientBehaviorSettings ({
|
||||||
|
...getClientBehaviorSettings (),
|
||||||
|
animation,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getClientEmbedAutoLoadMode = (): ClientEmbedAutoLoadMode =>
|
||||||
|
loadClientSettings ().behavior?.embedAutoLoad
|
||||||
|
?? legacyEmbedAutoLoadMode ()
|
||||||
|
?? 'auto'
|
||||||
|
|
||||||
|
|
||||||
|
export const setClientEmbedAutoLoadMode = (
|
||||||
|
embedAutoLoad: ClientEmbedAutoLoadMode,
|
||||||
|
): ClientBehaviorSettings =>
|
||||||
|
setClientBehaviorSettings ({
|
||||||
|
...getClientBehaviorSettings (),
|
||||||
|
embedAutoLoad,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const getClientThumbnailMode = (): ClientThumbnailMode =>
|
||||||
|
loadClientSettings ().behavior?.thumbnailMode
|
||||||
|
?? legacyThumbnailMode ()
|
||||||
|
?? 'normal'
|
||||||
|
|
||||||
|
|
||||||
|
export const setClientThumbnailMode = (
|
||||||
|
thumbnailMode: ClientThumbnailMode,
|
||||||
|
): ClientBehaviorSettings =>
|
||||||
|
setClientBehaviorSettings ({
|
||||||
|
...getClientBehaviorSettings (),
|
||||||
|
thumbnailMode,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const getClientBehaviorSettings = (): ClientBehaviorSettings => ({
|
||||||
|
animation: getClientAnimationMode (),
|
||||||
|
embedAutoLoad: getClientEmbedAutoLoadMode (),
|
||||||
|
thumbnailMode: getClientThumbnailMode (),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const setClientBehaviorSettings = (
|
||||||
|
behavior: ClientBehaviorSettings,
|
||||||
|
): ClientBehaviorSettings => {
|
||||||
|
const next: ClientBehaviorSettings = {
|
||||||
|
animation: (
|
||||||
|
normaliseAnimationMode (behavior.animation)
|
||||||
|
?? getClientAnimationMode ()
|
||||||
|
),
|
||||||
|
embedAutoLoad: (
|
||||||
|
normaliseEmbedAutoLoadMode (behavior.embedAutoLoad)
|
||||||
|
?? getClientEmbedAutoLoadMode ()
|
||||||
|
),
|
||||||
|
thumbnailMode: (
|
||||||
|
normaliseThumbnailMode (behavior.thumbnailMode)
|
||||||
|
?? getClientThumbnailMode ()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClientSettings (settings => ({
|
||||||
|
...settings,
|
||||||
|
behavior: next,
|
||||||
|
}))
|
||||||
|
applyClientAnimationMode (next.animation ?? 'normal')
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const setClientKeyboardSettings = (
|
export const setClientKeyboardSettings = (
|
||||||
keyboard: ClientKeyboardSettings,
|
keyboard: ClientKeyboardSettings,
|
||||||
): ClientKeyboardSettings => {
|
): ClientKeyboardSettings => {
|
||||||
@@ -921,7 +1054,8 @@ export const setClientGekanatorBackgroundMotion = (
|
|||||||
): void => {
|
): void => {
|
||||||
updateClientSettings (settings => ({
|
updateClientSettings (settings => ({
|
||||||
...settings,
|
...settings,
|
||||||
gekanator: { ...(settings.gekanator ?? { }), backgroundMotion } }))
|
gekanator: { ...(settings.gekanator ?? { }), backgroundMotion },
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
getClientKeyboardSettings,
|
getClientKeyboardSettings,
|
||||||
getEffectiveKeyBindings,
|
getEffectiveKeyBindings,
|
||||||
resetAllClientKeyBindings,
|
|
||||||
resetClientKeyBinding,
|
|
||||||
setClientKeyBinding,
|
|
||||||
setClientKeyboardSettings,
|
setClientKeyboardSettings,
|
||||||
} from '@/lib/settings'
|
} from '@/lib/settings'
|
||||||
|
|
||||||
@@ -46,10 +43,6 @@ type KeyboardShortcutsContextValue = {
|
|||||||
effectiveBindings: Record<ShortcutActionId, KeyBinding | null>
|
effectiveBindings: Record<ShortcutActionId, KeyBinding | null>
|
||||||
conflictActionIds: Set<ShortcutActionId>
|
conflictActionIds: Set<ShortcutActionId>
|
||||||
saveKeyboardSettings: (settings: ClientKeyboardSettings) => void
|
saveKeyboardSettings: (settings: ClientKeyboardSettings) => void
|
||||||
setEnabled: (enabled: boolean) => void
|
|
||||||
setBinding: (actionId: ShortcutActionId, binding: KeyBinding | null) => void
|
|
||||||
resetBinding: (actionId: ShortcutActionId) => void
|
|
||||||
resetAllBindings: () => void
|
|
||||||
registerHandlers: (handlers: ShortcutHandlers) => () => void
|
registerHandlers: (handlers: ShortcutHandlers) => () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,37 +124,11 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
|||||||
[registeredHandlers],
|
[registeredHandlers],
|
||||||
)
|
)
|
||||||
|
|
||||||
const setEnabled = useCallback ((enabled: boolean) => {
|
|
||||||
const next = setClientKeyboardSettings ({
|
|
||||||
...keyboardSettings,
|
|
||||||
enabled,
|
|
||||||
})
|
|
||||||
setKeyboardSettingsState (next)
|
|
||||||
}, [keyboardSettings])
|
|
||||||
|
|
||||||
const saveKeyboardSettings = useCallback ((settings: ClientKeyboardSettings) => {
|
const saveKeyboardSettings = useCallback ((settings: ClientKeyboardSettings) => {
|
||||||
const next = setClientKeyboardSettings (settings)
|
const next = setClientKeyboardSettings (settings)
|
||||||
setKeyboardSettingsState (next)
|
setKeyboardSettingsState (next)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const setBinding = useCallback ((
|
|
||||||
actionId: ShortcutActionId,
|
|
||||||
binding: KeyBinding | null,
|
|
||||||
) => {
|
|
||||||
const next = setClientKeyBinding (actionId, binding)
|
|
||||||
setKeyboardSettingsState (next)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const resetBinding = useCallback ((actionId: ShortcutActionId) => {
|
|
||||||
const next = resetClientKeyBinding (actionId)
|
|
||||||
setKeyboardSettingsState (next)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const resetAllBindings = useCallback (() => {
|
|
||||||
const next = resetAllClientKeyBindings ()
|
|
||||||
setKeyboardSettingsState (next)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const registerHandlers = useCallback ((handlers: ShortcutHandlers) => {
|
const registerHandlers = useCallback ((handlers: ShortcutHandlers) => {
|
||||||
const registrationId = ++registrationIdRef.current
|
const registrationId = ++registrationIdRef.current
|
||||||
|
|
||||||
@@ -179,6 +146,39 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const builtinHandlers = useMemo<ShortcutHandlers> (
|
||||||
|
() => ({
|
||||||
|
'global.openShortcutHelp': () => {
|
||||||
|
setShortcutHelpOpen (true)
|
||||||
|
},
|
||||||
|
'global.focusSearch': () => {
|
||||||
|
focusSearchTarget ()
|
||||||
|
},
|
||||||
|
'navigation.posts': () => navigate ('/posts'),
|
||||||
|
'navigation.tags': () => navigate ('/tags'),
|
||||||
|
'navigation.materials': () => navigate ('/materials'),
|
||||||
|
'navigation.wiki': () => navigate ('/wiki'),
|
||||||
|
'settings.account': () => navigate ('/users/settings?tab=account'),
|
||||||
|
'settings.theme': () => navigate ('/users/settings?tab=theme'),
|
||||||
|
'settings.keyboard': () => navigate ('/users/settings?tab=keyboard'),
|
||||||
|
'settings.behavior': () => navigate ('/users/settings?tab=behavior'),
|
||||||
|
}),
|
||||||
|
[navigate],
|
||||||
|
)
|
||||||
|
|
||||||
|
const availableActionIds = useMemo<Set<ShortcutActionId>> (
|
||||||
|
() => new Set (
|
||||||
|
SHORTCUT_DEFINITIONS
|
||||||
|
.filter (definition =>
|
||||||
|
definition.scope === 'global' || definition.scope === activeScope)
|
||||||
|
.filter (definition =>
|
||||||
|
builtinHandlers[definition.id] != null
|
||||||
|
|| mergedRegisteredHandlers[definition.id] != null)
|
||||||
|
.map (definition => definition.id),
|
||||||
|
),
|
||||||
|
[activeScope, builtinHandlers, mergedRegisteredHandlers],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (
|
if (
|
||||||
@@ -219,24 +219,7 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
|||||||
return
|
return
|
||||||
|
|
||||||
const actionId = candidates[0].id
|
const actionId = candidates[0].id
|
||||||
const builtinHandler = (
|
const handler = builtinHandlers[actionId] ?? mergedRegisteredHandlers[actionId]
|
||||||
{
|
|
||||||
'global.openShortcutHelp': () => {
|
|
||||||
setShortcutHelpOpen (true)
|
|
||||||
},
|
|
||||||
'global.focusSearch': () => {
|
|
||||||
focusSearchTarget ()
|
|
||||||
},
|
|
||||||
'navigation.posts': () => navigate ('/posts'),
|
|
||||||
'navigation.tags': () => navigate ('/tags'),
|
|
||||||
'navigation.materials': () => navigate ('/materials'),
|
|
||||||
'navigation.wiki': () => navigate ('/wiki'),
|
|
||||||
'settings.account': () => navigate ('/users/settings?tab=account'),
|
|
||||||
'settings.theme': () => navigate ('/users/settings?tab=theme'),
|
|
||||||
'settings.keyboard': () => navigate ('/users/settings?tab=keyboard'),
|
|
||||||
} as ShortcutHandlers
|
|
||||||
)[actionId]
|
|
||||||
const handler = builtinHandler ?? mergedRegisteredHandlers[actionId]
|
|
||||||
|
|
||||||
if (handler == null)
|
if (handler == null)
|
||||||
return
|
return
|
||||||
@@ -251,9 +234,9 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
|||||||
activeScope,
|
activeScope,
|
||||||
conflictActionIds,
|
conflictActionIds,
|
||||||
effectiveBindings,
|
effectiveBindings,
|
||||||
|
builtinHandlers,
|
||||||
keyboardSettings.enabled,
|
keyboardSettings.enabled,
|
||||||
mergedRegisteredHandlers,
|
mergedRegisteredHandlers,
|
||||||
navigate,
|
|
||||||
shortcutHelpOpen,
|
shortcutHelpOpen,
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -262,11 +245,7 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
|||||||
keyboardSettings,
|
keyboardSettings,
|
||||||
effectiveBindings,
|
effectiveBindings,
|
||||||
conflictActionIds,
|
conflictActionIds,
|
||||||
setEnabled,
|
|
||||||
saveKeyboardSettings,
|
saveKeyboardSettings,
|
||||||
setBinding,
|
|
||||||
resetBinding,
|
|
||||||
resetAllBindings,
|
|
||||||
registerHandlers,
|
registerHandlers,
|
||||||
}), [
|
}), [
|
||||||
activeScope,
|
activeScope,
|
||||||
@@ -274,11 +253,7 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
|||||||
effectiveBindings,
|
effectiveBindings,
|
||||||
keyboardSettings,
|
keyboardSettings,
|
||||||
registerHandlers,
|
registerHandlers,
|
||||||
resetAllBindings,
|
|
||||||
resetBinding,
|
|
||||||
saveKeyboardSettings,
|
saveKeyboardSettings,
|
||||||
setBinding,
|
|
||||||
setEnabled,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -289,7 +264,8 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
|
|||||||
onOpenChange={setShortcutHelpOpen}
|
onOpenChange={setShortcutHelpOpen}
|
||||||
activeScope={activeScope}
|
activeScope={activeScope}
|
||||||
effectiveBindings={effectiveBindings}
|
effectiveBindings={effectiveBindings}
|
||||||
conflictActionIds={conflictActionIds}/>
|
conflictActionIds={conflictActionIds}
|
||||||
|
availableActionIds={availableActionIds}/>
|
||||||
</KeyboardShortcutsContext.Provider>)
|
</KeyboardShortcutsContext.Provider>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { HelmetProvider } from 'react-helmet-async'
|
|||||||
|
|
||||||
import '@/index.css'
|
import '@/index.css'
|
||||||
import App from '@/App'
|
import App from '@/App'
|
||||||
|
import { applyClientAnimationMode, getClientAnimationMode } from '@/lib/settings'
|
||||||
|
|
||||||
const helmetContext = { }
|
const helmetContext = { }
|
||||||
|
|
||||||
@@ -13,6 +14,8 @@ const client = new QueryClient ({
|
|||||||
gcTime: 30 * 60 * 1000,
|
gcTime: 30 * 60 * 1000,
|
||||||
retry: 1 } } })
|
retry: 1 } } })
|
||||||
|
|
||||||
|
applyClientAnimationMode (getClientAnimationMode ())
|
||||||
|
|
||||||
createRoot (document.getElementById ('root')!).render (
|
createRoot (document.getElementById ('root')!).render (
|
||||||
<HelmetProvider context={helmetContext}>
|
<HelmetProvider context={helmetContext}>
|
||||||
<QueryClientProvider client={client}>
|
<QueryClientProvider client={client}>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Label from '@/components/common/Label'
|
|||||||
import PageTitle from '@/components/common/PageTitle'
|
import PageTitle from '@/components/common/PageTitle'
|
||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
import TagLink from '@/components/TagLink'
|
import TagLink from '@/components/TagLink'
|
||||||
|
import BehaviorSettingsSection from '@/components/users/BehaviorSettingsSection'
|
||||||
import KeyboardSettingsSection from '@/components/users/KeyboardSettingsSection'
|
import KeyboardSettingsSection from '@/components/users/KeyboardSettingsSection'
|
||||||
import InheritDialogue from '@/components/users/InheritDialogue'
|
import InheritDialogue from '@/components/users/InheritDialogue'
|
||||||
import UserCodeDialogue from '@/components/users/UserCodeDialogue'
|
import UserCodeDialogue from '@/components/users/UserCodeDialogue'
|
||||||
@@ -48,7 +49,7 @@ type Props = {
|
|||||||
|
|
||||||
type UserFormField = 'name'
|
type UserFormField = 'name'
|
||||||
type SettingsFormField = 'theme'
|
type SettingsFormField = 'theme'
|
||||||
type SettingsTab = 'account' | 'theme' | 'keyboard'
|
type SettingsTab = 'account' | 'theme' | 'keyboard' | 'behavior'
|
||||||
|
|
||||||
type TabSpec = {
|
type TabSpec = {
|
||||||
id: SettingsTab
|
id: SettingsTab
|
||||||
@@ -79,7 +80,8 @@ type SharedSectionProps = {
|
|||||||
const tabs: TabSpec[] = [
|
const tabs: TabSpec[] = [
|
||||||
{ id: 'account', label: 'アカウント' },
|
{ id: 'account', label: 'アカウント' },
|
||||||
{ id: 'theme', label: 'テーマ' },
|
{ id: 'theme', label: 'テーマ' },
|
||||||
{ id: 'keyboard', label: 'キーボード' } ]
|
{ id: 'keyboard', label: 'キーボード' },
|
||||||
|
{ id: 'behavior', label: '動作' } ]
|
||||||
|
|
||||||
const sectionClassName =
|
const sectionClassName =
|
||||||
'space-y-4 rounded-xl border border-border bg-background/80 p-4'
|
'space-y-4 rounded-xl border border-border bg-background/80 p-4'
|
||||||
@@ -379,7 +381,8 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
() => (
|
() => (
|
||||||
{ account: Boolean (userFieldErrors.name?.length),
|
{ account: Boolean (userFieldErrors.name?.length),
|
||||||
theme: Boolean (settingsFieldErrors.theme?.length),
|
theme: Boolean (settingsFieldErrors.theme?.length),
|
||||||
keyboard: conflictActionIds.size > 0 }),
|
keyboard: conflictActionIds.size > 0,
|
||||||
|
behavior: false }),
|
||||||
[conflictActionIds.size, settingsFieldErrors.theme, userFieldErrors.name])
|
[conflictActionIds.size, settingsFieldErrors.theme, userFieldErrors.name])
|
||||||
|
|
||||||
const applyDraftThemeSelection = (
|
const applyDraftThemeSelection = (
|
||||||
@@ -689,6 +692,8 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
{sectionProps && activeTab === 'theme' && <ThemeSection {...sectionProps}/>}
|
{sectionProps && activeTab === 'theme' && <ThemeSection {...sectionProps}/>}
|
||||||
{activeTab === 'keyboard' && (
|
{activeTab === 'keyboard' && (
|
||||||
<KeyboardSettingsSection sectionClassName={sectionClassName}/>)}
|
<KeyboardSettingsSection sectionClassName={sectionClassName}/>)}
|
||||||
|
{activeTab === 'behavior' && (
|
||||||
|
<BehaviorSettingsSection sectionClassName={sectionClassName}/>)}
|
||||||
</div>)}
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -736,6 +741,8 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
{activeTab === 'theme' && <ThemeSection {...sectionProps}/>}
|
{activeTab === 'theme' && <ThemeSection {...sectionProps}/>}
|
||||||
{activeTab === 'keyboard' && (
|
{activeTab === 'keyboard' && (
|
||||||
<KeyboardSettingsSection sectionClassName={sectionClassName}/>)}
|
<KeyboardSettingsSection sectionClassName={sectionClassName}/>)}
|
||||||
|
{activeTab === 'behavior' && (
|
||||||
|
<BehaviorSettingsSection sectionClassName={sectionClassName}/>)}
|
||||||
</>)}
|
</>)}
|
||||||
</div>)}
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする