このコミットが含まれているのは:
2026-10-28 21:13:08 +09:00
コミット 49721e6915
17個のファイルの変更、170行の追加、48行の削除
+1 -1
ファイルの表示
@@ -21,7 +21,7 @@
## Mandatory Counts data contract
- Counts のロカール非依存・内表行集合保持はルート AGENTS.md の必須契約に従う。
内表行の表示フィルターを作らない。言語名列はサーバーの Locale 定義を使い、
内表行の表示フィルターを作らない。名称・別名列はサーバーの Language コードを使い、
未登録列に別言語名を代入しない。列を隠しても取得済みの全言語データは保持する。
ソート・コピー・JSON の投影には安定したキーと値を使い、UI 訳語を識別に使わない。
+9 -3
ファイルの表示
@@ -4,6 +4,7 @@ import { SortableContext, arrayMove, horizontalListSortingStrategy,
sortableKeyboardCoordinates } from '@dnd-kit/sortable'
import { useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { CSS } from '@dnd-kit/utilities'
import { useClientMotion } from '@/lib/useClientMotion'
@@ -55,11 +56,16 @@ const SortableColumns = ({ ids, fixed = [], name, onChange, children }: {
<SortableContext items={ids} strategy={horizontalListSortingStrategy}>
{children (ids)}
</SortableContext>
{typeof document !== 'undefined' && createPortal (<DragOverlay
{typeof document !== 'undefined' && createPortal (<DragOverlay adjustScale={false}
style={{ width: 'max-content', height: 'auto', maxWidth: 'calc(100vw - 2rem)' }}
dropAnimation={motion.reduced ? null : {
duration: motion.duration ('drop'), easing: motion.easing }}>
duration: motion.duration ('drop'), easing: motion.easing,
keyframes: ({ transform }) => [
{ transform: CSS.Translate.toString (transform.initial) },
{ transform: CSS.Translate.toString (transform.final) }] }}>
{active != null && <div lang={name (active).language}
className="rounded border border-blue-500 bg-white p-2 text-slate-900 shadow-lg
className="w-max max-w-[min(20rem,calc(100vw_-_2rem))] truncate rounded
border border-blue-500 bg-white px-2 py-1 text-sm text-slate-900 shadow-lg
dark:border-blue-400 dark:bg-slate-800 dark:text-slate-100">{name (active).text}</div>}
</DragOverlay>, document.body)}
</DndContext>
+1 -1
ファイルの表示
@@ -40,7 +40,7 @@ const SortableItem = ({ id, name, fixed, children, moveGroup, handleId, renderSh
}
else item.listeners?.onKeyDown?.(event)
}}><span aria-hidden>⠿</span></button>
const style = { transform: CSS.Transform.toString (item.transform), transition: item.transition }
const style = { transform: CSS.Translate.toString (item.transform), transition: item.transition }
if (renderShell != null)
return renderShell ({ ref: item.setNodeRef, style, handle, dragging: item.isDragging })
return <div ref={item.setNodeRef} style={style}
+19 -8
ファイルの表示
@@ -4,6 +4,7 @@ import { SortableContext, arrayMove, rectSortingStrategy,
sortableKeyboardCoordinates } from '@dnd-kit/sortable'
import { useId, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { CSS } from '@dnd-kit/utilities'
import SortableItem from '@/components/common/SortableItem'
import { useClientMotion } from '@/lib/useClientMotion'
@@ -29,8 +30,10 @@ const Group = ({ id, children }: { id: string; children: ReactNode }) => {
}
const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed = [] }: {
const SortableList = (
{ groups, name, handleName, renderItem, renderGroup, onChange, fixed = [] }: {
groups: Groups; name: (id: string) => Name
handleName?: (id: string) => Name
renderItem: (id: string, group: string) => ReactNode
renderGroup: (id: string, content: ReactNode) => ReactNode
onChange: (groups: Groups) => void; fixed?: string[] },
@@ -39,6 +42,7 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
const scope = useId ()
const [active, setActive] = useState<string | null> (null)
const motion = useClientMotion ()
const accessibleName = handleName ?? name
const sensors = useSensors (
useSensor (MouseSensor, { activationConstraint: { distance: 4 } }),
useSensor (TouchSensor, { activationConstraint: { delay: 350, tolerance: 8 } }),
@@ -84,13 +88,14 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
}
return <DndContext sensors={sensors} collisionDetection={closestCenter}
accessibility={{ announcements: {
onDragStart: event => name (String (event.active.id)).text,
onDragStart: event => accessibleName (String (event.active.id)).text,
onDragOver: event => event.over == null ? undefined : (
event.over.id.toString ().startsWith ('group:') ? undefined
: name (String (event.over.id)).text),
: accessibleName (String (event.over.id)).text),
onDragMove: () => undefined, onDragEnd: () => undefined,
onDragCancel: () => undefined },
screenReaderInstructions: { draggable: active == null ? '' : name (active).text } }}
screenReaderInstructions: {
draggable: active == null ? '' : accessibleName (active).text } }}
onDragStart={event => {
const id = String (event.active.id)
drag.current = { id, groups }
@@ -108,17 +113,23 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
{Object.entries (groups).map (([group, ids]) => <div key={group}>
{renderGroup (group, <SortableContext items={ids} strategy={rectSortingStrategy}>
<Group id={group}>{ids.map (id => <SortableItem key={id} id={id}
name={name (id)} fixed={fixed.includes (id)} handleId={`${scope}-${id}`}
name={accessibleName (id)} fixed={fixed.includes (id)} handleId={`${scope}-${id}`}
moveGroup={direction => moveGroup (id, direction)}>
{renderItem (id, group)}
</SortableItem>)}</Group>
</SortableContext>)}
</div>)}
{typeof document !== 'undefined' && createPortal (
<DragOverlay dropAnimation={motion.reduced ? null : {
duration: motion.duration ('drop'), easing: motion.easing }}>
<DragOverlay adjustScale={false}
style={{ width: 'max-content', height: 'auto', maxWidth: 'calc(100vw - 2rem)' }}
dropAnimation={motion.reduced ? null : {
duration: motion.duration ('drop'), easing: motion.easing,
keyframes: ({ transform }) => [
{ transform: CSS.Translate.toString (transform.initial) },
{ transform: CSS.Translate.toString (transform.final) }] }}>
{active == null ? null : <div lang={name (active).language}
className="rounded border border-blue-500 bg-white px-3 py-2 text-sm
className="w-max max-w-[min(20rem,calc(100vw_-_2rem))] truncate rounded
border border-blue-500 bg-white px-2 py-1 text-sm
text-slate-900 shadow-lg dark:border-blue-400 dark:bg-slate-800 dark:text-slate-100">
{name (active).text}</div>}
</DragOverlay>, document.body)}
+3 -2
ファイルの表示
@@ -48,7 +48,8 @@ const ConditionEditor = (
const operator = fields.find (item => item.key === field)?.operators[0]
if (operator != null) setCondition ({ field, operator })
}}>
{fields.filter (field => field.operators.length > 0).map (field => (
{fields.filter (item => item.operators.length > 0
&& (!item.legacy || item.key === field)).map (field => (
<option key={field.key} value={field.key}>{fieldName (field.key)}</option>))}
</select>
<select value={operator} aria-label={message ('operator').text}
@@ -98,7 +99,7 @@ const CountsAdditionalConditions = ({ params, update }: {
const dialogue = useDialogue ()
const conditions = readCountState<CountsCondition[]> (params, 'conditions', [])
const metadata = definition.filterFields ?? definition.fields
const fields = metadata.filter (field => field.operators.length > 0)
const fields = metadata.filter (field => field.operators.length > 0 && !field.legacy)
const fieldName = (key: string) => (metadata.find (item => item.key === key)?.labels
?? [countsFieldLabel (key)]).map (label => message (
label, metadata.find (item => item.key === key)?.labelValues).text).join (' › ')
+3 -1
ファイルの表示
@@ -69,7 +69,9 @@ const FieldGroup = ({ fields, path, params, enabled, selected, select, update }:
const prefix = path ? `${path}.` : ''
return <SortableList groups={{ items: ordered.map (field => field.key) }}
fixed={path === '' ? ['thumbnail'] : []}
name={key => message ('reorder', {
name={key => message (fields.find (field => field.key === key)!.label,
fields.find (field => field.key === key)!.labelValues)}
handleName={key => message ('reorder', {
column: message (fields.find (field => field.key === key)!.label,
fields.find (field => field.key === key)!.labelValues).text })}
renderGroup={(_, content) => content}
+23 -5
ファイルの表示
@@ -1,6 +1,6 @@
import { countsFieldLabel } from '@/lib/countsDefinitions'
import { apiDownload, apiGet } from '@/lib/api'
import { currentViewingLocale } from '@/lib/locales'
import { currentViewingLocale, localeLanguage } from '@/lib/locales'
import { readCountState, encodeCountState } from '@/lib/queryState'
@@ -99,7 +99,7 @@ export type CountSort = { key: string; direction: 'asc' | 'desc' }
export type CountField = { key: string; label: string; children?: CountField[]
labelValues?: Record<string, string>
multipleValues?: boolean
nameLocale?: { code: string; languageCode: string; scriptCode: string } }
nameLocale?: { code: string; languageCode: string; scriptCode?: string } }
export type CountRecord = Record<string, unknown>
export const countColumns: Record<string, string> = {
@@ -233,7 +233,8 @@ export const selectedCountColumns = (
const defaults: Record<CountsResource, string[]> = {
posts: ['title', 'url', 'tags', 'parents', 'video_ms'],
tags: [...(definition?.nameColumns ?? []).filter (field =>
field.nameLocale?.code === currentViewingLocale ()).map (field => field.key),
field.nameLocale?.languageCode === localeLanguage (currentViewingLocale ()))
.map (field => field.key),
'category', 'parents'],
nico_tags: ['name', 'linked_tags'],
tag_descriptions: ['language_code', 'body', 'source_language_code'],
@@ -356,14 +357,31 @@ export const normaliseCountsParams = (
definition?: CountsDefinition,
): URLSearchParams => {
const params = new URLSearchParams (source)
const nameScope = params.get ('name_scope')
if (definition?.conditionAliases != null && (nameScope == null || nameScope === 'locale'))
{
const predicates = new Map ((definition.conditionAliases ?? [])
.map (entry => [entry.key, entry.value]))
for (const [key, value] of params)
if (/^conditions\.\d+\.field$/.test (key)) params.set (key, value.replace (
/(?:name|aliases)_[a-z0-9_]+/g, field => predicates.get (field) ?? field))
params.set ('name_scope', 'language')
}
const aliasesByKey = new Map ((definition?.columnAliases ?? [])
.map (entry => [entry.key, entry.value]))
for (const key of ['columns', 'children', 'order', 'sort', 'child_sort'])
if (params.has (key)) params.set (key, params.get (key)!.replace (
/(?:name|aliases)_[a-z0-9_]+/g, field => aliasesByKey.get (field) ?? field))
if (definition?.nameColumns != null)
{
const names = definition.nameColumns.filter (field => !field.multipleValues)
.map (field => field.key)
const current = definition.nameColumns.find (field =>
!field.multipleValues && field.nameLocale?.code === currentViewingLocale ())?.key
!field.multipleValues
&& field.nameLocale?.languageCode === localeLanguage (currentViewingLocale ()))?.key
const aliases = definition.nameColumns.find (field =>
field.multipleValues && field.nameLocale?.code === currentViewingLocale ())?.key
field.multipleValues
&& field.nameLocale?.languageCode === localeLanguage (currentViewingLocale ()))?.key
const rootKey = (key: string) => (key === 'name' && current != null) ? current
: ((key === 'aliases' && aliases != null) ? aliases : key)
const childKeys = (key: string): string[] => {
+5 -5
ファイルの表示
@@ -27,11 +27,14 @@ export type CountsFieldMetadata = {
setType?: 'tag_reference'; inputTypes?: string[]
labelValues?: Record<string, string>
nameLocale?: CountField['nameLocale']
legacy?: boolean
choices?: { value: string; label: string }[] }
export type CountsDefinition = {
fields: CountsFieldMetadata[]
outputFields?: CountField[]
nameColumns?: CountField[]
columnAliases?: { key: string; value: string }[]
conditionAliases?: { key: string; value: string }[]
filterFields?: CountsFieldMetadata[]
defaults?: { definition: string; mode: string; params: { key: string; value: string }[] }[]
axes: { latest: string[]; history: string[] }
@@ -53,12 +56,9 @@ const localiseCountsDefinition = (
const heading = (name: CountField['nameLocale'], aliasesFlg: boolean) => {
if (name == null) return null
const language = languageUIMessage (name.languageCode, locale).text
const scripts = new Set ((definition.nameColumns ?? [])
.filter (field => field.nameLocale?.languageCode === name.languageCode)
.map (field => field.nameLocale!.scriptCode.toLowerCase ()))
const label = aliasesFlg ? 'counts:localeAliases' : 'counts:localeName'
return { label: scripts.size > 1 ? `${label}WithScript` : label,
labelValues: { language, script: name.scriptCode } }
return { label: name.scriptCode == null ? label : `${label}WithScript`,
labelValues: { language, script: name.scriptCode ?? '' } }
}
const columns = (fields: CountField[]): CountField[] => fields.map (field => ({
...field, ...heading (field.nameLocale, field.multipleValues === true),