From c57003cd7301e2f296d3db6b4d7ef48fe414c4a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=97=D0=B8=D0=BC=D0=B8=D0=BD?= Date: Sun, 10 May 2026 21:19:07 +0000 Subject: [PATCH] fix(admin-ui): guest nav filter + suppress drafts 401 --- .../README.md | 394 ----- .../design/design-canvas.jsx | 936 ----------- .../design/dictionary-prototype.html | 118 -- .../design/dictionary-views.html | 486 ------ .../design/proto/data.jsx | 49 - .../design/proto/ui.jsx | 72 - .../design/proto/view-graph.jsx | 180 --- .../design/proto/view-hub.jsx | 154 -- .../design/proto/view-list.jsx | 154 -- .../patches/README.md | 133 -- .../screenshots/A-graph-view.png | Bin 21150 -> 0 bytes .../screenshots/B-hub-view.png | Bin 23095 -> 0 bytes .../screenshots/D-list-view.png | Bin 28504 -> 0 bytes .../design_handoff_ordinis_mdm/README.md | 499 ++++++ .../design/compact.html | 1398 +++++++++++++++++ .../design/ui-kit.html | 680 ++++++++ .../review.md | 0 .../screenshots/01-catalog-light.png | Bin 0 -> 32827 bytes .../screenshots/02-catalog-dark.png | Bin 0 -> 27918 bytes .../screenshots/03-uikit-light.png | Bin 0 -> 30126 bytes .../screenshots/04-uikit-dark.png | Bin 0 -> 30748 bytes ordinis-admin-ui/src/routes/__root.tsx | 80 +- .../src/routes/dictionaries.$name.tsx | 4 +- 23 files changed, 2625 insertions(+), 2712 deletions(-) delete mode 100644 ordinis-admin-ui/design_handoff_dictionary_catalog/README.md delete mode 100644 ordinis-admin-ui/design_handoff_dictionary_catalog/design/design-canvas.jsx delete mode 100644 ordinis-admin-ui/design_handoff_dictionary_catalog/design/dictionary-prototype.html delete mode 100644 ordinis-admin-ui/design_handoff_dictionary_catalog/design/dictionary-views.html delete mode 100644 ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/data.jsx delete mode 100644 ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/ui.jsx delete mode 100644 ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/view-graph.jsx delete mode 100644 ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/view-hub.jsx delete mode 100644 ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/view-list.jsx delete mode 100644 ordinis-admin-ui/design_handoff_dictionary_catalog/patches/README.md delete mode 100644 ordinis-admin-ui/design_handoff_dictionary_catalog/screenshots/A-graph-view.png delete mode 100644 ordinis-admin-ui/design_handoff_dictionary_catalog/screenshots/B-hub-view.png delete mode 100644 ordinis-admin-ui/design_handoff_dictionary_catalog/screenshots/D-list-view.png create mode 100644 ordinis-admin-ui/design_handoff_ordinis_mdm/README.md create mode 100644 ordinis-admin-ui/design_handoff_ordinis_mdm/design/compact.html create mode 100644 ordinis-admin-ui/design_handoff_ordinis_mdm/design/ui-kit.html rename ordinis-admin-ui/{design_handoff_dictionary_catalog => design_handoff_ordinis_mdm}/review.md (100%) create mode 100644 ordinis-admin-ui/design_handoff_ordinis_mdm/screenshots/01-catalog-light.png create mode 100644 ordinis-admin-ui/design_handoff_ordinis_mdm/screenshots/02-catalog-dark.png create mode 100644 ordinis-admin-ui/design_handoff_ordinis_mdm/screenshots/03-uikit-light.png create mode 100644 ordinis-admin-ui/design_handoff_ordinis_mdm/screenshots/04-uikit-dark.png diff --git a/ordinis-admin-ui/design_handoff_dictionary_catalog/README.md b/ordinis-admin-ui/design_handoff_dictionary_catalog/README.md deleted file mode 100644 index a854378..0000000 --- a/ordinis-admin-ui/design_handoff_dictionary_catalog/README.md +++ /dev/null @@ -1,394 +0,0 @@ -# Handoff: Dictionary Catalog Redesign + nstart-ui Popover Fix - -## Overview - -Two interconnected pieces of work for the **Ordinis admin UI** (`ordinis-admin-ui`, React 19 + TanStack Router + `@nstart/ui` 0.1.3): - -1. **`@nstart/ui` patch** — `Select` / `MultiSelect` / `DatePicker` popovers render inline and get clipped by modal `overflow:auto` (visible bug in the "Создать справочник" modal). Fix via `pnpm patch` to render into a portal on `document.body`. Same patch fixes `Checkbox.indeterminate` ref-forwarding. - -2. **Dictionary catalog redesign** — three new ways to browse the 40+ dictionaries with their FK relationships visible: - - **D · List view with inline FK chips** (smallest change; replaces current grid) - - **A · Graph view** (force-directed overview of all dicts + edges) - - **B · Hub view** (one dict in focus, neighbors on either side; new tab inside `dictionaries.$name.tsx`) - -## About the Design Files - -The HTML/JSX in `design/` is a **design reference**, not production code to copy directly. It's a working prototype showing intended look, layout, interactions, and data shape — but written as a single self-contained HTML file with inline Babel. - -**Your task:** recreate these views inside the existing `ordinis-admin-ui` React/TypeScript codebase, using its established patterns: -- TanStack Router routes -- TanStack Query for data -- `@nstart/ui` components (`Button`, `Modal`, `Tabs`, `Badge`, `Alert`, etc.) for chrome -- TailwindCSS v4 (project already uses it via `@tailwindcss/vite`) -- React Hook Form + AJV for forms -- Phosphor Icons (`@phosphor-icons/react`) - -**Do not** ship the prototype HTML to production. **Do not** import the prototype `.jsx` files — they use `window.*` globals and inline Babel which won't work in the Vite build. - -## Fidelity - -**Mid-fidelity.** Final colors, typography, spacing, and interactions are decided. Pixel-perfect recreation is the goal, but you must adapt to: -- Real component primitives from `@nstart/ui` (the prototype uses raw ` - {menuOpen && ( -
e.stopPropagation()}> - - -
- -
- )} - - - - -
- {children ||
{id}
} -
- - ); -} - -// Inline rename — commits on blur or Enter. -function DCEditable({ value, onChange, style, tag = 'span', onClick }) { - const T = tag; - return ( - e.stopPropagation()} - onBlur={(e) => onChange && onChange(e.currentTarget.textContent)} - onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }} - style={style}>{value} - ); -} - -// ───────────────────────────────────────────────────────────── -// Focus mode — overlay one artboard; ←/→ within section, ↑/↓ across -// sections, Esc or backdrop click to exit. -// ───────────────────────────────────────────────────────────── -function DCFocusOverlay({ entry, sectionMeta, sectionOrder }) { - const ctx = React.useContext(DCCtx); - const { sectionId, artboard } = entry; - const sec = ctx.section(sectionId); - const meta = sectionMeta[sectionId]; - const peers = meta.slotIds; - const aid = artboard.props.id ?? artboard.props.label; - const idx = peers.indexOf(aid); - const secIdx = sectionOrder.indexOf(sectionId); - - const go = (d) => { const n = peers[(idx + d + peers.length) % peers.length]; if (n) ctx.setFocus(`${sectionId}/${n}`); }; - const goSection = (d) => { - // Sections whose artboards are all deleted have slotIds:[] — step past - // them to the next non-empty section so ↑/↓ doesn't dead-end. - const n = sectionOrder.length; - for (let i = 1; i < n; i++) { - const ns = sectionOrder[(((secIdx + d * i) % n) + n) % n]; - const first = sectionMeta[ns] && sectionMeta[ns].slotIds[0]; - if (first) { ctx.setFocus(`${ns}/${first}`); return; } - } - }; - - React.useEffect(() => { - const k = (e) => { - if (e.key === 'ArrowLeft') { e.preventDefault(); go(-1); } - if (e.key === 'ArrowRight') { e.preventDefault(); go(1); } - if (e.key === 'ArrowUp') { e.preventDefault(); goSection(-1); } - if (e.key === 'ArrowDown') { e.preventDefault(); goSection(1); } - }; - document.addEventListener('keydown', k); - return () => document.removeEventListener('keydown', k); - }); - - const { width = 260, height = 480, children } = artboard.props; - const [vp, setVp] = React.useState({ w: window.innerWidth, h: window.innerHeight }); - React.useEffect(() => { const r = () => setVp({ w: window.innerWidth, h: window.innerHeight }); window.addEventListener('resize', r); return () => window.removeEventListener('resize', r); }, []); - const scale = Math.max(0.1, Math.min((vp.w - 200) / width, (vp.h - 260) / height, 2)); - - const [ddOpen, setDd] = React.useState(false); - const Arrow = ({ dir, onClick }) => ( - - ); - - // Portal to body so position:fixed is the real viewport regardless of any - // transform on DesignCanvas's ancestors (including the canvas zoom itself). - return ReactDOM.createPortal( -
ctx.setFocus(null)} - onWheel={(e) => e.preventDefault()} - style={{ position: 'fixed', inset: 0, zIndex: 100, background: 'rgba(24,20,16,.6)', backdropFilter: 'blur(14px)', - fontFamily: DC.font, color: '#fff' }}> - - {/* top bar: section dropdown (left) · close (right) */} -
e.stopPropagation()} - style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 72, display: 'flex', alignItems: 'flex-start', padding: '16px 20px 0', gap: 16 }}> -
- - {ddOpen && ( -
- {sectionOrder.filter((sid) => sectionMeta[sid].slotIds.length).map((sid) => ( - - ))} -
- )} -
-
- -
- - {/* card centered, label + index below — only the card itself stops - propagation so any backdrop click (including the margins around - the card) exits focus */} -
-
e.stopPropagation()} style={{ width: width * scale, height: height * scale, position: 'relative' }}> -
- {children ||
{aid}
} -
-
-
e.stopPropagation()} style={{ fontSize: 14, fontWeight: 500, opacity: .85, textAlign: 'center' }}> - {(sec.labels || {})[aid] ?? artboard.props.label} - {idx + 1} / {peers.length} -
-
- - go(-1)} /> - go(1)} /> - - {/* dots */} -
e.stopPropagation()} - style={{ position: 'absolute', bottom: 20, left: '50%', transform: 'translateX(-50%)', display: 'flex', gap: 8 }}> - {peers.map((p, i) => ( -
-
, - document.body, - ); -} - -// ───────────────────────────────────────────────────────────── -// Post-it — absolute-positioned sticky note -// ───────────────────────────────────────────────────────────── -function DCPostIt({ children, top, left, right, bottom, rotate = -2, width = 180 }) { - return ( -
{children}
- ); -} - -Object.assign(window, { DesignCanvas, DCSection, DCArtboard, DCPostIt }); - diff --git a/ordinis-admin-ui/design_handoff_dictionary_catalog/design/dictionary-prototype.html b/ordinis-admin-ui/design_handoff_dictionary_catalog/design/dictionary-prototype.html deleted file mode 100644 index 6721395..0000000 --- a/ordinis-admin-ui/design_handoff_dictionary_catalog/design/dictionary-prototype.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - -Ordinis · Dictionary catalog — prototype - - - - -
- - - - - - - - - - - - - - - diff --git a/ordinis-admin-ui/design_handoff_dictionary_catalog/design/dictionary-views.html b/ordinis-admin-ui/design_handoff_dictionary_catalog/design/dictionary-views.html deleted file mode 100644 index b27ef72..0000000 --- a/ordinis-admin-ui/design_handoff_dictionary_catalog/design/dictionary-views.html +++ /dev/null @@ -1,486 +0,0 @@ - - - - -Dictionary views — exploration - - - - - - - - - - - - - - diff --git a/ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/data.jsx b/ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/data.jsx deleted file mode 100644 index 0546299..0000000 --- a/ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/data.jsx +++ /dev/null @@ -1,49 +0,0 @@ -// Shared data for dictionary prototype. -// Каждый dict: id, title, desc, scope, bundle, fk[]. refBy вычисляется. - -const RAW_DICTS = [ - { id:"satellite_types", title:"Типы КА", desc:"Классификация КА по миссиям и характеристикам", scope:"PUBLIC", bundle:"cuod", fk:[] }, - { id:"satellite_statuses", title:"Статусы КА", desc:"Жизненный цикл: planned → operational → decommissioned/lost", scope:"PUBLIC", bundle:"cuod", fk:[] }, - { id:"antennas", title:"Антенны", desc:"Антенны наземных станций с G/T и поддерживаемыми диапазонами", scope:"PUBLIC", bundle:"cuod", fk:["frequency_bands"] }, - { id:"missions", title:"Миссии / программы", desc:"Программы ДЗЗ — Ресурс-П, Канопус-В, Sentinel и т.д.", scope:"PUBLIC", bundle:"cuod", fk:["satellite_types","operators"] }, - { id:"satellites", title:"Космические аппараты", desc:"Каталог КА (космических аппаратов) ДЗЗ-миссий", scope:"PUBLIC", bundle:"cuod", fk:["satellite_types","satellite_statuses","missions","operators","orbit_types"] }, - { id:"ground_stations", title:"Наземные станции", desc:"Каталог наземных пунктов приёма и управления", scope:"PUBLIC", bundle:"cuod", fk:["antennas","countries","timezones"] }, - { id:"instruments", title:"Инструменты съёмки", desc:"Полезная нагрузка / сенсоры на борту КА", scope:"PUBLIC", bundle:"cuod", fk:["sensor_types","satellites"] }, - { id:"sensor_types", title:"Типы сенсоров", desc:"Класс ПН: optical / SAR / hyperspectral / lidar / thermal", scope:"PUBLIC", bundle:"cuod", fk:[] }, - { id:"spectral_bands", title:"Спектральные каналы", desc:"Стандартные оптические/ИК каналы (PAN/R/G/B/NIR/SWIR/TIR)", scope:"PUBLIC", bundle:"cuod", fk:["instruments"] }, - { id:"frequency_bands", title:"Радиодиапазоны", desc:"Радио-диапазоны: L/S/C/X/Ku/Ka/V", scope:"PUBLIC", bundle:"cuod", fk:[] }, - { id:"orbit_types", title:"Типы орбит", desc:"LEO / SSO / MEO / GEO / HEO", scope:"PUBLIC", bundle:"cuod", fk:[] }, - { id:"product_levels", title:"Уровни продукта", desc:"L0 / L1A / L1B / L2 / L3", scope:"PUBLIC", bundle:"cuod", fk:[] }, - { id:"projection_codes", title:"Картографические проекции",desc:"EPSG-коды для картографии", scope:"PUBLIC", bundle:"geo", fk:[] }, - { id:"countries", title:"Страны (ISO 3166)", desc:"ISO 3166: двух- и трёхбуквенные коды, имена", scope:"PUBLIC", bundle:"geo", fk:[] }, - { id:"timezones", title:"Часовые пояса", desc:"IANA tz database", scope:"PUBLIC", bundle:"geo", fk:["countries"] }, - { id:"languages", title:"Языки (BCP-47)", desc:"ru-RU, en-US, … — справочник локалей", scope:"PUBLIC", bundle:"i18n", fk:[] }, - { id:"operators", title:"Операторы", desc:"Роскосмос, NASA, ESA — операторы программ", scope:"PUBLIC", bundle:"cuod", fk:["countries"] }, - { id:"acquisitions", title:"Сеансы съёмки", desc:"Планы и факты сеансов съёмки", scope:"INTERNAL", bundle:"cuod", fk:["satellites","ground_stations","instruments","spectral_bands","product_levels"] }, - { id:"products", title:"Продукты ДЗЗ", desc:"Поставляемые сцены и тайлы", scope:"INTERNAL", bundle:"cuod", fk:["acquisitions","product_levels","projection_codes"] }, - { id:"users_roles", title:"Роли пользователей", desc:"RBAC внутри платформы", scope:"INTERNAL", bundle:"core", fk:[] }, - { id:"webhooks_topics", title:"Webhook topics", desc:"Темы событий для исходящих webhook-ов", scope:"INTERNAL", bundle:"core", fk:[] }, - { id:"audit_event_types", title:"Типы аудит-событий", desc:"Каталог типов событий аудита", scope:"INTERNAL", bundle:"core", fk:[] }, - { id:"tenant_quotas", title:"Квоты тенантов", desc:"Лимиты на ресурсы по тенантам", scope:"TENANT", bundle:"core", fk:[] }, - { id:"tenant_branding", title:"Брендинг тенантов", desc:"Цвета, логотипы, домены", scope:"TENANT", bundle:"core", fk:["languages"] }, -]; - -// Compute refBy -const _byId = Object.fromEntries(RAW_DICTS.map(d => [d.id, { ...d, refBy: [] }])); -for (const d of RAW_DICTS) { - for (const t of d.fk) if (_byId[t]) _byId[t].refBy.push(d.id); -} -const DICTS = Object.values(_byId); -const BY_ID = _byId; - -const SCOPE_COLOR = { PUBLIC:"#3a6df0", INTERNAL:"#d4711a", TENANT:"#c64a8a" }; -const SCOPE_BG = { PUBLIC:"#eef3ff", INTERNAL:"#fff2e3", TENANT:"#fde7f1" }; -const SCOPE_LABEL = { PUBLIC:"Публичные", INTERNAL:"Внутренние", TENANT:"Тенант" }; -const BUNDLES = ["cuod","geo","i18n","core"]; - -window.DICTS = DICTS; -window.BY_ID = BY_ID; -window.SCOPE_COLOR = SCOPE_COLOR; -window.SCOPE_BG = SCOPE_BG; -window.SCOPE_LABEL = SCOPE_LABEL; -window.BUNDLES = BUNDLES; diff --git a/ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/ui.jsx b/ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/ui.jsx deleted file mode 100644 index 0d434e7..0000000 --- a/ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/ui.jsx +++ /dev/null @@ -1,72 +0,0 @@ -// Shared atomic UI for the dictionary prototype. -const { useState, useRef, useEffect, useLayoutEffect, useMemo } = React; - -function ScopeDot({ scope }) { - return ( - - ); -} - -function ScopeBadge({ scope }) { - return ( - - {scope} - - ); -} - -function FkChip({ id, dim, onClick }) { - const d = BY_ID[id]; - return ( - - ); -} - -function Btn({ children, primary, onClick, active }) { - return ( - - ); -} - -function Sup({ children, style }) { - return ( -
{children}
- ); -} - -Object.assign(window, { ScopeDot, ScopeBadge, FkChip, Btn, Sup }); diff --git a/ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/view-graph.jsx b/ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/view-graph.jsx deleted file mode 100644 index 72f4e58..0000000 --- a/ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/view-graph.jsx +++ /dev/null @@ -1,180 +0,0 @@ -// View A — Граф связей. Force-style static layout с интерактивностью. -const { useState: useStateA, useMemo: useMemoA } = React; - -const GRAPH_LAYOUT = { - satellite_types: {x:300, y:140}, - satellite_statuses: {x:140, y:230}, - missions: {x:230, y:330}, - satellites: {x:500, y:300}, - instruments: {x:700, y:220}, - sensor_types: {x:840, y:140}, - spectral_bands: {x:840, y:320}, - orbit_types: {x:500, y:160}, - ground_stations: {x:540, y:500}, - antennas: {x:320, y:540}, - frequency_bands: {x:160, y:500}, - countries: {x:700, y:580}, - timezones: {x:860, y:520}, - operators: {x:900, y:420}, - product_levels: {x:540, y:660}, - acquisitions: {x:360, y:680}, - products: {x:180, y:660}, - projection_codes: {x:80, y:580}, - users_roles: {x:1000,y:680}, - webhooks_topics: {x:1000,y:140}, - audit_event_types: {x:1000,y:240}, - tenant_quotas: {x:60, y:140}, - tenant_branding: {x:60, y:340}, - languages: {x:80, y:60}, -}; - -function GraphView({ onOpen }) { - const [hover, setHover] = useStateA(null); - const [showLeaves, setShowLeaves] = useStateA(true); - const [scopeFilter, setScopeFilter] = useStateA(new Set(["PUBLIC","INTERNAL","TENANT"])); - - const visible = useMemoA(() => DICTS.filter(d => { - if (!scopeFilter.has(d.scope)) return false; - if (!showLeaves && d.fk.length === 0 && d.refBy.length === 0) return false; - return true; - }), [scopeFilter, showLeaves]); - - const visIds = useMemoA(() => new Set(visible.map(d => d.id)), [visible]); - - const edges = useMemoA(() => { - const out = []; - for (const d of visible) for (const t of d.fk) if (visIds.has(t)) out.push({ from:d.id, to:t }); - return out; - }, [visible, visIds]); - - const focus = hover; - const isEdgeActive = e => focus && (e.from === focus || e.to === focus); - const isNodeActive = id => !focus || id === focus - || edges.some(e => (e.from === focus && e.to === id) || (e.to === focus && e.from === id)); - - const W = 1180, svgH = 760; - - const toggleScope = s => { - const n = new Set(scopeFilter); - n.has(s) ? n.delete(s) : n.add(s); - setScopeFilter(n); - }; - - return ( -
- {/* toolbar */} -
-
- Граф связей · {visible.length} узлов · {edges.length} связей -
-
- {["PUBLIC","INTERNAL","TENANT"].map(s => ( - toggleScope(s)}> -  {SCOPE_LABEL[s]} - - ))} -
- setShowLeaves(!showLeaves)}>Скрыть листья - Экспорт SVG -
-
- -
- - - - - - - - - - - - - - - {edges.map((e, i) => { - const a = GRAPH_LAYOUT[e.from], b = GRAPH_LAYOUT[e.to]; - if (!a || !b) return null; - const active = isEdgeActive(e); - const opacity = focus && !active ? 0.10 : 1; - return ( - - ); - })} - - {visible.map(d => { - const p = GRAPH_LAYOUT[d.id]; if (!p) return null; - const active = isNodeActive(d.id); - const r = 24 + Math.min(d.refBy.length * 3, 16); - return ( - setHover(d.id)} - onMouseLeave={() => setHover(null)} - onClick={() => onOpen(d.id)}> - - - {d.title.length > 14 ? d.title.slice(0, 12) + "…" : d.title} - - - {d.refBy.length > 0 ? `← ${d.refBy.length}` : "·"} - - - ); - })} - - - {/* hover-tooltip */} - {hover && (() => { - const d = BY_ID[hover]; - const p = GRAPH_LAYOUT[hover]; - return ( -
-
-
{d.title}
- -
-
- {d.id} · v1.0.0 -
-
{d.desc}
-
- → ссылается: {d.fk.length} - ← используют: {d.refBy.length} -
-
- ); - })()} -
- - {/* legend bar */} -
- Размер узла = входящие ссылки - · - Hover — фокус, Click — открыть hub - Layout: forced (статический snapshot) -
-
- ); -} - -window.GraphView = GraphView; diff --git a/ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/view-hub.jsx b/ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/view-hub.jsx deleted file mode 100644 index 812acce..0000000 --- a/ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/view-hub.jsx +++ /dev/null @@ -1,154 +0,0 @@ -// View B — Hub view. Один справочник в фокусе, соседи по сторонам. -const { useState: useStateB, useMemo: useMemoB } = React; - -function HubView({ centerId, onOpen }) { - const center = BY_ID[centerId] || DICTS[0]; - const inn = center.fk.map(id => BY_ID[id]).filter(Boolean); - const out = center.refBy.map(id => BY_ID[id]).filter(Boolean); - const [tab, setTab] = useStateB("links"); // links | fields | preview - - const Card = ({ d, side }) => ( -
onOpen(d.id)} - style={{ - cursor:"pointer", display:"flex", alignItems:"stretch", - background:"#fff", border:"1px solid #e5e7ee", borderRadius:8, - overflow:"hidden", transition:"transform .12s, box-shadow .12s", - }} - onMouseEnter={e => { e.currentTarget.style.transform = side==="left"?"translateX(-2px)":"translateX(2px)"; e.currentTarget.style.boxShadow="0 6px 16px -10px rgba(20,30,140,.25)"; }} - onMouseLeave={e => { e.currentTarget.style.transform = "none"; e.currentTarget.style.boxShadow="none"; }}> -
-
-
{d.title}
-
{d.id}
-
-
- ); - - const Arrow = ({ dir }) => ( - - - - - - - {dir === "right" - ? - : - } - - ); - - return ( -
- {/* topline */} -
-
- Hub · контекст справочника -
{center.title}
-
- {center.id} · bundle: {center.bundle} · v1.0.0 -
-
-
- ← Назад - История - Открыть редактор -
-
- - {/* tabs */} -
- {[ - ["links","Связи",`${inn.length+out.length}`], - ["fields","Поля","12"], - ["preview","Записи","248"], - ].map(([k,label,count]) => ( - - ))} -
- - {tab === "links" && ( - <> - {/* meta strip */} -
- -
{center.desc}
-
- {inn.length} исх. - {out.length} вход. -
-
- -
- {/* incoming dependencies (this dict depends on these) */} -
- Зависит от · {inn.length} -
- {inn.length === 0 &&
нет зависимостей
} - {inn.map(d => ( -
- - -
- ))} -
-
- - {/* center */} -
- Текущий -
{center.title}
-
{center.id}
-
-
{center.desc}
-
- - {/* outgoing (these depend on this) */} -
- На него ссылаются · {out.length} -
- {out.length === 0 &&
никто не ссылается
} - {out.map(d => ( -
- - -
- ))} -
-
-
- -
- Клик по карточке — сделать центром. Ctrl+клик — добавить в выборку для сравнения. Двойной клик — открыть в редакторе. -
- - )} - - {tab !== "links" && ( -
- placeholder -
Содержимое вкладки «{tab === "fields" ? "Поля" : "Записи"}» — без изменений к текущей реализации.
-
- )} -
- ); -} - -window.HubView = HubView; diff --git a/ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/view-list.jsx b/ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/view-list.jsx deleted file mode 100644 index 325d7c4..0000000 --- a/ordinis-admin-ui/design_handoff_dictionary_catalog/design/proto/view-list.jsx +++ /dev/null @@ -1,154 +0,0 @@ -// View D — Список со связями inline. Bundle-grouped, FK chips clickable. -const { useState: useStateD, useMemo: useMemoD } = React; - -function ListView({ onOpen }) { - const [q, setQ] = useStateD(""); - const [withDepsOnly, setWithDepsOnly] = useStateD(false); - const [scopes, setScopes] = useStateD(new Set(["PUBLIC","INTERNAL","TENANT"])); - const [bundleFilter, setBundleFilter] = useStateD(null); // null = все - - const filtered = useMemoD(() => { - const ql = q.trim().toLowerCase(); - return DICTS.filter(d => { - if (!scopes.has(d.scope)) return false; - if (bundleFilter && d.bundle !== bundleFilter) return false; - if (withDepsOnly && d.fk.length === 0 && d.refBy.length === 0) return false; - if (ql && !d.title.toLowerCase().includes(ql) && !d.id.toLowerCase().includes(ql) && !d.desc.toLowerCase().includes(ql)) return false; - return true; - }); - }, [q, withDepsOnly, scopes, bundleFilter]); - - const grouped = useMemoD(() => { - const g = {}; - for (const b of BUNDLES) g[b] = []; - for (const d of filtered) (g[d.bundle] = g[d.bundle] || []).push(d); - return g; - }, [filtered]); - - const toggleScope = s => { - const n = new Set(scopes); n.has(s) ? n.delete(s) : n.add(s); setScopes(n); - }; - - const Item = ({ d }) => ( -
onOpen(d.id)} - style={{ - display:"grid", gridTemplateColumns:"3px 1fr auto", - background:"#fff", border:"1px solid #e5e7ee", borderRadius:10, - overflow:"hidden", cursor:"pointer", transition:"box-shadow .12s, transform .12s", - }} - onMouseEnter={e => { e.currentTarget.style.boxShadow="0 8px 22px -14px rgba(20,30,140,.25)"; e.currentTarget.style.transform="translateY(-1px)"; }} - onMouseLeave={e => { e.currentTarget.style.boxShadow="none"; e.currentTarget.style.transform="none"; }}> -
-
-
- {d.title} - {d.id} -
-
{d.desc}
- - {(d.fk.length > 0 || d.refBy.length > 0) && ( -
- {d.fk.length > 0 && ( - <> - → ссылается - {d.fk.map(id => )} - - )} - {d.refBy.length > 0 && ( - <> - ← {d.refBy.length} использ. - {d.refBy.slice(0,3).map(id => )} - {d.refBy.length > 3 && ( - - +{d.refBy.length - 3} - - )} - - )} -
- )} -
-
- - v1.0.0 -
-
- ); - - return ( -
- {/* toolbar */} -
-
- setQ(e.target.value)} - style={{ - height:36, width:340, padding:"0 14px", - border:"1px solid #e5e7ee", borderRadius:8, - fontSize:13, color:"#11131a", - }}/> - - Показано {filtered.length} из {DICTS.length} - -
-
- {["PUBLIC","INTERNAL","TENANT"].map(s => ( - toggleScope(s)}> -  {SCOPE_LABEL[s]} - - ))} -
- setWithDepsOnly(!withDepsOnly)}> - Только со связями - -
-
- - {/* bundle pills */} -
- bundle - setBundleFilter(null)}>все - {BUNDLES.map(b => { - const n = filtered.filter(d => d.bundle === b).length; - return ( - setBundleFilter(bundleFilter===b ? null : b)}> - {b} {n} - - ); - })} -
- - {/* grouped list */} - {BUNDLES.map(b => { - const items = grouped[b]; - if (!items || items.length === 0) return null; - return ( -
-
- {b} - · {items.length} справочн. -
-
-
- {items.map(d => )} -
-
- ); - })} - - {filtered.length === 0 && ( -
- пусто -
Ничего не найдено по текущим фильтрам
-
- )} -
- ); -} - -window.ListView = ListView; diff --git a/ordinis-admin-ui/design_handoff_dictionary_catalog/patches/README.md b/ordinis-admin-ui/design_handoff_dictionary_catalog/patches/README.md deleted file mode 100644 index 9a741c4..0000000 --- a/ordinis-admin-ui/design_handoff_dictionary_catalog/patches/README.md +++ /dev/null @@ -1,133 +0,0 @@ -# Patches for @nstart/ui - -## How to apply - -```bash -pnpm patch @nstart/ui -# pnpm prints a temp directory, e.g.: -# /private/var/.../node_modules/.pnpm-patches/@nstart__ui@0.1.3 -# открой dist/index.js (или index.mjs — оба файла) в этом каталоге, внеси правки ниже, -# затем: -pnpm patch-commit /private/var/.../node_modules/.pnpm-patches/@nstart__ui@0.1.3 -# pnpm создаст файл patches/@nstart__ui@0.1.3.patch и пропишет в package.json: -# "pnpm": { "patchedDependencies": { "@nstart/ui@0.1.3": "patches/@nstart__ui@0.1.3.patch" } } -``` - -После этого `pnpm install` будет применять патч автоматически. - -## Что менять — Select / MultiSelect / DatePicker popover - -Все три компонента рендерят выпадашку **inline**, поэтому в модалках с -`overflow:auto` она клипается, а в формах двигает разметку. Нужно перевести -рендер popover в портал на `document.body` + `position: fixed`. - -### Шаги - -1. **Найти Popover-блок.** В `dist/index.js` ищи маркер открытия — у nstart-ui - это обычно `data-nstart-popover` или вызов компонента `Popover` / - `Dropdown`. Внутри будет что-то вроде: - - ```jsx - {open && ( -
- … -
- )} - ``` - -2. **Обернуть в portal.** Импортировать `createPortal` из `react-dom` (он в - bundle уже есть как peer-dep) и заменить: - - ```jsx - {open && createPortal( -
- … -
, - document.body - )} - ``` - -3. **Считать позицию trigger'а.** Trigger уже хранится в ref. Нужно вычислять - его `getBoundingClientRect()` при `open === true` и при window - resize/scroll. Добавить хук: - - ```jsx - const [rect, setRect] = useState(null); - useLayoutEffect(() => { - if (!open) return; - const update = () => setRect(triggerRef.current?.getBoundingClientRect()); - update(); - window.addEventListener('resize', update); - window.addEventListener('scroll', update, true); // capture — для модалок со внутренним скроллом - return () => { - window.removeEventListener('resize', update); - window.removeEventListener('scroll', update, true); - }; - }, [open]); - ``` - -4. **Закрытие по клику вне.** Если nstart использует `event.target` внутри - trigger-контейнера — после портала это сломается, потому что popover теперь - вне DOM-дерева trigger'а. Заменить на проверку через - `triggerRef.current.contains(target) || popoverRef.current.contains(target)`. - -5. **Auto-flip (если popover вылазит за viewport).** Добавить: - ```js - const willOverflow = rect.bottom + popoverHeight > window.innerHeight; - const top = willOverflow ? rect.top - popoverHeight - 4 : rect.bottom + 4; - ``` - -### Те же правки для DatePicker - -`@nstart/ui` DatePicker имеет ту же проблему — его календарь рендерится inline. -Найти `nstart-datepicker__panel` и обернуть тем же `createPortal`. - -### Тот же фикс для Checkbox indeterminate (баг #17) - -В `dist/index.js` найти `Checkbox` компонент. Скорее всего: - -```jsx -const Checkbox = forwardRef((props, ref) => ( - -)); -``` - -ref привязан к `