From d873427fe22c93e0644cbdced8751180818eb1df Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sat, 11 Jul 2026 12:19:44 +0300 Subject: [PATCH] Release glass material and favicon pipeline v0.6.0 --- apps/catalog/index.html | 5 +- apps/catalog/package.json | 6 +- .../public/favicon/apple-touch-icon.png | Bin 0 -> 4142 bytes apps/catalog/public/favicon/favicon.ico | Bin 0 -> 16958 bytes apps/catalog/public/favicon/icon-192.png | Bin 0 -> 4386 bytes apps/catalog/public/favicon/icon-512.png | Bin 0 -> 13039 bytes apps/catalog/public/favicon/icon-adaptive.svg | 13 ++ .../public/favicon/manifest.webmanifest.json | 29 ++++ apps/catalog/src/CatalogApp.tsx | 139 +++++++++++++++++- apps/catalog/src/favicon.ts | 84 +++++++++++ apps/catalog/src/styles.css | 127 ++++++++++++++++ docs/ARCHITECTURE.md | 2 +- docs/COMPONENTS.md | 2 + package-lock.json | 24 +-- package.json | 2 +- packages/tokens/package.json | 8 +- packages/tokens/themes.css | 12 +- packages/tokens/tokens.css | 8 +- packages/ui-core/package.json | 9 +- packages/ui-core/src/glass.ts | 52 +++++++ packages/ui-core/src/index.ts | 1 + packages/ui-core/styles.css | 39 ++++- packages/ui-dom/package.json | 8 +- packages/ui-react/package.json | 8 +- packages/ui-react/src/Glass.tsx | 8 + registry/components.json | 11 +- registry/registry.json | 2 +- server/catalog-server.mjs | 4 +- 28 files changed, 550 insertions(+), 53 deletions(-) create mode 100644 apps/catalog/public/favicon/apple-touch-icon.png create mode 100644 apps/catalog/public/favicon/favicon.ico create mode 100644 apps/catalog/public/favicon/icon-192.png create mode 100644 apps/catalog/public/favicon/icon-512.png create mode 100644 apps/catalog/public/favicon/icon-adaptive.svg create mode 100644 apps/catalog/public/favicon/manifest.webmanifest.json create mode 100644 apps/catalog/src/favicon.ts create mode 100644 packages/ui-core/src/glass.ts diff --git a/apps/catalog/index.html b/apps/catalog/index.html index 1d72386..84d4f2f 100644 --- a/apps/catalog/index.html +++ b/apps/catalog/index.html @@ -4,6 +4,10 @@ + + + + NODE.DC UI Catalog @@ -11,4 +15,3 @@ - diff --git a/apps/catalog/package.json b/apps/catalog/package.json index c5900b8..29bf122 100644 --- a/apps/catalog/package.json +++ b/apps/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@nodedc/ui-catalog", - "version": "0.5.1", + "version": "0.6.0", "private": true, "type": "module", "scripts": { @@ -9,8 +9,8 @@ "typecheck": "tsc -b --pretty false" }, "dependencies": { - "@nodedc/ui-core": "0.5.1", - "@nodedc/ui-react": "0.5.1", + "@nodedc/ui-core": "0.6.0", + "@nodedc/ui-react": "0.6.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/apps/catalog/public/favicon/apple-touch-icon.png b/apps/catalog/public/favicon/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5078c01653bd8fca4c6ff0d98769b86f54f17743 GIT binary patch literal 4142 zcmbtYi91y9`#xh|LiU|PRQ4q_)+SQ26hgLS5HZ=tR>q974TB^iS%&Pg6isFtGbtq7 zNTDW6A2d^lK8CUPJD=ab@V(A;-uF7!b)NS<=YH8mz?Z`1mpw&01&!x-o^!- zJN`TQc)-zOn8yH|cmgik*#Lk4J1WU_1ppv2f5GOATf&o-X>_E|=UCpg)kM`a0SWX*fuOe*XN;2h_~HCzyu)aMYRsa zrg+5pkLuc?E;If^EYa}ZZCHT!>@{(55bXS)kKNUO^&PuI7d*`cFk zIptZ@>g(O#oDW^>Pt^Pm!xiyB`lc||%WpO1xa3{|;7vL~V z3keM+ip)(e8K0~GuH-)mf>KT&GXnB5Z*fx#^S}oja;sf04TGuX zpWnOBH|l1oRddn+UNWNV3C$;czUx@&K!+KbKd85f94gNXe39ruXL6?B9~JxD<`)z zTM5sdfBjDDU;hnw->Zt2g?p0dP7N`v$$Zoe4bhkGa&Vs2QO3IWr76GfDhLE(xXb6@ z*!w*@7t8IWgK8j4>}%AMJi%k@#X(~)?X7&qPV_%Kc5#Lca2!lU(zJ8ulitrlO-&P? z$eIQ1xQ*-2TjBI)Hwo;`&6oP~$#_3Pp=<4W(@Fg9?ruNC@0ue{)&{c752bzl$U1_l z;M-^th|cHZF7FS1@kI!*JVIl%kByDlHS%A&0^`K!|EMVW)B3rOzXvD(&!2=bsUS{H zPU+0H%T>--tW)j8u^ALPQ7`(Y z^xjE_>FMc7(eKXO&>=&{WSZ#lr?PUz#jhfDsjAm|NiwqtdRzwObL_N)HCkhwQFZFu z%*;WcHK}lPUCHh8ud+AdF}{u_r8KeI2?<+>_lRQ?!k+Yd!htWuOCdOc4HEP4bzd|Z zJu>^-bfhkmrcGPizWD)}{KB%`B^L=r9hb`9fV_eZy(&oF&eCkz8yP52)u=TohsORY z%gyyH=cDM*7*##E2$}ihnc42(;m4akI)4>x+^_x{3^K~1IRrmUTjlOxwn8NKMMPAM z@(I$AYl-6G(wiJx5h<3^1^iUOW(7wFhj7RxH-#Vz&Jng+JOL_pJg(e~BlX~+n#84xP_{r$Ba*(;hPTz#)w6Rk*ufsk! zUa<|T29?2F^uV!d)OOCz)m1Gr$4qpE=aQT8GbJ*U_}k=d?#TzV3}Hp6VB0760FQIk z!znM?8HvQ0o7|a^jJdhFMD!gG54O02vu=^q z3kEzOx815n)3R|(#J|jlm8GR@FR4|jUq&4UWfh*V>NS-|-arCJJ6%hYsLHffH|&-q z>W}z1BG~S%2qRoVP?IMTq7mw^^K%p%O`ejZO zeexgH&+&OM_6e$2X1$HlP9U)Nw_mekGtF5aO2x4@Awf{90Z^Lm4+a@SOf?Qa&TJ<0 zZOIpDlXeCHo9>TN_v5v*e+xkG4+Q?^UVFiTO82W>7cax7^yK7uHpeJXBluHSnaenG z)IaaApICjWcDa#S6Mx{CTVl+ZGe>%iCu1p#5v#v`broe+q*4RzQk@koleW%JMb(K{ z;358Bo;N9{&~O6|Z2C)I*8N)jU@PihwJ`?A8c)m`cSX=*e07#aiG9I|@$JtGKYyWi zHy07~ZtlueRaUQd()I$K$9OBxvnZCt_!DvVX|4cZ$~~u;@=El*RweHx&-i8H4`1{h z^X?ruMx#9^)w5*8IbhN4Lhiizpw{y}J#nn3W=48?)-5#sC7H9b>d|28LKCPrd}j3s zdIhAO8KAAUn-kT_Hfix}uj=aT+|aY8$Rs~v)$3zR-{yHYtJf*Gs^q_40cNE5rk{-7 zNr@-+_4mgErlLqKF0@o$Kl;=VM$@C^^`Xi;J-E}3GwaRNlINYBv1A8>`6Q#8X)i&g zJ>|%J^XAPwz&(+7HWF^G^w$v$zkao8%6ITVVUc}jN5}aqGQb?x?Itq_4R>}P9UVkv z|JejXTn*Z@KO2E4Chbk@0$c4i;VNMS1l{o<4nAZB$UI<*TR)mNtvGb-lE-N0&{mUP z0JkY4ahr5ccpJ9 zYO7#sF5o`ba&cv4g_OF(W$)${tm1v$x`>a!t@u7Lx)j*EYh7_l8zkN-yr9{UF-3#1 z?c4oPw^#(Jm%8G2pya@s8i_XooWSj=*y*Y)m-u-06@XX$_C|uYHYiH5jv+v#FMA0< zd8im0DhYgz9guk;VokBBIPM#ILgngpfQhuM5c;-dnMy)07<8?v15z25Sss<0x|pzO<40Ed+ysGH396|iJ>N{9T<~` z`F#_R0@og5ol5V;`P$c{`vsq)37Qvr_pZ?*;Nki zEmZr&PU`Wa=BO$97~7W3lBj}t)298EG_xB?tu__K_>T_;0*zs{JphJMbY9KxU&1Df zrFAp_|BKV&7Vfl~ILTb7fJXiFSa2uBn)%>bCzu5h&f{NsPb>5m7Qxcagu6cZlhoZ1 z;%f>lL~q{#e2&s?_=2zs71Ds>%!o8sis~oE^NYG_OPkIw%Y}Ve;)wRtH^6imN7N)3b}XR1Rapl5eqwd`_P*UrorR}(Bo={~DxjKmz70W*O=BbAcU z)=>>^_P?;3i5JA7m?a`FkW>xc*&7Dpof5AiZRp4p)5@Lzk@u~ST0<0skv<#UzU=n_> zHBYjVb(x3}{zkrodO2jRVnfLeQWo^%*@lv>>Ue6oq9HFOJ2?HFgJRhb*ytI9 z5+eXrGx9b;@_r)zC#;I!!fvd-6Jk9^+Pj%n!Vz!fB0c_mS|JCAdo7+FD~6H3Ic;Lt zUE)AY@fNGk{&~nZy~mxpXLahgv3QObtn`nT`D0p;jFQrbp0|A17Y)%(9`enz+~cV_ zt_CFu!c_Lnarv^~A5?7_H$6wDK!RVOBIXIw%4Y90e5f92C5ZKtaG(3H$p~m45eBC3 zPCvy(MfAP)?E7kpOmDdq6T{L0tO~PM6F(g>iTNR2hSx;yZT%5!_hfK!`E3v3Zn&Oc z{+7vQfc%U?*y9C<{JqOt#3s@dJTp|)DPIE!e zX1teKmk|ACU}em+CY2A-hCQZzKe_uUMV2iGSDVp(aXb{3q4obg$hFe>R%J8v!MC?n zoL+%Ocy1e&e7a~`1M!;b>&XcGST+ozs;bH`GlLHUJNEm+qJELS1pMZnCm%9h>f@(R zECAy|Xp0$YCMomNQl$*^^dcd1F1w=^UyrYDc!z)s%EG(7_GWCM@>rvhJNr^{jv-fF zSACNr@Ro)VU>6{EN6)`~JXOkgd#0D7yZ6qnNGoyn(Gs+h+ew_r6WukW4pajZ8*FyA sl~2<{Q&0;(-8uBX^~7L5jo(HqT@8_R*SeYy{r~_j*gDy?!2Iw2AJy93F#rGn literal 0 HcmV?d00001 diff --git a/apps/catalog/public/favicon/favicon.ico b/apps/catalog/public/favicon/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..973f3b2db97f00b686175edb7497021fe0f1421d GIT binary patch literal 16958 zcmeI4U5Hd=7{}jfidmD~6|IX*6f)Wnf+Q)(+Cdbxi;&QZK)Oj&H18Baw&oDs3EBsE z;e{-Scp-ux3t3&XQc8hzBS{xa4J68D#MTCGvCjDW&p9uw!#VG1XXmV^nfF=$=b7_9 z&v`!nXU==xGc#yRfxmLuuw8EY*BbMLF{U3dWCEb^hG1JXKSVg-x1IsdfM>un;2H1? zcm_NJo&nE*XTUQsM+|&Sp20cFmA5pjC};{yfE9TwF5`NN#Qz%9Bb?4SU*5v+oYuSL z;LW@hnNdAY5_c@7|53*JGRm2!u*YM1x9@>Nc`7u6S~d`~=IDPC+v*JR<{{*x6mbpM zbDg9w;iLJn)8qphO6s|Jd{2ipzmk6boBpM&OeoVI_W6Pl-oZ7uX} zx!L$X=iqVb|8*Aq|H!eA<&1+~>e&Hy0j=Yjv7504Kynn=a~Y?9!v1rLPk>#r7`z9X z^J!nTyaVQgtmQKj#PwpwhOz7ZwFcaE`0Cl`W$=c>M=>>TYW|!Dt^k`SPU{|%rMyOP z)g6~xV}F3Bz$4%)Nb=M85U_HA7cSo~Bgf~_cO1Ur>bc0b-KvLxX+7rbCkB-Qs1wwI!WXB>VyUhnlJNxQy}MF+l)PO3%YRj>%OLv&8R0m%^jz=LezuOr|$c%7~f(SSFOrP4s^vo z8wFm4)*RsSZKv%|a;=DRt4CLCany=z<{($xaqQ0k%@3DTd|h!`wb^;}MRMzNlcYF$ zmRtg6A`5X&yOuSNNwnTg%ANJNz4i{J)G&m7Ch`f+uG+N@DgmuqT->{2w8Qp0`85wN z0Iu_;`_5YMUt|qVQy{56*Pr z=WlBh2iv=-cX2QZ^f{J9YlBqZYX-?F&@DYr?^Dl5T03fOpx507J!|S7=EXDM z8So5v20R0v0ndPEz%$?(@CcG94$rVuPA(nwDs=p)jIDe|vg5Bf^g9wO-fMtE4d zA@!Om>l@8V%>?=tO+{b!pa&)!zo-cY^ko;aOlg!TEV9~L)h^$OjWm=g1)(lR+WsEL N>d?wIY~{gi?*Ti^k*5Fv literal 0 HcmV?d00001 diff --git a/apps/catalog/public/favicon/icon-192.png b/apps/catalog/public/favicon/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..d553a58e81dd92630500cf4bdc2f104993412841 GIT binary patch literal 4386 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 + + + + \ No newline at end of file diff --git a/apps/catalog/public/favicon/manifest.webmanifest.json b/apps/catalog/public/favicon/manifest.webmanifest.json new file mode 100644 index 0000000..f7256e8 --- /dev/null +++ b/apps/catalog/public/favicon/manifest.webmanifest.json @@ -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" + } + ] +} diff --git a/apps/catalog/src/CatalogApp.tsx b/apps/catalog/src/CatalogApp.tsx index 3aa8ab7..9fe8138 100644 --- a/apps/catalog/src/CatalogApp.tsx +++ b/apps/catalog/src/CatalogApp.tsx @@ -1,5 +1,5 @@ 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 { AdminNavigationPanel, AppHeader, @@ -13,6 +13,7 @@ import { Dropdown, FieldFrame, GlassSurface, + GlassMaterialSurface, HeaderAvatar, HeaderNavigation, HeaderProfile, @@ -39,8 +40,9 @@ import { type ShareAccessMember, type ToolbarPlacement, } 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 }> = [ { label: "NODE.DC", value: [255, 47, 146], hex: "#ff2f92" }, @@ -83,7 +85,10 @@ interface StoredLayout { logoUrl?: string; logoFileName?: string; logoFileSrc?: string; + faviconFileName?: string; + faviconAssets?: FaviconAssetUrls; }; + glass?: GlassMaterialSettings; toolbar?: { placement?: ToolbarPlacement; background?: string; @@ -140,14 +145,20 @@ const sectionDefinitions: Record(null); const pendingLogoFileRef = useRef(null); + const [glassMaterial, setGlassMaterial] = useState({ ...defaultGlassMaterial }); + const [faviconFileName, setFaviconFileName] = useState("icon-adaptive.svg"); + const [faviconError, setFaviconError] = useState(""); + const [generatedFavicons, setGeneratedFavicons] = useState([]); + const [faviconAssets, setFaviconAssets] = useState({ + ico: "/favicon/favicon.ico", + apple: "/favicon/apple-touch-icon.png", + icon192: "/favicon/icon-192.png", + icon512: "/favicon/icon-512.png", + }); + const pendingFaviconIcoRef = useRef(null); const [toolbarPlacement, setToolbarPlacement] = useState("bottom"); const [toolbarBg, setToolbarBg] = useState("#111115"); const [toolbarBorder, setToolbarBorder] = useState("#111117"); @@ -350,6 +372,14 @@ export function CatalogApp() { document.documentElement.style.setProperty("--nodedc-canvas-soft", material.nestedHex); }, [accent, fieldMaterial, material.fieldOpacity, material.nestedHex, material.panelOpacity, panelMaterial, theme]); + useEffect(() => { + applyGlassMaterial(document.documentElement, glassMaterial); + }, [glassMaterial]); + + useEffect(() => { + applyFaviconAssets(faviconAssets); + }, [faviconAssets]); + useEffect(() => { let active = true; fetch("/api/layout", { cache: "no-store" }) @@ -386,7 +416,10 @@ export function CatalogApp() { if (typeof media.logoUrl === "string") setLogoUrl(media.logoUrl); if (media.logoFileName) setLogoFileName(media.logoFileName); 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; if (toolbar) { if (toolbar.placement === "left" || toolbar.placement === "right" || toolbar.placement === "bottom") setToolbarPlacement(toolbar.placement); @@ -412,7 +445,8 @@ export function CatalogApp() { useEffect(() => () => { if (mediaObjectUrlRef.current) URL.revokeObjectURL(mediaObjectUrlRef.current); if (logoObjectUrlRef.current) URL.revokeObjectURL(logoObjectUrlRef.current); - }, []); + generatedFavicons.forEach((item) => URL.revokeObjectURL(item.previewUrl)); + }, [generatedFavicons]); const handleStageMediaFile = (file?: File) => { if (!file) return; @@ -449,6 +483,27 @@ export function CatalogApp() { 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 email = shareEmail.trim().toLowerCase(); if (!email) return; @@ -506,6 +561,28 @@ export function CatalogApp() { setLogoFileName(uploaded.fileName); 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 = { theme, accentHex, @@ -532,7 +609,10 @@ export function CatalogApp() { logoUrl, logoFileName, logoFileSrc: persistedLogoFileSrc, + faviconFileName, + faviconAssets: persistedFaviconAssets, }, + glass: glassMaterial, toolbar: { placement: toolbarPlacement, background: toolbarBg, @@ -785,6 +865,53 @@ export function CatalogApp() { onFileChange={handleLogoFile} /> + +
+ + {faviconFileName} +
+ {[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
{size} × {size}
; + })} +
+ {faviconError ? {faviconError} : null} +
+
+ + ); + case "glass": + return ( +
+
+ +
+ + CANONICAL GLASS +

Модальное окно и Inspector

+

Фон остаётся различимым, но текст и контролы сохраняют контраст. Этот материал не применяется к обычным панелям приложения.

+
+
+
+ +
+ setGlassMaterial((current) => ({ ...current, tintHex }))} /> + `${value}%`} onChange={(tintOpacity) => setGlassMaterial((current) => ({ ...current, tintOpacity }))} /> + `${value}px`} onChange={(blur) => setGlassMaterial((current) => ({ ...current, blur }))} /> + `${value}%`} onChange={(saturation) => setGlassMaterial((current) => ({ ...current, saturation }))} /> + `${value}%`} onChange={(brightness) => setGlassMaterial((current) => ({ ...current, brightness }))} /> + `${value}%`} onChange={(outlineOpacity) => setGlassMaterial((current) => ({ ...current, outlineOpacity }))} /> + `${value}%`} onChange={(shadowOpacity) => setGlassMaterial((current) => ({ ...current, shadowOpacity }))} /> +
+
); case "modals": @@ -933,7 +1060,7 @@ export function CatalogApp() { footer={ <> - Design System 0.5.1 + Design System 0.6.0 } /> diff --git a/apps/catalog/src/favicon.ts b/apps/catalog/src/favicon.ts new file mode 100644 index 0000000..b2e4747 --- /dev/null +++ b/apps/catalog/src/favicon.ts @@ -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((resolve, reject) => canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("favicon_canvas_failed")), "image/png")); +} + +export async function generateFavicons(file: File): Promise { + 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(`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"); +} diff --git a/apps/catalog/src/styles.css b/apps/catalog/src/styles.css index 975be16..5cd75cd 100644 --- a/apps/catalog/src/styles.css +++ b/apps/catalog/src/styles.css @@ -55,6 +55,133 @@ textarea { 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 { z-index: 1; background: diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e1e92c3..262add9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md index 7ed9c42..493dc2d 100644 --- a/docs/COMPONENTS.md +++ b/docs/COMPONENTS.md @@ -12,6 +12,8 @@ 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 — для действия без текста. diff --git a/package-lock.json b/package-lock.json index 694774e..3fc180c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nodedc-design-guideline", - "version": "0.5.1", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nodedc-design-guideline", - "version": "0.5.1", + "version": "0.6.0", "workspaces": [ "packages/*", "apps/*" @@ -25,10 +25,10 @@ }, "apps/catalog": { "name": "@nodedc/ui-catalog", - "version": "0.5.1", + "version": "0.6.0", "dependencies": { - "@nodedc/ui-core": "0.5.1", - "@nodedc/ui-react": "0.5.1", + "@nodedc/ui-core": "0.6.0", + "@nodedc/ui-react": "0.6.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, @@ -1888,27 +1888,27 @@ }, "packages/tokens": { "name": "@nodedc/tokens", - "version": "0.5.1" + "version": "0.6.0" }, "packages/ui-core": { "name": "@nodedc/ui-core", - "version": "0.5.1", + "version": "0.6.0", "dependencies": { - "@nodedc/tokens": "0.5.1" + "@nodedc/tokens": "0.6.0" } }, "packages/ui-dom": { "name": "@nodedc/ui-dom", - "version": "0.5.1", + "version": "0.6.0", "dependencies": { - "@nodedc/ui-core": "0.5.1" + "@nodedc/ui-core": "0.6.0" } }, "packages/ui-react": { "name": "@nodedc/ui-react", - "version": "0.5.1", + "version": "0.6.0", "dependencies": { - "@nodedc/ui-core": "0.5.1", + "@nodedc/ui-core": "0.6.0", "lucide-react": "^0.468.0" }, "devDependencies": { diff --git a/package.json b/package.json index cade927..e89fa9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nodedc-design-guideline", - "version": "0.5.1", + "version": "0.6.0", "private": true, "description": "Canonical NODE.DC design system, reusable UI packages and living component catalog.", "type": "module", diff --git a/packages/tokens/package.json b/packages/tokens/package.json index 3bb8a0f..fc9ded3 100644 --- a/packages/tokens/package.json +++ b/packages/tokens/package.json @@ -1,8 +1,12 @@ { "name": "@nodedc/tokens", - "version": "0.5.1", + "version": "0.6.0", "type": "module", - "files": ["tokens.json", "tokens.css", "themes.css"], + "files": [ + "tokens.json", + "tokens.css", + "themes.css" + ], "exports": { "./tokens.json": "./tokens.json", "./tokens.css": "./tokens.css", diff --git a/packages/tokens/themes.css b/packages/tokens/themes.css index 3a12c50..1c78da4 100644 --- a/packages/tokens/themes.css +++ b/packages/tokens/themes.css @@ -57,8 +57,10 @@ --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-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-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-rgb: 11 11 14; + --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); } @@ -120,7 +122,9 @@ --nodedc-primary-action-hover: rgba(8, 8, 10, 0.98); --nodedc-primary-action-color: rgba(247, 248, 244, 0.96); --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-shadow: 0 24px 72px rgba(0, 0, 0, 0.24), inset 0 1px 0 rgba(255, 255, 255, 0.8); + --nodedc-modal-glass-tint-rgb: 255 255 255; + --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); } diff --git a/packages/tokens/tokens.css b/packages/tokens/tokens.css index 1f35e9e..acc2b4a 100644 --- a/packages/tokens/tokens.css +++ b/packages/tokens/tokens.css @@ -16,7 +16,7 @@ --nodedc-header-control-height: 2.78rem; --nodedc-header-logo-width: 7.25rem; --nodedc-header-logo-height: 1.79rem; - --nodedc-inspector-width: 24.375rem; + --nodedc-inspector-width: 22.125rem; --nodedc-inspector-inner-width: 20.625rem; --nodedc-inspector-label-width: 9.625rem; --nodedc-inspector-control-width: 10.125rem; @@ -39,6 +39,12 @@ --nodedc-blur-control: 24px; --nodedc-blur-panel: 44px; --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-duration-fast: 120ms; diff --git a/packages/ui-core/package.json b/packages/ui-core/package.json index 0946474..6901f37 100644 --- a/packages/ui-core/package.json +++ b/packages/ui-core/package.json @@ -1,8 +1,11 @@ { "name": "@nodedc/ui-core", - "version": "0.5.1", + "version": "0.6.0", "type": "module", - "files": ["dist", "styles.css"], + "files": [ + "dist", + "styles.css" + ], "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { @@ -17,6 +20,6 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@nodedc/tokens": "0.5.1" + "@nodedc/tokens": "0.6.0" } } diff --git a/packages/ui-core/src/glass.ts b/packages/ui-core/src/glass.ts new file mode 100644 index 0000000..c4e13a0 --- /dev/null +++ b/packages/ui-core/src/glass.ts @@ -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 { + 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); + } +} diff --git a/packages/ui-core/src/index.ts b/packages/ui-core/src/index.ts index 33b6ece..3c8b7a3 100644 --- a/packages/ui-core/src/index.ts +++ b/packages/ui-core/src/index.ts @@ -1,4 +1,5 @@ export * from "./accent.js"; export * from "./floating.js"; +export * from "./glass.js"; export * from "./theme.js"; export * from "./toolbar.js"; diff --git a/packages/ui-core/styles.css b/packages/ui-core/styles.css index 4c81cbc..01f5b37 100644 --- a/packages/ui-core/styles.css +++ b/packages/ui-core/styles.css @@ -1914,8 +1914,8 @@ textarea.nodedc-field__control { background: var(--nodedc-modal-surface); color: var(--nodedc-text-primary); box-shadow: var(--nodedc-modal-shadow); - backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(1.38) brightness(1.06); - -webkit-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(var(--nodedc-modal-glass-saturation)) brightness(var(--nodedc-modal-glass-brightness)); 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)); max-height: calc(100vh - 7.25rem); border-radius: var(--nodedc-radius-panel); - background: var(--nodedc-app-panel-surface); - box-shadow: var(--nodedc-app-panel-shadow); - backdrop-filter: none; - -webkit-backdrop-filter: none; + background: var(--nodedc-modal-surface); + 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)); 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 { min-height: 4.875rem; padding: 1.125rem 0.75rem 0.875rem 1.625rem; diff --git a/packages/ui-dom/package.json b/packages/ui-dom/package.json index 3a5c73d..fbbc6ba 100644 --- a/packages/ui-dom/package.json +++ b/packages/ui-dom/package.json @@ -1,8 +1,10 @@ { "name": "@nodedc/ui-dom", - "version": "0.5.1", + "version": "0.6.0", "type": "module", - "files": ["dist"], + "files": [ + "dist" + ], "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { @@ -16,6 +18,6 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@nodedc/ui-core": "0.5.1" + "@nodedc/ui-core": "0.6.0" } } diff --git a/packages/ui-react/package.json b/packages/ui-react/package.json index b188c42..22f2123 100644 --- a/packages/ui-react/package.json +++ b/packages/ui-react/package.json @@ -1,8 +1,10 @@ { "name": "@nodedc/ui-react", - "version": "0.5.1", + "version": "0.6.0", "type": "module", - "files": ["dist"], + "files": [ + "dist" + ], "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { @@ -16,7 +18,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@nodedc/ui-core": "0.5.1", + "@nodedc/ui-core": "0.6.0", "lucide-react": "^0.468.0" }, "peerDependencies": { diff --git a/packages/ui-react/src/Glass.tsx b/packages/ui-react/src/Glass.tsx index 05f5722..a304132 100644 --- a/packages/ui-react/src/Glass.tsx +++ b/packages/ui-react/src/Glass.tsx @@ -35,3 +35,11 @@ export function GlassSurface({ ); } +export interface GlassMaterialSurfaceProps extends HTMLAttributes { + children: ReactNode; +} + +/** Canonical modal/Inspector material. General application panels must not use it. */ +export function GlassMaterialSurface({ children, className, ...props }: GlassMaterialSurfaceProps) { + return
{children}
; +} diff --git a/registry/components.json b/registry/components.json index cffaf75..d4dd665 100644 --- a/registry/components.json +++ b/registry/components.json @@ -5,15 +5,16 @@ "id": "glass-surface", "status": "baseline", "package": "@nodedc/ui-react", - "exports": ["GlassSurface"], - "domContract": ["nodedc-glass", "nodedc-material-rim"], - "summary": "Canonical matte/glossy glass surface for cards, panels and window shells.", - "anatomy": ["theme-provided surface", "optional material rim", "backdrop blur", "content"], + "exports": ["GlassSurface", "GlassMaterialSurface"], + "domContract": ["nodedc-glass", "nodedc-glass-material", "nodedc-material-rim"], + "summary": "Matte application surfaces plus the canonical Engine V4 glass material for modal and Inspector shells.", + "anatomy": ["theme-provided surface", "modal tint", "gradient rim", "backdrop blur/saturation/brightness", "content"], "variants": ["default", "strong", "soft"], "rules": [ "Geometry is invariant across application themes.", "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." ] }, { diff --git a/registry/registry.json b/registry/registry.json index f33a5fe..59cab32 100644 --- a/registry/registry.json +++ b/registry/registry.json @@ -1,6 +1,6 @@ { "schemaVersion": "1.0.0", - "libraryVersion": "0.5.1", + "libraryVersion": "0.6.0", "name": "NODE.DC Design System", "purpose": "Canonical code-level UI system for current and future NODE.DC applications.", "packages": [ diff --git a/server/catalog-server.mjs b/server/catalog-server.mjs index e7390ec..f45e1ce 100644 --- a/server/catalog-server.mjs +++ b/server/catalog-server.mjs @@ -18,6 +18,7 @@ const mimeTypes = { ".css": "text/css; charset=utf-8", ".gif": "image/gif", ".html": "text/html; charset=utf-8", + ".ico": "image/x-icon", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".js": "text/javascript; charset=utf-8", @@ -48,9 +49,10 @@ async function readJsonBody(request, maxBytes = 2 * 1024 * 1024) { } 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 = { "image/gif": ".gif", + "image/x-icon": ".ico", "image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp",