Merge branch 'feat/catalog-scope-color-rows' into 'main'
feat(catalog): scope-colored row stripes + filter pills + chips See merge request 2-6/2-6-4/terravault/ordinis!80
This commit is contained in:
@@ -1,99 +1,63 @@
|
|||||||
/**
|
/**
|
||||||
* Hub view (View B из design handoff) — focused dictionary в центре + соседи
|
* DictionaryHubView — Связи tab per redesign/screens/04-editor-links.png.
|
||||||
* по обе стороны.
|
|
||||||
*
|
*
|
||||||
* Layout:
|
* <p>2-column split:
|
||||||
* ┌──────────────┬─────────────────┬──────────────┐
|
* <ul>
|
||||||
* │ → ссылается │ <current> │ ← N использ. │
|
* <li><b>→ ССЫЛАЕТСЯ НА (N)</b>: outgoing FK — каждая row показывает
|
||||||
* │ (outgoing) │ (focused) │ (refBy) │
|
* target_dict (link) + record_count + required/optional chip + source_field
|
||||||
* └──────────────┴─────────────────┴──────────────┘
|
* (mono right).</li>
|
||||||
|
* <li><b>← ССЫЛАЮТСЯ НА НАС (M)</b>: incoming FK — source_dict (link) +
|
||||||
|
* activeRecordsInSourceDict + onClose policy chip ("критично" для BLOCK).
|
||||||
|
* Footer warning если total dependents > N — указывает что удаление
|
||||||
|
* записи в этом dict сложно.</li>
|
||||||
|
* </ul>
|
||||||
*
|
*
|
||||||
* Outgoing FK derived from schemaJson properties[*].x-references
|
* <p>Replaces старый 3-col hub view с central dict card — center больше не
|
||||||
* (format: "dict.field"). Incoming refBy via useDictionaryDependents.
|
* нужен (всё info уже в InfoPanel слева).
|
||||||
*
|
|
||||||
* Click на neighbor card → navigate к его Hub view (recursion fine).
|
|
||||||
*/
|
*/
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Badge } from '@/ui'
|
import { useDictionaries, useDictionaryDependents } from '@/api/queries'
|
||||||
import { useDictionaryDependents, useDictionaries } from '@/api/queries'
|
import type {
|
||||||
import type { DictionaryDetail, DictionaryDefinition, JsonSchema } from '@/api/client'
|
DictionaryDefinition,
|
||||||
|
DictionaryDetail,
|
||||||
|
JsonSchema,
|
||||||
|
SchemaDependent,
|
||||||
|
} from '@/api/client'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const SCOPE_STRIP: Record<string, string> = {
|
type OutgoingFkRow = {
|
||||||
PUBLIC: 'bg-emerald-500',
|
/** Target dict name. */
|
||||||
INTERNAL: 'bg-amber-500',
|
target: string
|
||||||
RESTRICTED: 'bg-rose-500',
|
/** Source field name in current schema. */
|
||||||
|
sourceField: string
|
||||||
|
/** Target field name (usually .id или .code). */
|
||||||
|
targetField: string
|
||||||
|
/** Required в JSON schema. */
|
||||||
|
required: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Walk schema properties → build outgoing FK rows. */
|
||||||
* Walk schemaJson properties — собираем targets из x-references.
|
const extractOutgoingFk = (schema: JsonSchema | undefined): OutgoingFkRow[] => {
|
||||||
* Format: "targetDict.targetField" → возвращаем уникальные targetDict names.
|
|
||||||
*/
|
|
||||||
const extractOutgoingFk = (schema: JsonSchema | undefined): string[] => {
|
|
||||||
if (!schema?.properties) return []
|
if (!schema?.properties) return []
|
||||||
const targets = new Set<string>()
|
const required = new Set(schema.required ?? [])
|
||||||
for (const prop of Object.values(schema.properties)) {
|
const rows: OutgoingFkRow[] = []
|
||||||
|
for (const [field, prop] of Object.entries(schema.properties)) {
|
||||||
const ref = prop['x-references']
|
const ref = prop['x-references']
|
||||||
if (typeof ref === 'string' && ref.length > 0) {
|
if (typeof ref === 'string' && ref.length > 0) {
|
||||||
const [target] = ref.split('.')
|
const [target, targetField] = ref.split('.')
|
||||||
if (target) targets.add(target)
|
if (target) {
|
||||||
|
rows.push({
|
||||||
|
target,
|
||||||
|
sourceField: field,
|
||||||
|
targetField: targetField ?? 'id',
|
||||||
|
required: required.has(field),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Array.from(targets).sort()
|
return rows.sort((a, b) => a.target.localeCompare(b.target))
|
||||||
}
|
|
||||||
|
|
||||||
const NeighborCard = ({
|
|
||||||
dict,
|
|
||||||
side,
|
|
||||||
}: {
|
|
||||||
dict: DictionaryDefinition
|
|
||||||
side: 'left' | 'right'
|
|
||||||
}) => (
|
|
||||||
<Link
|
|
||||||
to="/dictionaries/$name"
|
|
||||||
params={{ name: dict.name }}
|
|
||||||
search={{ tab: 'relations' }}
|
|
||||||
className={`group block bg-white border border-line rounded-lg p-3 transition hover:shadow-hover hover:border-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ${
|
|
||||||
side === 'left' ? 'text-right' : 'text-left'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className={`flex items-baseline gap-2 ${side === 'left' ? 'justify-end' : ''}`}>
|
|
||||||
<span
|
|
||||||
className={`inline-block size-1.5 rounded-full shrink-0 ${
|
|
||||||
SCOPE_STRIP[dict.scope] ?? 'bg-ink/40'
|
|
||||||
}`}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<h4 className="font-sans text-[14px] font-semibold text-accent truncate group-hover:underline">
|
|
||||||
{dict.displayName ?? dict.name}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div className="mt-0.5 font-mono text-[10px] text-mute truncate">
|
|
||||||
{dict.name}
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
|
|
||||||
const SectionLabel = ({
|
|
||||||
side,
|
|
||||||
count,
|
|
||||||
}: {
|
|
||||||
side: 'left' | 'right'
|
|
||||||
count: number
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`text-cap text-mute mb-2 ${
|
|
||||||
side === 'left' ? 'text-right' : 'text-left'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{side === 'left'
|
|
||||||
? t('hub.outgoing', { count, defaultValue: '→ ссылается ({{count}})' })
|
|
||||||
: t('hub.incoming', { count, defaultValue: '← {{count}} использ.' })}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DictionaryHubView({ detail }: { detail: DictionaryDetail }) {
|
export function DictionaryHubView({ detail }: { detail: DictionaryDetail }) {
|
||||||
@@ -101,113 +65,146 @@ export function DictionaryHubView({ detail }: { detail: DictionaryDetail }) {
|
|||||||
const { data: allDicts } = useDictionaries()
|
const { data: allDicts } = useDictionaries()
|
||||||
const { data: refByRaw } = useDictionaryDependents(detail.name)
|
const { data: refByRaw } = useDictionaryDependents(detail.name)
|
||||||
|
|
||||||
// Outgoing FK targets — names derived из schemaJson.
|
const outgoing = useMemo(() => extractOutgoingFk(detail.schemaJson), [detail.schemaJson])
|
||||||
const outgoingNames = useMemo(
|
const incoming: SchemaDependent[] = useMemo(() => refByRaw ?? [], [refByRaw])
|
||||||
() => extractOutgoingFk(detail.schemaJson),
|
|
||||||
[detail.schemaJson],
|
// Resolve target dict definitions для record count badge.
|
||||||
|
const dictByName = useMemo(() => {
|
||||||
|
if (!allDicts) return new Map<string, DictionaryDefinition>()
|
||||||
|
return new Map(allDicts.map((d) => [d.name, d]))
|
||||||
|
}, [allDicts])
|
||||||
|
|
||||||
|
// Aggregate total incoming deps count для warning footer.
|
||||||
|
const totalIncomingDeps = useMemo(
|
||||||
|
() => incoming.reduce((sum, d) => sum + d.activeRecordsInSourceDict, 0),
|
||||||
|
[incoming],
|
||||||
)
|
)
|
||||||
|
const HEAVY_THRESHOLD = 100
|
||||||
// Resolve names → DictionaryDefinition (для card rendering).
|
|
||||||
// Filter недоступные / unknown names (защита от устаревших schema refs).
|
|
||||||
const outgoingDicts = useMemo(() => {
|
|
||||||
if (!allDicts) return []
|
|
||||||
const byName = new Map(allDicts.map((d) => [d.name, d]))
|
|
||||||
return outgoingNames
|
|
||||||
.map((name) => byName.get(name))
|
|
||||||
.filter((d): d is DictionaryDefinition => Boolean(d))
|
|
||||||
}, [allDicts, outgoingNames])
|
|
||||||
|
|
||||||
// refBy: уникальные source dicts (те что ссылаются на нас) → resolve в definitions.
|
|
||||||
const incomingDicts = useMemo(() => {
|
|
||||||
if (!allDicts || !refByRaw) return []
|
|
||||||
const byName = new Map(allDicts.map((d) => [d.name, d]))
|
|
||||||
const seen = new Set<string>()
|
|
||||||
const out: DictionaryDefinition[] = []
|
|
||||||
for (const dep of refByRaw) {
|
|
||||||
if (seen.has(dep.sourceDict)) continue
|
|
||||||
seen.add(dep.sourceDict)
|
|
||||||
const def = byName.get(dep.sourceDict)
|
|
||||||
if (def) out.push(def)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}, [allDicts, refByRaw])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
aria-label={t('hub.heading', { defaultValue: 'Связи справочника' })}
|
{/* Outgoing → */}
|
||||||
className="grid grid-cols-1 md:grid-cols-[1fr_minmax(280px,_360px)_1fr] gap-4 md:gap-6 items-start py-2"
|
<section className="rounded-lg border border-line bg-surface overflow-hidden">
|
||||||
>
|
<header className="px-4 py-2.5 border-b border-line-2">
|
||||||
{/* Left rail: outgoing FK */}
|
<div className="text-cap text-mute tracking-[0.16em] uppercase">
|
||||||
<div className="md:order-1 order-2 space-y-2">
|
→ {t('hub.outgoing', { defaultValue: 'ссылается на' })}
|
||||||
<SectionLabel side="left" count={outgoingDicts.length} />
|
<span className="text-mono text-ink ml-1">({outgoing.length})</span>
|
||||||
{outgoingDicts.length === 0 ? (
|
</div>
|
||||||
<div className="text-cell text-mute/70 text-right italic">
|
</header>
|
||||||
{t('hub.noOutgoing', { defaultValue: 'нет исходящих ссылок' })}
|
{outgoing.length === 0 ? (
|
||||||
|
<div className="px-4 py-8 text-center text-body text-mute">
|
||||||
|
{t('hub.empty.outgoing', {
|
||||||
|
defaultValue: 'Нет исходящих FK',
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<ul>
|
||||||
{outgoingDicts.map((d) => (
|
{outgoing.map((row) => {
|
||||||
<NeighborCard key={d.id} dict={d} side="left" />
|
const target = dictByName.get(row.target)
|
||||||
))}
|
return (
|
||||||
</div>
|
<li
|
||||||
|
key={`${row.target}.${row.sourceField}`}
|
||||||
|
className="border-b border-line-2 last:border-b-0"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to="/dictionaries/$name"
|
||||||
|
params={{ name: row.target }}
|
||||||
|
className="flex items-center gap-3 px-4 py-2.5 hover:bg-surface-2/50 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-mono text-body text-accent font-medium truncate flex-1 min-w-0">
|
||||||
|
{row.target}
|
||||||
|
</span>
|
||||||
|
{target?.recordCount !== undefined && (
|
||||||
|
<span className="text-mono text-cell text-mute tabular-nums shrink-0">
|
||||||
|
{target.recordCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-cap font-semibold tracking-wider px-1.5 py-0.5 rounded-sm shrink-0',
|
||||||
|
row.required
|
||||||
|
? 'bg-accent-bg text-accent'
|
||||||
|
: 'bg-surface-2 text-mute',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{row.required ? 'required' : 'optional'}
|
||||||
|
</span>
|
||||||
|
<span className="text-mono text-cell text-ink-2 shrink-0 min-w-[80px] text-right">
|
||||||
|
{row.sourceField}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
{/* Center: focused dict (large card) */}
|
{/* Incoming ← */}
|
||||||
<div className="md:order-2 order-1 relative">
|
<section className="rounded-lg border border-line bg-surface overflow-hidden flex flex-col">
|
||||||
<div
|
<header className="px-4 py-2.5 border-b border-line-2">
|
||||||
className="relative bg-white rounded-xl p-5 border-2 border-accent"
|
<div className="text-cap text-mute tracking-[0.16em] uppercase">
|
||||||
style={{ boxShadow: '0 18px 36px -22px rgba(20,30,140,.5)' }}
|
← {t('hub.incoming', { defaultValue: 'ссылаются на нас' })}
|
||||||
>
|
<span className="text-mono text-ink ml-1">({incoming.length})</span>
|
||||||
<div className="flex items-start gap-2 mb-2 flex-wrap">
|
|
||||||
<Badge variant="info">{detail.scope}</Badge>
|
|
||||||
{detail.approvalRequired && (
|
|
||||||
<Badge variant="warning">
|
|
||||||
{t('dict.list.approval', { defaultValue: 'approval' })}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
<span className="text-mono ml-auto text-mute">
|
|
||||||
v{detail.schemaVersion}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-title-xl font-sans text-accent leading-tight">
|
</header>
|
||||||
{detail.displayName ?? detail.name}
|
{incoming.length === 0 ? (
|
||||||
</h2>
|
<div className="px-4 py-8 text-center text-body text-mute">
|
||||||
<div className="text-mono text-mute mt-0.5">
|
{t('hub.empty.incoming', {
|
||||||
{detail.name}
|
defaultValue: 'Никто не ссылается',
|
||||||
</div>
|
})}
|
||||||
{detail.description && (
|
|
||||||
<p className="text-[13px] text-ink mt-3 leading-relaxed">
|
|
||||||
{detail.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div className="text-cap mt-4 pt-3 border-t border-line flex items-center justify-between text-mute">
|
|
||||||
<span>{detail.bundle}</span>
|
|
||||||
<span>{detail.supportedLocales.join(', ')}</span>
|
|
||||||
{typeof detail.recordCount === 'number' && (
|
|
||||||
<span>
|
|
||||||
{t('dict.list.recordCount', { count: detail.recordCount })}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right rail: refBy */}
|
|
||||||
<div className="md:order-3 order-3 space-y-2">
|
|
||||||
<SectionLabel side="right" count={incomingDicts.length} />
|
|
||||||
{incomingDicts.length === 0 ? (
|
|
||||||
<div className="text-cell text-mute/70 italic">
|
|
||||||
{t('hub.noIncoming', { defaultValue: 'никто не ссылается' })}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<>
|
||||||
{incomingDicts.map((d) => (
|
<ul className="flex-1">
|
||||||
<NeighborCard key={d.id} dict={d} side="right" />
|
{incoming.map((dep) => (
|
||||||
))}
|
<li
|
||||||
</div>
|
key={`${dep.sourceDict}.${dep.sourceField}`}
|
||||||
|
className="border-b border-line-2 last:border-b-0"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to="/dictionaries/$name"
|
||||||
|
params={{ name: dep.sourceDict }}
|
||||||
|
className="flex items-center gap-3 px-4 py-2.5 hover:bg-surface-2/50 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-mono text-body text-accent font-medium truncate flex-1 min-w-0">
|
||||||
|
{dep.sourceDict}
|
||||||
|
</span>
|
||||||
|
{dep.activeRecordsInSourceDict > 0 && (
|
||||||
|
<span className="text-mono text-cell text-mute tabular-nums shrink-0">
|
||||||
|
{dep.activeRecordsInSourceDict}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{dep.onClose === 'BLOCK' && (
|
||||||
|
<span className="text-cap font-semibold tracking-wider px-1.5 py-0.5 rounded-sm bg-pink-bg text-pink shrink-0">
|
||||||
|
{t('hub.critical', { defaultValue: 'критично' })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{dep.onClose === 'CASCADE' && (
|
||||||
|
<span className="text-cap font-semibold tracking-wider px-1.5 py-0.5 rounded-sm bg-warn-bg text-warn shrink-0">
|
||||||
|
CASCADE
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-mono text-cell text-ink-2 shrink-0 min-w-[80px] text-right">
|
||||||
|
.{dep.sourceField}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{/* Footer warning если в this dict много incoming refs — удаление
|
||||||
|
* записи будет сложным (cascade chain или blockers). */}
|
||||||
|
{totalIncomingDeps > HEAVY_THRESHOLD && (
|
||||||
|
<div className="px-4 py-2.5 border-t border-line-2 bg-warn-bg/30 text-cell text-ink-2">
|
||||||
|
{t('hub.heavyDeps', {
|
||||||
|
count: totalIncomingDeps,
|
||||||
|
defaultValue: `Удаление записи в этом справочнике сложно — ${totalIncomingDeps} ссылок`,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { dictionaryDependentsQuery, useDictionaries, useDictionaryDependents } f
|
|||||||
import type { DataScope, DictionaryDefinition, SchemaDependent } from '@/api/client'
|
import type { DataScope, DictionaryDefinition, SchemaDependent } from '@/api/client'
|
||||||
import { DictionaryEditorDialog } from '@/components/schema/DictionaryEditorDialog'
|
import { DictionaryEditorDialog } from '@/components/schema/DictionaryEditorDialog'
|
||||||
import { useCanMutate } from '@/auth/useCanMutate'
|
import { useCanMutate } from '@/auth/useCanMutate'
|
||||||
import { SCOPE_DOT, SCOPE_ORDER } from '@/lib/scope-style'
|
import { SCOPE_BG_TINT, SCOPE_DOT, SCOPE_ORDER, SCOPE_TEXT } from '@/lib/scope-style'
|
||||||
|
|
||||||
// ===== Search params =====
|
// ===== Search params =====
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ function DictionariesPage() {
|
|||||||
title={t(`dict.list.section.${scope}`)}
|
title={t(`dict.list.section.${scope}`)}
|
||||||
className={`text-cap tracking-[0.14em] uppercase font-semibold px-3 py-1.5 rounded-md border transition-colors focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
className={`text-cap tracking-[0.14em] uppercase font-semibold px-3 py-1.5 rounded-md border transition-colors focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
||||||
selected
|
selected
|
||||||
? 'border-ink bg-surface text-ink ring-1 ring-ink'
|
? `${SCOPE_BG_TINT[scope]} ${SCOPE_TEXT[scope]} border-transparent`
|
||||||
: 'border-line bg-surface text-ink-2 hover:border-ink-2 hover:bg-surface-2'
|
: 'border-line bg-surface text-ink-2 hover:border-ink-2 hover:bg-surface-2'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -473,18 +473,28 @@ function DictRow({ d, t }: { d: DictionaryDefinition; t: TFunc }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per redesign prototype: row левый край имеет 4px stripe в scope color +
|
||||||
|
// INTERNAL/RESTRICTED rows получают subtle bg tint для visual scope grouping.
|
||||||
|
// PUBLIC = plain white (default — самый common scope не нужно подкрашивать).
|
||||||
|
const rowTint = d.scope === 'PUBLIC' ? '' : `${SCOPE_BG_TINT[d.scope]}/30`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
role="link"
|
role="link"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onKeyDown={handleKey}
|
onKeyDown={handleKey}
|
||||||
className="border-b border-line-2 last:border-b-0 cursor-pointer hover:bg-surface-2/40 focus-visible:outline-none focus-visible:bg-accent-bg/30 transition-colors"
|
className={`relative border-b border-line-2 last:border-b-0 cursor-pointer hover:bg-surface-2/40 focus-visible:outline-none focus-visible:bg-accent-bg/30 transition-colors ${rowTint}`}
|
||||||
>
|
>
|
||||||
{/* name + subtitle (description short) */}
|
{/* name + subtitle (description short) + scope-color left stripe */}
|
||||||
<td className="px-3 py-2 align-top min-w-0">
|
<td className="relative px-3 py-2 align-top min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
{/* 3px scope stripe — absolutely positioned на левом краю row.
|
||||||
<span className={`inline-block size-1.5 rounded-full shrink-0 ${SCOPE_DOT[d.scope]}`} aria-hidden />
|
* td имеет relative чтобы before работало в table context. */}
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className={`absolute left-0 top-0 bottom-0 w-[3px] ${SCOPE_DOT[d.scope]}`}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2 pl-1">
|
||||||
<span className="text-body font-medium text-ink truncate">
|
<span className="text-body font-medium text-ink truncate">
|
||||||
{d.displayName ?? d.name}
|
{d.displayName ?? d.name}
|
||||||
</span>
|
</span>
|
||||||
@@ -493,7 +503,7 @@ function DictRow({ d, t }: { d: DictionaryDefinition; t: TFunc }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{d.description && (
|
{d.description && (
|
||||||
<p className="text-cell text-mute mt-0.5 truncate max-w-md">
|
<p className="text-cell text-mute mt-0.5 truncate max-w-md pl-1">
|
||||||
{d.description}
|
{d.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -506,9 +516,13 @@ function DictRow({ d, t }: { d: DictionaryDefinition; t: TFunc }) {
|
|||||||
<td className="px-3 py-2 hidden lg:table-cell">
|
<td className="px-3 py-2 hidden lg:table-cell">
|
||||||
<span className="text-cap text-mute">{d.bundle}</span>
|
<span className="text-cap text-mute">{d.bundle}</span>
|
||||||
</td>
|
</td>
|
||||||
{/* scope badge */}
|
{/* scope badge — colored по scope (PUBLIC accent / INTERNAL warn / RESTRICTED pink) */}
|
||||||
<td className="px-3 py-2 hidden sm:table-cell">
|
<td className="px-3 py-2 hidden sm:table-cell">
|
||||||
<Badge variant="info">{d.scope}</Badge>
|
<span
|
||||||
|
className={`inline-flex items-center px-2 py-0.5 rounded-sm text-cap tracking-wider font-semibold uppercase ${SCOPE_BG_TINT[d.scope]} ${SCOPE_TEXT[d.scope]}`}
|
||||||
|
>
|
||||||
|
{d.scope}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
{/* records count */}
|
{/* records count */}
|
||||||
<td className="px-3 py-2 text-right tabular-nums text-mono text-ink-2 hidden md:table-cell">
|
<td className="px-3 py-2 text-right tabular-nums text-mono text-ink-2 hidden md:table-cell">
|
||||||
|
|||||||
Reference in New Issue
Block a user