Release glass material and favicon pipeline v0.6.0
This commit is contained in:
parent
6b42d913ea
commit
d873427fe2
|
|
@ -4,6 +4,10 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#050506" />
|
<meta name="theme-color" content="#050506" />
|
||||||
|
<link rel="icon" href="/favicon/favicon.ico" sizes="any" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon/icon-adaptive.svg" />
|
||||||
|
<link rel="apple-touch-icon" href="/favicon/apple-touch-icon.png" />
|
||||||
|
<link rel="manifest" href="/favicon/manifest.webmanifest.json" />
|
||||||
<title>NODE.DC UI Catalog</title>
|
<title>NODE.DC UI Catalog</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -11,4 +15,3 @@
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@nodedc/ui-catalog",
|
"name": "@nodedc/ui-catalog",
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
"typecheck": "tsc -b --pretty false"
|
"typecheck": "tsc -b --pretty false"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/ui-core": "0.5.1",
|
"@nodedc/ui-core": "0.6.0",
|
||||||
"@nodedc/ui-react": "0.5.1",
|
"@nodedc/ui-react": "0.6.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0"
|
"react-dom": "^19.1.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -0,0 +1,13 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.icon-fav-01 { fill: #333334; } /* Тёмный цвет для светлого таб-бара */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.icon-fav-01 { fill: #FEFEFE; } /* Светлый цвет для тёмного таб-бара */
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
.icon-fav-01 { fill: #000000; } /* Тёмный цвет для светлого таб-бара (для явности) */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<path class="icon-fav-01" d="M17.8738 15.9159L16.0002 18.9718L14.1267 15.9159H17.8738ZM23.6307 12.7864H8.36914L15.9999 25.2308L23.6307 12.7864Z"/>
|
||||||
|
<path class="icon-fav-01" d="M10.9793 19.4872L6.67269 11.5918H25.5344L21.2281 19.4872H25.0581L31.2614 8H0.738281L7.16244 19.4872H10.9793Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 852 B |
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"theme_color": "#eeeff4",
|
||||||
|
"background_color": "#eeeff4",
|
||||||
|
"display": "browser",
|
||||||
|
"scope": "/",
|
||||||
|
"start_url": "/",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icon-adaptive.svg",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"sizes": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icon-192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icon-512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "apple-touch-icon.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "180x180"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useMemo, useRef, 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 { applyGlassMaterial, applyNodedcTheme, defaultGlassMaterial, type GlassMaterialSettings, type NodedcTheme, type RgbTuple } from "@nodedc/ui-core";
|
||||||
import {
|
import {
|
||||||
AdminNavigationPanel,
|
AdminNavigationPanel,
|
||||||
AppHeader,
|
AppHeader,
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
Dropdown,
|
Dropdown,
|
||||||
FieldFrame,
|
FieldFrame,
|
||||||
GlassSurface,
|
GlassSurface,
|
||||||
|
GlassMaterialSurface,
|
||||||
HeaderAvatar,
|
HeaderAvatar,
|
||||||
HeaderNavigation,
|
HeaderNavigation,
|
||||||
HeaderProfile,
|
HeaderProfile,
|
||||||
|
|
@ -39,8 +40,9 @@ import {
|
||||||
type ShareAccessMember,
|
type ShareAccessMember,
|
||||||
type ToolbarPlacement,
|
type ToolbarPlacement,
|
||||||
} from "@nodedc/ui-react";
|
} from "@nodedc/ui-react";
|
||||||
|
import { applyFaviconAssets, createIcoBlob, generateFavicons, type FaviconAssetUrls, type GeneratedFavicon } from "./favicon.js";
|
||||||
|
|
||||||
type CatalogSection = "controls" | "media" | "modals" | "icons";
|
type CatalogSection = "controls" | "media" | "glass" | "modals" | "icons";
|
||||||
|
|
||||||
const accents: Array<{ label: string; value: RgbTuple; hex: string }> = [
|
const accents: Array<{ label: string; value: RgbTuple; hex: string }> = [
|
||||||
{ label: "NODE.DC", value: [255, 47, 146], hex: "#ff2f92" },
|
{ label: "NODE.DC", value: [255, 47, 146], hex: "#ff2f92" },
|
||||||
|
|
@ -83,7 +85,10 @@ interface StoredLayout {
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
logoFileName?: string;
|
logoFileName?: string;
|
||||||
logoFileSrc?: string;
|
logoFileSrc?: string;
|
||||||
|
faviconFileName?: string;
|
||||||
|
faviconAssets?: FaviconAssetUrls;
|
||||||
};
|
};
|
||||||
|
glass?: GlassMaterialSettings;
|
||||||
toolbar?: {
|
toolbar?: {
|
||||||
placement?: ToolbarPlacement;
|
placement?: ToolbarPlacement;
|
||||||
background?: string;
|
background?: string;
|
||||||
|
|
@ -140,14 +145,20 @@ const sectionDefinitions: Record<CatalogSection, { eyebrow: string; title: strin
|
||||||
description: "Общий CMS/Launcher-контракт файла, URL, превью и нейтральных карточек настроек.",
|
description: "Общий CMS/Launcher-контракт файла, URL, превью и нейтральных карточек настроек.",
|
||||||
icon: "image",
|
icon: "image",
|
||||||
},
|
},
|
||||||
|
glass: {
|
||||||
|
eyebrow: "03 / GLASSY MATERIAL",
|
||||||
|
title: "Glassy material",
|
||||||
|
description: "Единый материал только для модальных окон и перемещаемого Inspector.",
|
||||||
|
icon: "apps",
|
||||||
|
},
|
||||||
modals: {
|
modals: {
|
||||||
eyebrow: "03 / MODALS",
|
eyebrow: "04 / MODALS",
|
||||||
title: "Модалки",
|
title: "Модалки",
|
||||||
description: "Полная карта modal-паттернов Launcher, нового Engine и BIM Viewer.",
|
description: "Полная карта modal-паттернов Launcher, нового Engine и BIM Viewer.",
|
||||||
icon: "clipboard",
|
icon: "clipboard",
|
||||||
},
|
},
|
||||||
icons: {
|
icons: {
|
||||||
eyebrow: "04 / ICONS",
|
eyebrow: "05 / ICONS",
|
||||||
title: "Иконки",
|
title: "Иконки",
|
||||||
description: "Канонический общий набор по Launcher, SEO, BIM Viewer и новым участкам Engine.",
|
description: "Канонический общий набор по Launcher, SEO, BIM Viewer и новым участкам Engine.",
|
||||||
icon: "grid",
|
icon: "grid",
|
||||||
|
|
@ -306,6 +317,17 @@ export function CatalogApp() {
|
||||||
const [logoError, setLogoError] = useState("");
|
const [logoError, setLogoError] = useState("");
|
||||||
const logoObjectUrlRef = useRef<string | null>(null);
|
const logoObjectUrlRef = useRef<string | null>(null);
|
||||||
const pendingLogoFileRef = useRef<File | null>(null);
|
const pendingLogoFileRef = useRef<File | null>(null);
|
||||||
|
const [glassMaterial, setGlassMaterial] = useState<GlassMaterialSettings>({ ...defaultGlassMaterial });
|
||||||
|
const [faviconFileName, setFaviconFileName] = useState("icon-adaptive.svg");
|
||||||
|
const [faviconError, setFaviconError] = useState("");
|
||||||
|
const [generatedFavicons, setGeneratedFavicons] = useState<GeneratedFavicon[]>([]);
|
||||||
|
const [faviconAssets, setFaviconAssets] = useState<FaviconAssetUrls>({
|
||||||
|
ico: "/favicon/favicon.ico",
|
||||||
|
apple: "/favicon/apple-touch-icon.png",
|
||||||
|
icon192: "/favicon/icon-192.png",
|
||||||
|
icon512: "/favicon/icon-512.png",
|
||||||
|
});
|
||||||
|
const pendingFaviconIcoRef = useRef<Blob | null>(null);
|
||||||
const [toolbarPlacement, setToolbarPlacement] = useState<ToolbarPlacement>("bottom");
|
const [toolbarPlacement, setToolbarPlacement] = useState<ToolbarPlacement>("bottom");
|
||||||
const [toolbarBg, setToolbarBg] = useState("#111115");
|
const [toolbarBg, setToolbarBg] = useState("#111115");
|
||||||
const [toolbarBorder, setToolbarBorder] = useState("#111117");
|
const [toolbarBorder, setToolbarBorder] = useState("#111117");
|
||||||
|
|
@ -350,6 +372,14 @@ export function CatalogApp() {
|
||||||
document.documentElement.style.setProperty("--nodedc-canvas-soft", material.nestedHex);
|
document.documentElement.style.setProperty("--nodedc-canvas-soft", material.nestedHex);
|
||||||
}, [accent, fieldMaterial, material.fieldOpacity, material.nestedHex, material.panelOpacity, panelMaterial, theme]);
|
}, [accent, fieldMaterial, material.fieldOpacity, material.nestedHex, material.panelOpacity, panelMaterial, theme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applyGlassMaterial(document.documentElement, glassMaterial);
|
||||||
|
}, [glassMaterial]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applyFaviconAssets(faviconAssets);
|
||||||
|
}, [faviconAssets]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
fetch("/api/layout", { cache: "no-store" })
|
fetch("/api/layout", { cache: "no-store" })
|
||||||
|
|
@ -386,7 +416,10 @@ export function CatalogApp() {
|
||||||
if (typeof media.logoUrl === "string") setLogoUrl(media.logoUrl);
|
if (typeof media.logoUrl === "string") setLogoUrl(media.logoUrl);
|
||||||
if (media.logoFileName) setLogoFileName(media.logoFileName);
|
if (media.logoFileName) setLogoFileName(media.logoFileName);
|
||||||
if (media.logoFileSrc) setLogoFileSrc(media.logoFileSrc);
|
if (media.logoFileSrc) setLogoFileSrc(media.logoFileSrc);
|
||||||
|
if (media.faviconFileName) setFaviconFileName(media.faviconFileName);
|
||||||
|
if (media.faviconAssets) setFaviconAssets(media.faviconAssets);
|
||||||
}
|
}
|
||||||
|
if (stored.glass) setGlassMaterial({ ...defaultGlassMaterial, ...stored.glass });
|
||||||
const toolbar = stored.toolbar;
|
const toolbar = stored.toolbar;
|
||||||
if (toolbar) {
|
if (toolbar) {
|
||||||
if (toolbar.placement === "left" || toolbar.placement === "right" || toolbar.placement === "bottom") setToolbarPlacement(toolbar.placement);
|
if (toolbar.placement === "left" || toolbar.placement === "right" || toolbar.placement === "bottom") setToolbarPlacement(toolbar.placement);
|
||||||
|
|
@ -412,7 +445,8 @@ export function CatalogApp() {
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
if (mediaObjectUrlRef.current) URL.revokeObjectURL(mediaObjectUrlRef.current);
|
if (mediaObjectUrlRef.current) URL.revokeObjectURL(mediaObjectUrlRef.current);
|
||||||
if (logoObjectUrlRef.current) URL.revokeObjectURL(logoObjectUrlRef.current);
|
if (logoObjectUrlRef.current) URL.revokeObjectURL(logoObjectUrlRef.current);
|
||||||
}, []);
|
generatedFavicons.forEach((item) => URL.revokeObjectURL(item.previewUrl));
|
||||||
|
}, [generatedFavicons]);
|
||||||
|
|
||||||
const handleStageMediaFile = (file?: File) => {
|
const handleStageMediaFile = (file?: File) => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
@ -449,6 +483,27 @@ export function CatalogApp() {
|
||||||
setLogoError("");
|
setLogoError("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFaviconFile = async (file?: File) => {
|
||||||
|
if (!file) return;
|
||||||
|
const isImage = /^(image\/(png|jpeg|webp))$/i.test(file.type) || /\.(png|jpe?g|webp)$/i.test(file.name);
|
||||||
|
if (!isImage) {
|
||||||
|
setFaviconError("Для генератора нужен PNG, JPG или WEBP.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const next = await generateFavicons(file);
|
||||||
|
setGeneratedFavicons((current) => {
|
||||||
|
current.forEach((item) => URL.revokeObjectURL(item.previewUrl));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
pendingFaviconIcoRef.current = await createIcoBlob(next);
|
||||||
|
setFaviconFileName(file.name);
|
||||||
|
setFaviconError("");
|
||||||
|
} catch {
|
||||||
|
setFaviconError("Не удалось прочитать исходное изображение.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const inviteShareMember = () => {
|
const inviteShareMember = () => {
|
||||||
const email = shareEmail.trim().toLowerCase();
|
const email = shareEmail.trim().toLowerCase();
|
||||||
if (!email) return;
|
if (!email) return;
|
||||||
|
|
@ -506,6 +561,28 @@ export function CatalogApp() {
|
||||||
setLogoFileName(uploaded.fileName);
|
setLogoFileName(uploaded.fileName);
|
||||||
pendingLogoFileRef.current = null;
|
pendingLogoFileRef.current = null;
|
||||||
}
|
}
|
||||||
|
let persistedFaviconAssets = faviconAssets;
|
||||||
|
if (generatedFavicons.length && pendingFaviconIcoRef.current) {
|
||||||
|
const upload = async (blob: Blob, name: string) => {
|
||||||
|
const response = await fetch("/api/layout/media", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "content-type": blob.type || "application/octet-stream", "x-file-name": encodeURIComponent(name) },
|
||||||
|
body: blob,
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error("favicon_upload_failed");
|
||||||
|
return await response.json() as { url: string };
|
||||||
|
};
|
||||||
|
const bySize = new Map(generatedFavicons.map((item) => [item.size, item]));
|
||||||
|
const [ico, apple, icon192, icon512] = await Promise.all([
|
||||||
|
upload(pendingFaviconIcoRef.current, "favicon.ico"),
|
||||||
|
upload(bySize.get(180)!.blob, "apple-touch-icon.png"),
|
||||||
|
upload(bySize.get(192)!.blob, "icon-192.png"),
|
||||||
|
upload(bySize.get(512)!.blob, "icon-512.png"),
|
||||||
|
]);
|
||||||
|
persistedFaviconAssets = { ico: ico.url, apple: apple.url, icon192: icon192.url, icon512: icon512.url };
|
||||||
|
setFaviconAssets(persistedFaviconAssets);
|
||||||
|
pendingFaviconIcoRef.current = null;
|
||||||
|
}
|
||||||
const draft: StoredLayout = {
|
const draft: StoredLayout = {
|
||||||
theme,
|
theme,
|
||||||
accentHex,
|
accentHex,
|
||||||
|
|
@ -532,7 +609,10 @@ export function CatalogApp() {
|
||||||
logoUrl,
|
logoUrl,
|
||||||
logoFileName,
|
logoFileName,
|
||||||
logoFileSrc: persistedLogoFileSrc,
|
logoFileSrc: persistedLogoFileSrc,
|
||||||
|
faviconFileName,
|
||||||
|
faviconAssets: persistedFaviconAssets,
|
||||||
},
|
},
|
||||||
|
glass: glassMaterial,
|
||||||
toolbar: {
|
toolbar: {
|
||||||
placement: toolbarPlacement,
|
placement: toolbarPlacement,
|
||||||
background: toolbarBg,
|
background: toolbarBg,
|
||||||
|
|
@ -785,6 +865,53 @@ export function CatalogApp() {
|
||||||
onFileChange={handleLogoFile}
|
onFileChange={handleLogoFile}
|
||||||
/>
|
/>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
<SettingsCard
|
||||||
|
eyebrow="BROWSER BRAND"
|
||||||
|
title="Favicon generator"
|
||||||
|
description="Один квадратный исходник превращается в ICO, Apple Touch и PWA-набор; общая Save сохраняет набор на сервере и применяет его к приложению."
|
||||||
|
>
|
||||||
|
<div className="catalog-favicon-generator">
|
||||||
|
<label className="nodedc-media-file__button catalog-favicon-generator__upload">
|
||||||
|
Выбрать и сгенерировать
|
||||||
|
<input type="file" accept="image/png,image/jpeg,image/webp,.png,.jpg,.jpeg,.webp" onChange={(event) => { void handleFaviconFile(event.target.files?.[0]); }} />
|
||||||
|
</label>
|
||||||
|
<span className="catalog-favicon-generator__name">{faviconFileName}</span>
|
||||||
|
<div className="catalog-favicon-grid">
|
||||||
|
{[16, 32, 64, 180, 192, 512].map((size) => {
|
||||||
|
const generated = generatedFavicons.find((item) => item.size === size);
|
||||||
|
const fallback = size === 180 ? faviconAssets.apple : size === 192 ? faviconAssets.icon192 : size === 512 ? faviconAssets.icon512 : faviconAssets.ico;
|
||||||
|
return <figure key={size}><span><img src={generated?.previewUrl ?? fallback} alt="" /></span><figcaption>{size} × {size}</figcaption></figure>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{faviconError ? <span className="nodedc-media-field__error" role="alert">{faviconError}</span> : null}
|
||||||
|
</div>
|
||||||
|
</SettingsCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "glass":
|
||||||
|
return (
|
||||||
|
<div className="catalog-glass-lab">
|
||||||
|
<section className="catalog-glass-stage">
|
||||||
|
<video key={stageMediaSrc} autoPlay muted loop playsInline poster="/launcher-stage-poster.png" aria-hidden="true"><source src={stageMediaSrc} type="video/mp4" /></video>
|
||||||
|
<div className="catalog-glass-stage__shade" />
|
||||||
|
<GlassMaterialSurface className="catalog-glass-stage__sample">
|
||||||
|
<span>CANONICAL GLASS</span>
|
||||||
|
<h2>Модальное окно и Inspector</h2>
|
||||||
|
<p>Фон остаётся различимым, но текст и контролы сохраняют контраст. Этот материал не применяется к обычным панелям приложения.</p>
|
||||||
|
<div className="catalog-inline"><TextField label="Пример поля" defaultValue="NODE.DC" /><Button variant="primary" shape="pill">Применить</Button></div>
|
||||||
|
</GlassMaterialSurface>
|
||||||
|
</section>
|
||||||
|
<SettingsCard eyebrow="ENGINE / GLASS V4" title="Параметры материала" description="Общий token-контракт ui-core; изменения сразу видны на preview, модалках и Inspector.">
|
||||||
|
<div className="catalog-glass-controls">
|
||||||
|
<ControlRow label="Цвет тонировки"><ColorField value={glassMaterial.tintHex} onChange={(tintHex) => setGlassMaterial((current) => ({ ...current, tintHex }))} /></ControlRow>
|
||||||
|
<RangeControl label="Прозрачность" value={glassMaterial.tintOpacity} min={0} max={96} formatValue={(value) => `${value}%`} onChange={(tintOpacity) => setGlassMaterial((current) => ({ ...current, tintOpacity }))} />
|
||||||
|
<RangeControl label="Blur" value={glassMaterial.blur} min={0} max={120} formatValue={(value) => `${value}px`} onChange={(blur) => setGlassMaterial((current) => ({ ...current, blur }))} />
|
||||||
|
<RangeControl label="Saturation" value={glassMaterial.saturation} min={70} max={220} formatValue={(value) => `${value}%`} onChange={(saturation) => setGlassMaterial((current) => ({ ...current, saturation }))} />
|
||||||
|
<RangeControl label="Brightness" value={glassMaterial.brightness} min={70} max={160} formatValue={(value) => `${value}%`} onChange={(brightness) => setGlassMaterial((current) => ({ ...current, brightness }))} />
|
||||||
|
<RangeControl label="Контур" value={glassMaterial.outlineOpacity} min={0} max={40} formatValue={(value) => `${value}%`} onChange={(outlineOpacity) => setGlassMaterial((current) => ({ ...current, outlineOpacity }))} />
|
||||||
|
<RangeControl label="Тень" value={glassMaterial.shadowOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(shadowOpacity) => setGlassMaterial((current) => ({ ...current, shadowOpacity }))} />
|
||||||
|
</div>
|
||||||
|
</SettingsCard>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case "modals":
|
case "modals":
|
||||||
|
|
@ -933,7 +1060,7 @@ export function CatalogApp() {
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<span className="nodedc-admin-panel__nav-icon" aria-hidden="true"><Icon name="shield" /></span>
|
<span className="nodedc-admin-panel__nav-icon" aria-hidden="true"><Icon name="shield" /></span>
|
||||||
<span>Design System 0.5.1</span>
|
<span>Design System 0.6.0</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
export const faviconSizes = [16, 32, 64, 180, 192, 512] as const;
|
||||||
|
|
||||||
|
export type GeneratedFavicon = {
|
||||||
|
size: (typeof faviconSizes)[number];
|
||||||
|
blob: Blob;
|
||||||
|
previewUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function canvasBlob(canvas: HTMLCanvasElement) {
|
||||||
|
return new Promise<Blob>((resolve, reject) => canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("favicon_canvas_failed")), "image/png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateFavicons(file: File): Promise<GeneratedFavicon[]> {
|
||||||
|
const bitmap = await createImageBitmap(file);
|
||||||
|
try {
|
||||||
|
return await Promise.all(faviconSizes.map(async (size) => {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
if (!context) throw new Error("favicon_context_failed");
|
||||||
|
context.imageSmoothingEnabled = true;
|
||||||
|
context.imageSmoothingQuality = "high";
|
||||||
|
const scale = Math.min(size / bitmap.width, size / bitmap.height);
|
||||||
|
const width = bitmap.width * scale;
|
||||||
|
const height = bitmap.height * scale;
|
||||||
|
context.drawImage(bitmap, (size - width) / 2, (size - height) / 2, width, height);
|
||||||
|
const blob = await canvasBlob(canvas);
|
||||||
|
return { size, blob, previewUrl: URL.createObjectURL(blob) };
|
||||||
|
}));
|
||||||
|
} finally {
|
||||||
|
bitmap.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createIcoBlob(images: GeneratedFavicon[]) {
|
||||||
|
const selected = images.filter((item) => item.size === 16 || item.size === 32 || item.size === 64);
|
||||||
|
const payloads = await Promise.all(selected.map((item) => item.blob.arrayBuffer()));
|
||||||
|
const headerSize = 6 + selected.length * 16;
|
||||||
|
const totalSize = headerSize + payloads.reduce((sum, payload) => sum + payload.byteLength, 0);
|
||||||
|
const buffer = new ArrayBuffer(totalSize);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
view.setUint16(0, 0, true);
|
||||||
|
view.setUint16(2, 1, true);
|
||||||
|
view.setUint16(4, selected.length, true);
|
||||||
|
let offset = headerSize;
|
||||||
|
selected.forEach((item, index) => {
|
||||||
|
const entry = 6 + index * 16;
|
||||||
|
const payload = payloads[index];
|
||||||
|
view.setUint8(entry, item.size);
|
||||||
|
view.setUint8(entry + 1, item.size);
|
||||||
|
view.setUint8(entry + 2, 0);
|
||||||
|
view.setUint8(entry + 3, 0);
|
||||||
|
view.setUint16(entry + 4, 1, true);
|
||||||
|
view.setUint16(entry + 6, 32, true);
|
||||||
|
view.setUint32(entry + 8, payload.byteLength, true);
|
||||||
|
view.setUint32(entry + 12, offset, true);
|
||||||
|
new Uint8Array(buffer, offset, payload.byteLength).set(new Uint8Array(payload));
|
||||||
|
offset += payload.byteLength;
|
||||||
|
});
|
||||||
|
return new Blob([buffer], { type: "image/x-icon" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FaviconAssetUrls = { ico?: string; apple?: string; icon192?: string; icon512?: string };
|
||||||
|
|
||||||
|
export function applyFaviconAssets(assets?: FaviconAssetUrls) {
|
||||||
|
if (!assets) return;
|
||||||
|
const setLink = (id: string, rel: string, href?: string, sizes?: string) => {
|
||||||
|
if (!href) return;
|
||||||
|
let link = document.querySelector<HTMLLinkElement>(`link[data-nodedc-favicon="${id}"]`);
|
||||||
|
if (!link) {
|
||||||
|
link = document.createElement("link");
|
||||||
|
link.dataset.nodedcFavicon = id;
|
||||||
|
link.rel = rel;
|
||||||
|
document.head.append(link);
|
||||||
|
}
|
||||||
|
link.href = href;
|
||||||
|
if (sizes) link.sizes = sizes;
|
||||||
|
};
|
||||||
|
setLink("ico", "icon", assets.ico);
|
||||||
|
setLink("apple", "apple-touch-icon", assets.apple, "180x180");
|
||||||
|
setLink("192", "icon", assets.icon192, "192x192");
|
||||||
|
setLink("512", "icon", assets.icon512, "512x512");
|
||||||
|
}
|
||||||
|
|
@ -55,6 +55,133 @@ textarea {
|
||||||
opacity: 0.92;
|
opacity: 0.92;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalog-favicon-generator {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.9rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-favicon-generator__upload {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-favicon-generator__upload input {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-favicon-generator__name {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--nodedc-text-secondary);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-favicon-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-template-columns: repeat(6, minmax(5rem, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-favicon-grid figure {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
place-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: var(--nodedc-radius-control);
|
||||||
|
background: var(--nodedc-canvas-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-favicon-grid figure > span {
|
||||||
|
display: grid;
|
||||||
|
width: 3.25rem;
|
||||||
|
height: 3.25rem;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
background: var(--nodedc-field-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-favicon-grid img {
|
||||||
|
width: min(2.5rem, 100%);
|
||||||
|
height: min(2.5rem, 100%);
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-favicon-grid figcaption {
|
||||||
|
color: var(--nodedc-text-muted);
|
||||||
|
font-size: var(--nodedc-font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-glass-lab {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.55fr) minmax(19rem, 0.75fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-glass-stage {
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
display: grid;
|
||||||
|
min-height: 38rem;
|
||||||
|
place-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--nodedc-radius-panel);
|
||||||
|
background: #09090b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-glass-stage > video,
|
||||||
|
.catalog-glass-stage__shade {
|
||||||
|
position: absolute;
|
||||||
|
z-index: -2;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-glass-stage__shade {
|
||||||
|
z-index: -1;
|
||||||
|
background: linear-gradient(135deg, rgba(0, 0, 0, 0.16), rgba(0, 0, 0, 0.44));
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-glass-stage__sample {
|
||||||
|
display: grid;
|
||||||
|
width: min(34rem, calc(100% - 3rem));
|
||||||
|
gap: 0.8rem;
|
||||||
|
padding: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-glass-stage__sample > span {
|
||||||
|
color: var(--nodedc-text-muted);
|
||||||
|
font-size: var(--nodedc-font-size-xs);
|
||||||
|
font-weight: var(--nodedc-font-weight-strong);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-glass-stage__sample h2,
|
||||||
|
.catalog-glass-stage__sample p { margin: 0; }
|
||||||
|
.catalog-glass-stage__sample p { color: var(--nodedc-text-secondary); line-height: 1.55; }
|
||||||
|
.catalog-glass-stage__sample .catalog-inline { align-items: end; }
|
||||||
|
.catalog-glass-stage__sample .nodedc-field { flex: 1; }
|
||||||
|
|
||||||
|
.catalog-glass-controls {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.catalog-glass-lab { grid-template-columns: 1fr; }
|
||||||
|
.catalog-favicon-grid { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
}
|
||||||
|
|
||||||
.catalog-launcher-stage__shade {
|
.catalog-launcher-stage__shade {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background:
|
background:
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ Core не зависит от React.
|
||||||
|
|
||||||
Каталог является исполняемой документацией. Компонент не считается зафиксированным, если его состояния нельзя увидеть и проверить в каталоге.
|
Каталог является исполняемой документацией. Компонент не считается зафиксированным, если его состояния нельзя увидеть и проверить в каталоге.
|
||||||
|
|
||||||
Кнопка Save в `ApplicationPanel` записывает один server-side catalog layout: тему, обе material-схемы, Environment controls, Toolbar, stage-media и знак приложения. Browser localStorage не является источником истины.
|
Кнопка Save в `ApplicationPanel` записывает один server-side catalog layout: тему, обе material-схемы, Glass V4, Environment controls, Toolbar, stage-media, знак приложения и сгенерированный favicon-набор. Browser localStorage не является источником истины.
|
||||||
|
|
||||||
## Граница shared/domain
|
## Граница shared/domain
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
|
|
||||||
Material rim допустим только как часть floating glass. Жёсткая цветная рамка, случайный browser outline или debug border не являются rim.
|
Material rim допустим только как часть floating glass. Жёсткая цветная рамка, случайный browser outline или debug border не являются rim.
|
||||||
|
|
||||||
|
`GlassMaterialSurface` — отдельный переносимый контракт Engine Glass V4. Он централизует tint/opacity, blur, saturation, brightness, gradient rim и shadow. Его используют только modal `Window` и modeless draggable Inspector; обычные Launcher/SEO панели остаются непрозрачными. Настройки применяются через `applyGlassMaterial`, поэтому consumer не пересобирает CSS материала вручную.
|
||||||
|
|
||||||
## Button и IconButton
|
## Button и IconButton
|
||||||
|
|
||||||
Button используется для всех текстовых действий. IconButton — для действия без текста.
|
Button используется для всех текстовых действий. IconButton — для действия без текста.
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "nodedc-design-guideline",
|
"name": "nodedc-design-guideline",
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "nodedc-design-guideline",
|
"name": "nodedc-design-guideline",
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"apps/*"
|
"apps/*"
|
||||||
|
|
@ -25,10 +25,10 @@
|
||||||
},
|
},
|
||||||
"apps/catalog": {
|
"apps/catalog": {
|
||||||
"name": "@nodedc/ui-catalog",
|
"name": "@nodedc/ui-catalog",
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/ui-core": "0.5.1",
|
"@nodedc/ui-core": "0.6.0",
|
||||||
"@nodedc/ui-react": "0.5.1",
|
"@nodedc/ui-react": "0.6.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0"
|
"react-dom": "^19.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -1888,27 +1888,27 @@
|
||||||
},
|
},
|
||||||
"packages/tokens": {
|
"packages/tokens": {
|
||||||
"name": "@nodedc/tokens",
|
"name": "@nodedc/tokens",
|
||||||
"version": "0.5.1"
|
"version": "0.6.0"
|
||||||
},
|
},
|
||||||
"packages/ui-core": {
|
"packages/ui-core": {
|
||||||
"name": "@nodedc/ui-core",
|
"name": "@nodedc/ui-core",
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/tokens": "0.5.1"
|
"@nodedc/tokens": "0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/ui-dom": {
|
"packages/ui-dom": {
|
||||||
"name": "@nodedc/ui-dom",
|
"name": "@nodedc/ui-dom",
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/ui-core": "0.5.1"
|
"@nodedc/ui-core": "0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/ui-react": {
|
"packages/ui-react": {
|
||||||
"name": "@nodedc/ui-react",
|
"name": "@nodedc/ui-react",
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/ui-core": "0.5.1",
|
"@nodedc/ui-core": "0.6.0",
|
||||||
"lucide-react": "^0.468.0"
|
"lucide-react": "^0.468.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "nodedc-design-guideline",
|
"name": "nodedc-design-guideline",
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Canonical NODE.DC design system, reusable UI packages and living component catalog.",
|
"description": "Canonical NODE.DC design system, reusable UI packages and living component catalog.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@nodedc/tokens",
|
"name": "@nodedc/tokens",
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": ["tokens.json", "tokens.css", "themes.css"],
|
"files": [
|
||||||
|
"tokens.json",
|
||||||
|
"tokens.css",
|
||||||
|
"themes.css"
|
||||||
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
"./tokens.json": "./tokens.json",
|
"./tokens.json": "./tokens.json",
|
||||||
"./tokens.css": "./tokens.css",
|
"./tokens.css": "./tokens.css",
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,10 @@
|
||||||
--nodedc-primary-action-hover: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 84%, white);
|
--nodedc-primary-action-hover: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 84%, white);
|
||||||
--nodedc-primary-action-color: rgb(var(--nodedc-on-accent-rgb));
|
--nodedc-primary-action-color: rgb(var(--nodedc-on-accent-rgb));
|
||||||
--nodedc-primary-action-shadow: 0 12px 28px rgb(var(--nodedc-accent-rgb) / 0.2);
|
--nodedc-primary-action-shadow: 0 12px 28px rgb(var(--nodedc-accent-rgb) / 0.2);
|
||||||
--nodedc-modal-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.014)), rgba(7, 7, 10, 0.54);
|
--nodedc-modal-glass-tint-rgb: 11 11 14;
|
||||||
--nodedc-modal-shadow: 0 34px 120px rgba(0, 0, 0, 0.64), 0 10px 32px rgba(0, 0, 0, 0.32), inset 0 1px 0 rgba(255, 255, 255, 0.045);
|
--nodedc-modal-glass-tint-opacity: 0.54;
|
||||||
|
--nodedc-modal-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.068), rgba(255, 255, 255, 0.02)), rgb(var(--nodedc-modal-glass-tint-rgb) / var(--nodedc-modal-glass-tint-opacity));
|
||||||
|
--nodedc-modal-shadow: 0 34px 120px rgb(0 0 0 / var(--nodedc-modal-glass-shadow-opacity)), 0 10px 32px rgba(0, 0, 0, 0.32), inset 0 1px 0 rgba(255, 255, 255, 0.045);
|
||||||
--nodedc-floating-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.012)), rgba(10, 10, 13, 0.94);
|
--nodedc-floating-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.012)), rgba(10, 10, 13, 0.94);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,7 +122,9 @@
|
||||||
--nodedc-primary-action-hover: rgba(8, 8, 10, 0.98);
|
--nodedc-primary-action-hover: rgba(8, 8, 10, 0.98);
|
||||||
--nodedc-primary-action-color: rgba(247, 248, 244, 0.96);
|
--nodedc-primary-action-color: rgba(247, 248, 244, 0.96);
|
||||||
--nodedc-primary-action-shadow: none;
|
--nodedc-primary-action-shadow: none;
|
||||||
--nodedc-modal-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(255, 255, 255, 0.36)), rgba(255, 255, 255, 0.72);
|
--nodedc-modal-glass-tint-rgb: 255 255 255;
|
||||||
--nodedc-modal-shadow: 0 24px 72px rgba(0, 0, 0, 0.24), inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
--nodedc-modal-glass-tint-opacity: 0.72;
|
||||||
|
--nodedc-modal-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(255, 255, 255, 0.36)), rgb(var(--nodedc-modal-glass-tint-rgb) / var(--nodedc-modal-glass-tint-opacity));
|
||||||
|
--nodedc-modal-shadow: 0 24px 72px rgb(0 0 0 / var(--nodedc-modal-glass-shadow-light-opacity)), inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||||
--nodedc-floating-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 255, 255, 0.72)), rgba(255, 255, 255, 0.94);
|
--nodedc-floating-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 255, 255, 0.72)), rgba(255, 255, 255, 0.94);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
--nodedc-header-control-height: 2.78rem;
|
--nodedc-header-control-height: 2.78rem;
|
||||||
--nodedc-header-logo-width: 7.25rem;
|
--nodedc-header-logo-width: 7.25rem;
|
||||||
--nodedc-header-logo-height: 1.79rem;
|
--nodedc-header-logo-height: 1.79rem;
|
||||||
--nodedc-inspector-width: 24.375rem;
|
--nodedc-inspector-width: 22.125rem;
|
||||||
--nodedc-inspector-inner-width: 20.625rem;
|
--nodedc-inspector-inner-width: 20.625rem;
|
||||||
--nodedc-inspector-label-width: 9.625rem;
|
--nodedc-inspector-label-width: 9.625rem;
|
||||||
--nodedc-inspector-control-width: 10.125rem;
|
--nodedc-inspector-control-width: 10.125rem;
|
||||||
|
|
@ -39,6 +39,12 @@
|
||||||
--nodedc-blur-control: 24px;
|
--nodedc-blur-control: 24px;
|
||||||
--nodedc-blur-panel: 44px;
|
--nodedc-blur-panel: 44px;
|
||||||
--nodedc-blur-modal: 64px;
|
--nodedc-blur-modal: 64px;
|
||||||
|
--nodedc-modal-glass-saturation: 1.38;
|
||||||
|
--nodedc-modal-glass-brightness: 1.06;
|
||||||
|
--nodedc-modal-glass-outline-opacity: 0.11;
|
||||||
|
--nodedc-modal-glass-outline-soft-opacity: 0.0495;
|
||||||
|
--nodedc-modal-glass-shadow-opacity: 0.64;
|
||||||
|
--nodedc-modal-glass-shadow-light-opacity: 0.24;
|
||||||
--nodedc-blur-dropdown: 44px;
|
--nodedc-blur-dropdown: 44px;
|
||||||
|
|
||||||
--nodedc-duration-fast: 120ms;
|
--nodedc-duration-fast: 120ms;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "@nodedc/ui-core",
|
"name": "@nodedc/ui-core",
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": ["dist", "styles.css"],
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"styles.css"
|
||||||
|
],
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
@ -17,6 +20,6 @@
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/tokens": "0.5.1"
|
"@nodedc/tokens": "0.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
export interface GlassMaterialSettings {
|
||||||
|
tintHex: string;
|
||||||
|
tintOpacity: number;
|
||||||
|
blur: number;
|
||||||
|
saturation: number;
|
||||||
|
brightness: number;
|
||||||
|
outlineOpacity: number;
|
||||||
|
shadowOpacity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultGlassMaterial: GlassMaterialSettings = {
|
||||||
|
tintHex: "#0b0b0e",
|
||||||
|
tintOpacity: 54,
|
||||||
|
blur: 64,
|
||||||
|
saturation: 138,
|
||||||
|
brightness: 106,
|
||||||
|
outlineOpacity: 11,
|
||||||
|
shadowOpacity: 64,
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeHex(value: string) {
|
||||||
|
const match = /^#?([0-9a-f]{6})$/i.exec(value.trim());
|
||||||
|
return match ? `#${match[1].toLowerCase()}` : defaultGlassMaterial.tintHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number) {
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGlassMaterialVariables(settings: GlassMaterialSettings): Record<string, string> {
|
||||||
|
const tintHex = normalizeHex(settings.tintHex);
|
||||||
|
const red = Number.parseInt(tintHex.slice(1, 3), 16);
|
||||||
|
const green = Number.parseInt(tintHex.slice(3, 5), 16);
|
||||||
|
const blue = Number.parseInt(tintHex.slice(5, 7), 16);
|
||||||
|
return {
|
||||||
|
"--nodedc-modal-glass-tint-rgb": `${red} ${green} ${blue}`,
|
||||||
|
"--nodedc-modal-glass-tint-opacity": String(clamp(settings.tintOpacity, 0, 100) / 100),
|
||||||
|
"--nodedc-blur-modal": `${clamp(settings.blur, 0, 120)}px`,
|
||||||
|
"--nodedc-modal-glass-saturation": String(clamp(settings.saturation, 70, 220) / 100),
|
||||||
|
"--nodedc-modal-glass-brightness": String(clamp(settings.brightness, 70, 160) / 100),
|
||||||
|
"--nodedc-modal-glass-outline-opacity": String(clamp(settings.outlineOpacity, 0, 40) / 100),
|
||||||
|
"--nodedc-modal-glass-outline-soft-opacity": String((clamp(settings.outlineOpacity, 0, 40) / 100) * 0.45),
|
||||||
|
"--nodedc-modal-glass-shadow-opacity": String(clamp(settings.shadowOpacity, 0, 100) / 100),
|
||||||
|
"--nodedc-modal-glass-shadow-light-opacity": String((clamp(settings.shadowOpacity, 0, 100) / 100) * 0.375),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyGlassMaterial(target: HTMLElement, settings: GlassMaterialSettings) {
|
||||||
|
for (const [name, value] of Object.entries(createGlassMaterialVariables(settings))) {
|
||||||
|
target.style.setProperty(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export * from "./accent.js";
|
export * from "./accent.js";
|
||||||
export * from "./floating.js";
|
export * from "./floating.js";
|
||||||
|
export * from "./glass.js";
|
||||||
export * from "./theme.js";
|
export * from "./theme.js";
|
||||||
export * from "./toolbar.js";
|
export * from "./toolbar.js";
|
||||||
|
|
|
||||||
|
|
@ -1914,8 +1914,8 @@ textarea.nodedc-field__control {
|
||||||
background: var(--nodedc-modal-surface);
|
background: var(--nodedc-modal-surface);
|
||||||
color: var(--nodedc-text-primary);
|
color: var(--nodedc-text-primary);
|
||||||
box-shadow: var(--nodedc-modal-shadow);
|
box-shadow: var(--nodedc-modal-shadow);
|
||||||
backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(1.38) brightness(1.06);
|
backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(var(--nodedc-modal-glass-saturation)) brightness(var(--nodedc-modal-glass-brightness));
|
||||||
-webkit-backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(1.38) brightness(1.06);
|
-webkit-backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(var(--nodedc-modal-glass-saturation)) brightness(var(--nodedc-modal-glass-brightness));
|
||||||
animation: nodedc-window-in var(--nodedc-duration-normal) var(--nodedc-ease-standard);
|
animation: nodedc-window-in var(--nodedc-duration-normal) var(--nodedc-ease-standard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1931,13 +1931,40 @@ textarea.nodedc-field__control {
|
||||||
width: min(var(--nodedc-inspector-width), calc(100vw - 3rem));
|
width: min(var(--nodedc-inspector-width), calc(100vw - 3rem));
|
||||||
max-height: calc(100vh - 7.25rem);
|
max-height: calc(100vh - 7.25rem);
|
||||||
border-radius: var(--nodedc-radius-panel);
|
border-radius: var(--nodedc-radius-panel);
|
||||||
background: var(--nodedc-app-panel-surface);
|
background: var(--nodedc-modal-surface);
|
||||||
box-shadow: var(--nodedc-app-panel-shadow);
|
box-shadow: var(--nodedc-modal-shadow);
|
||||||
backdrop-filter: none;
|
backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(var(--nodedc-modal-glass-saturation)) brightness(var(--nodedc-modal-glass-brightness));
|
||||||
-webkit-backdrop-filter: none;
|
-webkit-backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(var(--nodedc-modal-glass-saturation)) brightness(var(--nodedc-modal-glass-brightness));
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-window::before,
|
||||||
|
.nodedc-glass-material::before {
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
padding: 1px;
|
||||||
|
background: linear-gradient(140deg, rgb(255 255 255 / var(--nodedc-modal-glass-outline-opacity)), transparent 42%, rgb(255 255 255 / var(--nodedc-modal-glass-outline-soft-opacity)));
|
||||||
|
content: "";
|
||||||
|
pointer-events: none;
|
||||||
|
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-glass-material {
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--nodedc-radius-modal);
|
||||||
|
background: var(--nodedc-modal-surface);
|
||||||
|
color: var(--nodedc-text-primary);
|
||||||
|
box-shadow: var(--nodedc-modal-shadow);
|
||||||
|
backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(var(--nodedc-modal-glass-saturation)) brightness(var(--nodedc-modal-glass-brightness));
|
||||||
|
-webkit-backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(var(--nodedc-modal-glass-saturation)) brightness(var(--nodedc-modal-glass-brightness));
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-window[data-placement="end"] .nodedc-window__head {
|
.nodedc-window[data-placement="end"] .nodedc-window__head {
|
||||||
min-height: 4.875rem;
|
min-height: 4.875rem;
|
||||||
padding: 1.125rem 0.75rem 0.875rem 1.625rem;
|
padding: 1.125rem 0.75rem 0.875rem 1.625rem;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "@nodedc/ui-dom",
|
"name": "@nodedc/ui-dom",
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": ["dist"],
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
@ -16,6 +18,6 @@
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/ui-core": "0.5.1"
|
"@nodedc/ui-core": "0.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "@nodedc/ui-react",
|
"name": "@nodedc/ui-react",
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": ["dist"],
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
@ -16,7 +18,7 @@
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/ui-core": "0.5.1",
|
"@nodedc/ui-core": "0.6.0",
|
||||||
"lucide-react": "^0.468.0"
|
"lucide-react": "^0.468.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -35,3 +35,11 @@ export function GlassSurface({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GlassMaterialSurfaceProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Canonical modal/Inspector material. General application panels must not use it. */
|
||||||
|
export function GlassMaterialSurface({ children, className, ...props }: GlassMaterialSurfaceProps) {
|
||||||
|
return <div className={cn("nodedc-glass-material", className)} {...props}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,16 @@
|
||||||
"id": "glass-surface",
|
"id": "glass-surface",
|
||||||
"status": "baseline",
|
"status": "baseline",
|
||||||
"package": "@nodedc/ui-react",
|
"package": "@nodedc/ui-react",
|
||||||
"exports": ["GlassSurface"],
|
"exports": ["GlassSurface", "GlassMaterialSurface"],
|
||||||
"domContract": ["nodedc-glass", "nodedc-material-rim"],
|
"domContract": ["nodedc-glass", "nodedc-glass-material", "nodedc-material-rim"],
|
||||||
"summary": "Canonical matte/glossy glass surface for cards, panels and window shells.",
|
"summary": "Matte application surfaces plus the canonical Engine V4 glass material for modal and Inspector shells.",
|
||||||
"anatomy": ["theme-provided surface", "optional material rim", "backdrop blur", "content"],
|
"anatomy": ["theme-provided surface", "modal tint", "gradient rim", "backdrop blur/saturation/brightness", "content"],
|
||||||
"variants": ["default", "strong", "soft"],
|
"variants": ["default", "strong", "soft"],
|
||||||
"rules": [
|
"rules": [
|
||||||
"Geometry is invariant across application themes.",
|
"Geometry is invariant across application themes.",
|
||||||
"A material rim is a glass highlight, not a hard product-colored border.",
|
"A material rim is a glass highlight, not a hard product-colored border.",
|
||||||
"Nested surfaces must use a softer tone to avoid card-inside-card noise."
|
"Nested surfaces must use a softer tone to avoid card-inside-card noise.",
|
||||||
|
"GlassyMaterialSurface is restricted to modal Windows and the draggable Inspector."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"schemaVersion": "1.0.0",
|
"schemaVersion": "1.0.0",
|
||||||
"libraryVersion": "0.5.1",
|
"libraryVersion": "0.6.0",
|
||||||
"name": "NODE.DC Design System",
|
"name": "NODE.DC Design System",
|
||||||
"purpose": "Canonical code-level UI system for current and future NODE.DC applications.",
|
"purpose": "Canonical code-level UI system for current and future NODE.DC applications.",
|
||||||
"packages": [
|
"packages": [
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ const mimeTypes = {
|
||||||
".css": "text/css; charset=utf-8",
|
".css": "text/css; charset=utf-8",
|
||||||
".gif": "image/gif",
|
".gif": "image/gif",
|
||||||
".html": "text/html; charset=utf-8",
|
".html": "text/html; charset=utf-8",
|
||||||
|
".ico": "image/x-icon",
|
||||||
".jpg": "image/jpeg",
|
".jpg": "image/jpeg",
|
||||||
".jpeg": "image/jpeg",
|
".jpeg": "image/jpeg",
|
||||||
".js": "text/javascript; charset=utf-8",
|
".js": "text/javascript; charset=utf-8",
|
||||||
|
|
@ -48,9 +49,10 @@ async function readJsonBody(request, maxBytes = 2 * 1024 * 1024) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeUploadName(name, contentType = "") {
|
function safeUploadName(name, contentType = "") {
|
||||||
const allowed = new Set([".gif", ".jpeg", ".jpg", ".m4v", ".mov", ".mp4", ".png", ".webm", ".webp"]);
|
const allowed = new Set([".gif", ".ico", ".jpeg", ".jpg", ".m4v", ".mov", ".mp4", ".png", ".webm", ".webp"]);
|
||||||
const contentTypeExtensions = {
|
const contentTypeExtensions = {
|
||||||
"image/gif": ".gif",
|
"image/gif": ".gif",
|
||||||
|
"image/x-icon": ".ico",
|
||||||
"image/jpeg": ".jpg",
|
"image/jpeg": ".jpg",
|
||||||
"image/png": ".png",
|
"image/png": ".png",
|
||||||
"image/webp": ".webp",
|
"image/webp": ".webp",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue