From f1e28c267a5ceebbb4e829911214d33e75711740 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Fri, 10 Jul 2026 18:21:14 +0300 Subject: [PATCH] Add pink media stage and sharing modal system --- CHANGELOG.md | 9 + README.md | 4 +- apps/catalog/package.json | 6 +- apps/catalog/src/CatalogApp.tsx | 230 +++++++++++++++++++--- apps/catalog/src/styles.css | 50 +++++ docs/COMPONENTS.md | 12 +- docs/CONSUMPTION.md | 2 +- docs/ICONS.md | 8 +- docs/SOURCE_BASELINE.md | 5 +- docs/THEMING.md | 3 +- docs/WINDOWS_AND_LAYERS.md | 14 ++ package-lock.json | 24 +-- package.json | 2 +- packages/tokens/package.json | 2 +- packages/tokens/themes.css | 6 +- packages/tokens/tokens.css | 1 + packages/tokens/tokens.json | 2 +- packages/ui-core/package.json | 4 +- packages/ui-core/styles.css | 235 ++++++++++++++++++++++ packages/ui-dom/package.json | 4 +- packages/ui-dom/src/index.ts | 1 + packages/ui-dom/src/shareLink.ts | 59 ++++++ packages/ui-react/package.json | 4 +- packages/ui-react/src/Icon.tsx | 2 +- packages/ui-react/src/SharingModals.tsx | 246 ++++++++++++++++++++++++ packages/ui-react/src/index.ts | 1 + registry/components.json | 33 +++- registry/registry.json | 2 +- registry/sources.json | 10 +- 29 files changed, 917 insertions(+), 64 deletions(-) create mode 100644 packages/ui-dom/src/shareLink.ts create mode 100644 packages/ui-react/src/SharingModals.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 74a95ef..40c2a85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.5.0 — media stage and modal inventory + +- Switched the default NODE.DC application accent to pink while retaining live accent editing and semantic status colors. +- Reduced icon glyph weight/size by roughly 10% without changing button and circular hit-target geometry. +- Connected `MediaSourceField` MP4 uploads to the Launcher-style catalog stage with safe object URL replacement and cleanup. +- Added the Engine-derived `ShareAccessModal` with compact member stack, roles, invitation and immutable owner states. +- Added the BIM-derived `ShareLinkModal` and framework-neutral `createShareLinkController`. +- Audited BIM form, confirmation, share-link, contextual action, expandable detail and version-history modals and added runnable catalog specimens. + ## 0.4.0 — package-native application contract - Corrected `AppHeader` against the production Launcher two-layer geometry: 68 px outer toolbar and 48 px visible row. diff --git a/README.md b/README.md index 4a1346f..cc6e7ee 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ - launcher-style ApplicationShell без обязательной нижней витрины; - единый state-controller открытия navigation/content окон; - CMS/Launcher media field: файл, URL, превью и storage-adapter; +- Engine workflow sharing и BIM share-link modal; +- каталог form/confirmation/action/detail/history modal-паттернов; - аудированный общий набор иконок; - инспектор/панель настроек нового поколения; - поведение portal-слоёв, Escape, outside click и фокуса; @@ -51,7 +53,7 @@ npm run build ## Статус -Версия `0.4.x` — пакетный Launcher-пресет шапки и оконного workspace, media/settings patterns, точная геометрия Hub/Engine и центральный каталог иконок. Исходные приложения на первом этапе не изменяются. +Версия `0.5.x` — розовый NODE.DC accent, runtime MP4 stage media, workflow/BIM sharing modals и полная карта оконных паттернов. Исходные приложения на первом этапе не изменяются. Подробности: diff --git a/apps/catalog/package.json b/apps/catalog/package.json index 20961b2..926ac26 100644 --- a/apps/catalog/package.json +++ b/apps/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@nodedc/ui-catalog", - "version": "0.4.0", + "version": "0.5.0", "private": true, "type": "module", "scripts": { @@ -9,8 +9,8 @@ "typecheck": "tsc -b --pretty false" }, "dependencies": { - "@nodedc/ui-core": "0.4.0", - "@nodedc/ui-react": "0.4.0", + "@nodedc/ui-core": "0.5.0", + "@nodedc/ui-react": "0.5.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/apps/catalog/src/CatalogApp.tsx b/apps/catalog/src/CatalogApp.tsx index 21288c8..634abbe 100644 --- a/apps/catalog/src/CatalogApp.tsx +++ b/apps/catalog/src/CatalogApp.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState, type CSSProperties, type ReactNode } from "react"; +import { useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from "react"; import { applyNodedcTheme, type NodedcTheme, type RgbTuple } from "@nodedc/ui-core"; import { AdminNavigationPanel, @@ -24,6 +24,8 @@ import { MediaSourceField, RangeControl, Select, + ShareAccessModal, + ShareLinkModal, SettingsCard, StatusBadge, Switch, @@ -33,22 +35,31 @@ import { Window, WindowFooterActions, type IconName, + type ShareAccessMember, } from "@nodedc/ui-react"; type CatalogSection = "foundation" | "controls" | "media" | "windows" | "modals" | "inspector" | "icons"; const engineAccentStyle = { - "--nodedc-accent-rgb": "148 123 219", - "--nodedc-on-accent-rgb": "247 248 244", + "--nodedc-accent-rgb": "255 47 146", + "--nodedc-on-accent-rgb": "11 17 23", } as CSSProperties; const accents: Array<{ label: string; value: RgbTuple; hex: string }> = [ - { label: "Hub", value: [195, 255, 102], hex: "#c3ff66" }, - { label: "Engine", value: [148, 123, 219], hex: "#947bdb" }, - { label: "BIM", value: [78, 164, 255], hex: "#4ea4ff" }, - { label: "SEO", value: [32, 32, 34], hex: "#202022" }, + { label: "NODE.DC", value: [255, 47, 146], hex: "#ff2f92" }, + { label: "Rose", value: [255, 62, 108], hex: "#ff3e6c" }, + { label: "Blush", value: [255, 108, 175], hex: "#ff6caf" }, + { label: "Magenta", value: [215, 70, 255], hex: "#d746ff" }, ]; +type ShareRole = "viewer" | "editor" | "admin"; + +const shareRoleOptions = [ + { value: "viewer", label: "Просмотр" }, + { value: "editor", label: "Редактор" }, + { value: "admin", label: "Соавтор" }, +] satisfies Array<{ value: ShareRole; label: string }>; + const selectOptions = [ { value: "active", label: "Активен", description: "Сервис доступен пользователям" }, { value: "maintenance", label: "Техработы", description: "Временно недоступен" }, @@ -83,7 +94,7 @@ const sectionDefinitions: Record("dark"); - const [accentHex, setAccentHex] = useState("#c3ff66"); + const [accentHex, setAccentHex] = useState("#ff2f92"); const workspace = useApplicationWorkspace(); const { navigationOpen: guidelineOpen, activeView: activeSection, contentExpanded: panelExpanded } = workspace; const [selectedStatus, setSelectedStatus] = useState<(typeof selectOptions)[number]["value"]>("active"); @@ -216,20 +227,41 @@ export function CatalogApp() { const [glowDistance, setGlowDistance] = useState(105); const [usePortColors, setUsePortColors] = useState(false); const [connectionType, setConnectionType] = useState("spline"); - const [lightColor, setLightColor] = useState("#7a3cff"); + const [lightColor, setLightColor] = useState("#ff2f92"); const [connectionColor, setConnectionColor] = useState("#404040"); - const [fillColor, setFillColor] = useState("#8f83ff"); + const [fillColor, setFillColor] = useState("#ff6caf"); const [strokeColor, setStrokeColor] = useState("#2b2b36"); const [windowOpen, setWindowOpen] = useState(false); const [inspectorOpen, setInspectorOpen] = useState(false); const [createModalOpen, setCreateModalOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); + const [shareAccessOpen, setShareAccessOpen] = useState(false); + const [shareLinkOpen, setShareLinkOpen] = useState(false); + const [contextActionOpen, setContextActionOpen] = useState(false); + const [historyOpen, setHistoryOpen] = useState(false); + const [detailOpen, setDetailOpen] = useState(false); + const [detailExpanded, setDetailExpanded] = useState(false); const [projectName, setProjectName] = useState("NODE.DC Platform"); const [notes, setNotes] = useState("Общий контракт компонентов без доменной логики приложения."); const [mediaSource, setMediaSource] = useState<"file" | "url">("file"); const [mediaUrl, setMediaUrl] = useState(""); - const [mediaFileName, setMediaFileName] = useState("hub_vitrina-mqv7yd7k.mov"); + const [mediaFileName, setMediaFileName] = useState("launcher-stage.mp4"); + const [fileMediaSrc, setFileMediaSrc] = useState("/launcher-stage.mp4"); + const [mediaError, setMediaError] = useState(""); const [mediaVisible, setMediaVisible] = useState(true); + const mediaObjectUrlRef = useRef(null); + const [shareEmail, setShareEmail] = useState(""); + const [shareRole, setShareRole] = useState("editor"); + const [shareMessage, setShareMessage] = useState(""); + const [shareMembers, setShareMembers] = useState>>([ + { id: "owner", name: "DC Constructions", email: "dc@nodedc.ru", role: "admin", roleLabel: "Владелец", immutable: true }, + { id: "editor", name: "Maria Petrova", email: "maria@nodedc.ru", role: "editor", roleLabel: "Редактор" }, + ]); + + const externalMediaSrc = /^(https?:)?\/\//i.test(mediaUrl) && /\.(mp4|webm|mov|m4v)(\?.*)?$/i.test(mediaUrl) + ? mediaUrl + : null; + const stageMediaSrc = mediaSource === "url" && externalMediaSrc ? externalMediaSrc : fileMediaSrc; const accent = useMemo(() => rgbFromHex(accentHex) ?? accents[0].value, [accentHex]); @@ -237,6 +269,38 @@ export function CatalogApp() { applyNodedcTheme(document.documentElement, { theme, accent }); }, [accent, theme]); + useEffect(() => () => { + if (mediaObjectUrlRef.current) URL.revokeObjectURL(mediaObjectUrlRef.current); + }, []); + + const handleStageMediaFile = (file?: File) => { + if (!file) return; + const isMp4 = file.type === "video/mp4" || /\.mp4$/i.test(file.name); + if (!isMp4) { + setMediaError("Для заставки нужен MP4-файл."); + return; + } + if (mediaObjectUrlRef.current) URL.revokeObjectURL(mediaObjectUrlRef.current); + const objectUrl = URL.createObjectURL(file); + mediaObjectUrlRef.current = objectUrl; + setFileMediaSrc(objectUrl); + setMediaFileName(file.name); + setMediaSource("file"); + setMediaVisible(true); + setMediaError(""); + }; + + const inviteShareMember = () => { + const email = shareEmail.trim().toLowerCase(); + if (!email) return; + setShareMembers((current) => [ + ...current.filter((member) => member.email !== email), + { id: email, name: email.split("@")[0], email, role: shareRole, roleLabel: shareRoleOptions.find((item) => item.value === shareRole)?.label }, + ]); + setShareMessage(`Доступ выдан пользователю ${email}`); + setShareEmail(""); + }; + const openSection = (next: string) => { workspace.openView(next as CatalogSection); }; @@ -425,15 +489,18 @@ export function CatalogApp() { source={mediaSource} url={mediaUrl} fileName={mediaFileName} - previewSrc={mediaSource === "url" && mediaUrl ? mediaUrl : "/launcher-stage-poster.png"} - previewKind={mediaSource === "file" ? "image" : undefined} - path="content.videoSrc → assets/uploads/media/component-library" - hint="Адаптер приложения сохраняет файл и возвращает URL; компонент не знает API storage." + previewSrc={stageMediaSrc} + previewKind="video" + accept="video/mp4,.mp4" + path="stage.videoSrc → runtime media source" + hint="Выбранный MP4 сразу подставляется в главное фоновое окно; backend-adapter может сохранить его в storage." + error={mediaError} onSourceChange={setMediaSource} - onUrlChange={setMediaUrl} - onFileChange={(file) => { - if (file) setMediaFileName(file.name); + onUrlChange={(nextUrl) => { + setMediaUrl(nextUrl); + setMediaError(nextUrl && !/\.(mp4|webm|mov|m4v)(\?.*)?$/i.test(nextUrl) ? "Ссылка должна вести на видеофайл." : ""); }} + onFileChange={handleStageMediaFile} /> @@ -474,6 +541,26 @@ export function CatalogApp() {

Destructive-цвет появляется у подтверждения, а не окрашивает всё окно заранее.

+ +

Участники, роли, приглашение и immutable-владелец. API доступа остаётся в Engine.

+ +
+ +

Read-only ссылка, copy-state и единая механика focus/Escape/backdrop.

+ +
+ +

Короткое окно действий над выбранным размером или объектом без отдельной modal-системы.

+ +
+ +

Большое прокручиваемое data-окно; BIM-таблица остаётся доменной композицией внутри общего Window.

+ +
+ +

Рабочая detail-modal с контентом и вложениями, которая может расширяться без смены механики слоя.

+ +
); case "inspector": @@ -553,8 +640,8 @@ export function CatalogApp() { } stage={
-