diff --git a/bun.lock b/bun.lock index 5f49ddf1..09a2123b 100644 --- a/bun.lock +++ b/bun.lock @@ -391,7 +391,7 @@ }, "packages/design-studio": { "name": "@wrnexus/design-studio", - "version": "0.1.5", + "version": "0.1.6", "bin": { "wrnexus-design-studio": "src/cli.ts", }, @@ -409,7 +409,7 @@ }, "packages/design-system": { "name": "@wrnexus/design-system", - "version": "0.1.1", + "version": "0.1.2", }, "packages/dev-server": { "name": "@wrnexus/dev-server", @@ -782,7 +782,7 @@ }, "packages/ui": { "name": "@wrnexus/ui", - "version": "0.8.121", + "version": "0.8.122", "dependencies": { "@wrnexus/core": "workspace:*", "@wrnexus/design-system": "workspace:*", diff --git a/docs/ui-visual-contract-0.8.json b/docs/ui-visual-contract-0.8.json index dc5414bf..a61c3fc2 100644 --- a/docs/ui-visual-contract-0.8.json +++ b/docs/ui-visual-contract-0.8.json @@ -17,7 +17,7 @@ "packages/ui/components/Carousel.wrn": "5d8bd031007e7522c81f6acb16538534230cb8ea5fad4eef12b10db5f109c67a", "packages/ui/components/Chart.wrn": "d87254752284ba10eb6f36b8654872b0defe84222c01c739c0086809e05dba12", "packages/ui/components/ChatBubble.wrn": "f81c2630542b7c8a171acf2c9102a91b64cc06f925a014bc7f96d3778adbafdc", - "packages/ui/components/Clipboard.wrn": "fcc4e8a3b3a24b599eb6a52337a31d06c2c4dee3b1a000281d324b8b71678f1b", + "packages/ui/components/Clipboard.wrn": "0fb6a19dbcd3b20c28154c9482839ff32b221059c2b1125578d3edeb13cbe6ef", "packages/ui/components/Collapse.wrn": "c5ecae44b455498e5eae527100abdc27bf9e18f7a361ce563d45087c0cc4a3d3", "packages/ui/components/ColorPicker.wrn": "561b1cef73ff86f50072fefabade212b0cf10a48e6dd6c6aa93fd6e900fe3f50", "packages/ui/components/Columns.wrn": "c892798d910745453b617a7f0dcf20ab1fd6506095c150de0f36e9fcf7661783", @@ -41,7 +41,7 @@ "packages/ui/components/FileInput.wrn": "76b0719609ff2204cccbad9692a3b820ce63ee98754f2394be6dbd9b8510e873", "packages/ui/components/FileUploadProgress.wrn": "4f4dbaf9eb165d9aa48742c03ce51d895891bb234f80b2865bce03d4da0e24ba", "packages/ui/components/FlowCanvas.wrn": "8ca776800047ae4f4e1032352f2e17a295c9b8198d48d7ce3fc9ee26c2033ecd", - "packages/ui/components/Footer.wrn": "ea931e624eb820beaef698e93191ed707403ea8b4be4117fa5b83d5a6b0b0f55", + "packages/ui/components/Footer.wrn": "989bae53515398e24fbea19a3020dcd802d1e58d82d02a9b40a7752eb08a1833", "packages/ui/components/Grid.wrn": "b44e041a627cf0f62f676b83ca3c0ea2b671e07876f0649310081edefde4217a", "packages/ui/components/Hero.wrn": "dbc4cb3099d422edc2a7bc0500499db5c7c67aa272cbe9329a21e6d665aca047", "packages/ui/components/HeroActions.wrn": "f3e633751e1bdfca11d5fe3d4eb96d4aaba11cba298a62794ed3e2201d3ca038", @@ -52,7 +52,7 @@ "packages/ui/components/LayoutSplitter.wrn": "394a765f26c6634d4d75cf91b47b2874bee2250f15f399c325b740fc671cb1ad", "packages/ui/components/LegendIndicator.wrn": "6d72694259ad877ae987f260887a1d60301f692bf05622d9a36b3a7f02b57dd3", "packages/ui/components/Link.wrn": "59b7590d68ddbc8c70dd1926ebeed22dbe7b3d0d60613eb74d156633808d9db9", - "packages/ui/components/List.wrn": "79ba3e76767a8ffccfc6a25c1d4d13e765cfaf2ef02c961d0490b385fe199036", + "packages/ui/components/List.wrn": "14c4690cd600de9035f5a979bd29749eea31059de2f6bef70cd38543fb1130dc", "packages/ui/components/ListGroup.wrn": "40c1e8f3030fdc41101f65a06ef8752e52a9e88d85f2c85773fa1544bb6527b9", "packages/ui/components/Map.wrn": "aba6b73a89fe69f84a245d6685771cd7f34e813ca4c54d45a46c2e96135ecb70", "packages/ui/components/MarketingSectionHeader.wrn": "b05e7fa1341fbd633145a702ab07fe04b163194bd2513f14b572eb63e09348ea", @@ -67,8 +67,8 @@ "packages/ui/components/Pagination.wrn": "df6faf0dbafd1249092b45e586159b575c3d37c73d81847376a28c3ea8fdb5cd", "packages/ui/components/PinInput.wrn": "c17b2a1bb38ba0420de7c9179e64fd104cbd8d3b84127a308a4333d46232b3fb", "packages/ui/components/Popover.wrn": "aa73c63bde88d6f52061a98fdc8c84f1147e784abfeb802f094a89c061b9a893", - "packages/ui/components/PortalDashboard.wrn": "ef67fdf9faac850ef34b1d2564386d41a0cfa4c754ed0988e85f2e7ea2f50cdb", - "packages/ui/components/PreferenceSwitcher.wrn": "33637d758e63a4a6ce5ca51362f34387aacc1f64134434912c7ecd119b7cb5a8", + "packages/ui/components/PortalDashboard.wrn": "342d001689a386533bb4f864e93557460d051466eca010b8aa46ee7586c85cb0", + "packages/ui/components/PreferenceSwitcher.wrn": "09a70986d444d5985277e3cbff5d90804ea3136c0b23ee8426daa0c0d6b41351", "packages/ui/components/PublicPageShell.wrn": "1e28f4f387f235198a723ed13b0128fe484894e4962b73a482a5a28c8acf003f", "packages/ui/components/RangeSlider.wrn": "a4005dcca80e3e23b6488e865b92486b368f46e00d3f05e7b0663d421652be06", "packages/ui/components/Rating.wrn": "8456d577da017eac6e54db78b4beced73930da6fe8ef7a5b74c65c2902571ea9", diff --git a/packages/design-studio/package.json b/packages/design-studio/package.json index 779ea844..bd3c91f3 100644 --- a/packages/design-studio/package.json +++ b/packages/design-studio/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/design-studio", - "version": "0.1.5", + "version": "0.1.6", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/design-studio/src/client.ts b/packages/design-studio/src/client.ts index c04e9ee3..c114a504 100644 --- a/packages/design-studio/src/client.ts +++ b/packages/design-studio/src/client.ts @@ -1,3 +1,4 @@ +import { handlePreviewClipboard } from "./preview-clipboard.ts"; import { STUDIO_PRESETS } from "./presets.ts"; import { setupGuidance } from "./guidance.ts"; import { StudioSession, importDesign, exportDesign, reviewDesign } from "./session.ts"; @@ -307,6 +308,13 @@ async function previews() { $("show-draft").hidden = !reviewed; } window.addEventListener("message", (event) => { + void handlePreviewClipboard( + event, + [$("preview-frame"), $("compare-frame")], + (text) => navigator.clipboard.writeText(text), + navigator.userActivation?.isActive ?? false, + ); + const frame = $("preview-frame"); if ( event.source === frame.contentWindow && diff --git a/packages/design-studio/src/gallery.ts b/packages/design-studio/src/gallery.ts index 5442848d..008f09fb 100644 --- a/packages/design-studio/src/gallery.ts +++ b/packages/design-studio/src/gallery.ts @@ -81,8 +81,44 @@ export function gallerySample(name: string) { ], palette: [{ kind: "task", label: "Add task" }], }); - if (name === "Toaster") - content = ''; + if (name === "Toaster") { + props.max = 1; + content = + ''; + } + if (name === "PreferenceSwitcher") + Object.assign(props, { + themeLabel: "Theme", + colorLabel: "Accent color", + languageLabel: "Language", + }); + if (name === "Tooltip") + Object.assign(props, { + variant: "soft", + title: "Helpful information", + description: "Hover or focus a control to see a short explanation.", + }); + if (name === "PortalDashboard") + Object.assign(props, { + title: "Sample service dashboard", + description: "Sample tasks, shortcuts and recent activity.", + tasks: [ + { + label: "Review submitted documents", + detail: "Confirm the required attachments before approval.", + status: "urgent", + meta: "Due today", + href: "#review", + }, + { + label: "Update contact information", + detail: "Check the applicant’s preferred email address.", + status: "warning", + meta: "In progress", + href: "#contact", + }, + ], + }); if (name === "BackToTop") { props.threshold = 0; props.alwaysVisible = true; diff --git a/packages/design-studio/src/presets.ts b/packages/design-studio/src/presets.ts index cfbfb0ce..3f1b5c61 100644 --- a/packages/design-studio/src/presets.ts +++ b/packages/design-studio/src/presets.ts @@ -24,9 +24,20 @@ function palette(tokens: TokenMap, values: Palette) { "text-subtle": subtle, "border-strong": border, "primary-muted": muted, + muted: subtle, + focus: primary, + "input-background": surface, + "input-border": border, + "code-background": muted, + "code-text": text, + "code-border": border, }; for (const [name, value] of Object.entries(colors)) - tokens[`color.${name}`] = { ...tokens[`color.${name}`]!, value }; + tokens[`color.${name}`] = { + type: "color", + legacy: tokens[`color.${name}`]?.legacy ?? `color-${name}`, + value, + }; } function direction( id: string, @@ -70,10 +81,10 @@ export const STUDIO_PRESETS: Record = { forest: direction( "forest", "Forest / Sage", - ["#E8DCC7", "#D4B895", "#C7CCB5", "#283320", "#48543E", "#7B866A", "#43552D", "#E8DCC7"], + ["#E5EBDD", "#F4F6EE", "#D5DFC8", "#233827", "#4C604C", "#9BAE91", "#355E40", "#FFFFFF"], ["#20291C", "#2C3726", "#394530", "#E8DCC7", "#BDC5A5", "#718060", "#B7D092", "#20291C"], - "24px", - serif, + "10px", + sans, ), ocean: direction( "ocean", @@ -103,10 +114,10 @@ export const STUDIO_PRESETS: Record = { ember: direction( "ember", "Ember / Terracotta", - ["#E8DCC7", "#D4B895", "#CEB094", "#3D241C", "#694837", "#A7795A", "#8E3C21", "#E8DCC7"], + ["#F5E3D5", "#FFF9F3", "#EBCDB7", "#48271D", "#775342", "#C59C81", "#A34525", "#FFFFFF"], ["#2B1B15", "#3B2820", "#50362B", "#E8DCC7", "#CEB094", "#9C7057", "#F4AE80", "#2B1B15"], - "16px", - serif, + "6px", + sans, ), editorial: direction( "editorial", @@ -129,18 +140,44 @@ export const STUDIO_PRESETS: Record = { brutalist: direction( "brutalist", "Brutalist / Primary", - ["#FFFFFF", "#FFFF00", "#FFFFFF", "#000000", "#000000", "#000000", "#0000FF", "#FFFFFF"], + ["#FFFFFF", "#FFFFFF", "#F0F0F0", "#000000", "#333333", "#000000", "#0000FF", "#FFFFFF"], ["#000000", "#000000", "#202020", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFF00", "#000000"], "0", - mono, - "4px 4px 0 #000000", + sans, + "2px 2px 0 #000000", ), contrast: direction( "contrast", "Contrast / Clear", - ["#FFFFFF", "#FFFFFF", "#E6E6E6", "#000000", "#333333", "#000000", "#002FA7", "#FFFFFF"], - ["#000000", "#000000", "#1B1B1B", "#FFFFFF", "#E0E0E0", "#FFFFFF", "#FFFF00", "#000000"], - "4px", + ["#FFFFFF", "#FFFFFF", "#EDF1F6", "#101828", "#344054", "#667085", "#173EA5", "#FFFFFF"], + ["#0D1117", "#161D29", "#253044", "#FFFFFF", "#D0D8E8", "#8999B2", "#A5C8FF", "#0D1117"], + "8px", sans, ), }; + +// Neutral editorial status colors retain textual and icon cues without stray hues. +for (const tokens of [ + STUDIO_PRESETS.editorial!.tokens, + STUDIO_PRESETS.editorial!.modes!.light!, + STUDIO_PRESETS.editorial!.modes!.dark!, +]) { + for (const name of ["secondary", "success", "warning", "danger", "info"]) { + for (const suffix of ["", "-hover", "-active", "-text"]) + tokens[`color.${name}${suffix}`] = { + type: "color", + value: tokens["color.primary"]!.value, + legacy: `color-${name}${suffix}`, + }; + tokens[`color.${name}-soft`] = { + type: "color", + value: tokens["color.surface-2"]!.value, + legacy: `color-${name}-soft`, + }; + tokens[`color.${name}-contrast`] = { + type: "color", + value: tokens["color.primary-contrast"]!.value, + legacy: `color-${name}-contrast`, + }; + } +} diff --git a/packages/design-studio/src/preview-clipboard.ts b/packages/design-studio/src/preview-clipboard.ts new file mode 100644 index 00000000..c9e3a927 --- /dev/null +++ b/packages/design-studio/src/preview-clipboard.ts @@ -0,0 +1,53 @@ +/** The sandbox can request a bounded text copy, never read clipboard contents. */ +export async function handlePreviewClipboard( + event: Pick, + frames: Pick[], + writeText: (text: string) => Promise, + activated = true, +) { + const data = event.data; + const frame = frames.find((frame) => frame.contentWindow === event.source); + if ( + !frame || + !data || + data.type !== "wrn-studio-copy" || + typeof data.id !== "string" || + !/^[a-z0-9-]{1,80}$/.test(data.id) || + data.path !== new URL(frame.src).pathname || + typeof data.text !== "string" || + data.text.length > 16384 + ) + return false; + let ok = false; + try { + if (!activated) throw Error("Copy requires a user gesture."); + await writeText(data.text); + ok = true; + } catch { + /* The control reports failure and keeps the value selectable. */ + } + frame.contentWindow?.postMessage({ type: "wrn-studio-copy-result", id: data.id, ok }, "*"); + return true; +} + +/** Runs only in the generated trusted preview, with an opaque sandbox origin. */ +export const previewClipboardRuntime = ` +if (parent !== window) { + const copies = new Map(); + window.addEventListener('message', event => { + if (event.source !== parent || event.data?.type !== 'wrn-studio-copy-result') return; + const pending = copies.get(event.data.id); + if (!pending) return; + clearTimeout(pending.timer); copies.delete(event.data.id); + event.data.ok ? pending.resolve() : pending.reject(new Error('Clipboard copy was denied')); + }); + window.wrnClipboard = {writeText(text) { + return new Promise((resolve,reject) => { + const id = crypto.randomUUID(); + const timer = setTimeout(() => { copies.delete(id); reject(new Error('Clipboard copy timed out')); }, 4000); + copies.set(id, {resolve,reject,timer}); + parent.postMessage({type:'wrn-studio-copy',id,path:location.pathname,text:String(text)}, '*'); + }); + }}; +} +`; diff --git a/packages/design-studio/src/preview.ts b/packages/design-studio/src/preview.ts index 34f3d265..bd869b27 100644 --- a/packages/design-studio/src/preview.ts +++ b/packages/design-studio/src/preview.ts @@ -1,3 +1,4 @@ +import { previewClipboardRuntime } from "./preview-clipboard.ts"; import { validateAgentPage } from "./agent-pages.ts"; import { readFileSync, mkdirSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; @@ -174,6 +175,7 @@ export async function previewDocument( } const runtime = bindings + + previewClipboardRuntime + REACTIVE_RUNTIME + providers + "\ndocument.addEventListener('DOMContentLoaded',()=>parent.postMessage({type:'wrn-studio-preview-ready',path:location.pathname},'*'),{once:true});"; @@ -181,9 +183,9 @@ export async function previewDocument( meta: { title: system.name + " preview" }, body, htmlAttrs: ` dir="${options.direction === "rtl" ? "rtl" : "ltr"}" data-theme="${escape(mode)}"`, - extraHead: ``, + extraHead: ``, componentRecipeCss: theme.recipeCss, - extraBody: ``, + extraBody: ``, }); }); } diff --git a/packages/design-studio/test/clipboard.test.ts b/packages/design-studio/test/clipboard.test.ts new file mode 100644 index 00000000..dfb8984e --- /dev/null +++ b/packages/design-studio/test/clipboard.test.ts @@ -0,0 +1,62 @@ +import { expect, test } from "bun:test"; +import { handlePreviewClipboard } from "../src/preview-clipboard.ts"; + +test("clipboard broker accepts only current preview frames and bounded user-initiated text", async () => { + const results: unknown[] = []; + const source = { postMessage: (value: unknown) => results.push(value) } as unknown as Window; + const frames = [{ contentWindow: source, src: "https://example.test/preview/current" }]; + const event = { + source, + data: { + type: "wrn-studio-copy", + id: "copy-1", + path: "/preview/current", + text: "npm install @wrnexus/ui", + }, + }; + const copied: string[] = []; + const write = async (text: string) => { + copied.push(text); + }; + expect(await handlePreviewClipboard(event, frames, write)).toBe(true); + expect(copied).toEqual(["npm install @wrnexus/ui"]); + expect(results).toEqual([{ type: "wrn-studio-copy-result", id: "copy-1", ok: true }]); + expect(await handlePreviewClipboard({ ...event, source: null }, frames, write)).toBe(false); + expect( + await handlePreviewClipboard( + { ...event, data: { ...event.data, path: "/preview/old" } }, + frames, + write, + ), + ).toBe(false); + expect( + await handlePreviewClipboard( + { ...event, data: { ...event.data, text: "x".repeat(16385) } }, + frames, + write, + ), + ).toBe(false); + expect(await handlePreviewClipboard(event, frames, write, false)).toBe(true); + expect(copied).toHaveLength(1); + expect(results.at(-1)).toEqual({ type: "wrn-studio-copy-result", id: "copy-1", ok: false }); +}); + +test("clipboard denial reports failure rather than claiming a copy succeeded", async () => { + let result: any; + const source = { + postMessage: (value: unknown) => { + result = value; + }, + } as unknown as Window; + await handlePreviewClipboard( + { + source, + data: { type: "wrn-studio-copy", id: "copy-2", path: "/preview/current", text: "test" }, + }, + [{ contentWindow: source, src: "https://example.test/preview/current" }], + async () => { + throw Error("denied"); + }, + ); + expect(result.ok).toBe(false); +}); diff --git a/packages/design-studio/test/presets.test.ts b/packages/design-studio/test/presets.test.ts index 017da717..a3d03e82 100644 --- a/packages/design-studio/test/presets.test.ts +++ b/packages/design-studio/test/presets.test.ts @@ -33,3 +33,18 @@ test("choosing a new preset stays undoable and never mutates the catalog", () => session.undo(); expect(session.valid.id).toBe("square"); }); + +test("revised directions stay distinct and Editorial status tokens remain monochrome", () => { + const organic = ["soft", "forest", "ember"].map((id) => STUDIO_PRESETS[id]!); + expect(new Set(organic.map((s) => s.modes!.light!["color.bg"]!.value)).size).toBe(3); + expect(new Set(organic.map((s) => s.tokens["shape.control"]!.value)).size).toBe(3); + for (const tokens of [ + STUDIO_PRESETS.editorial!.tokens, + ...Object.values(STUDIO_PRESETS.editorial!.modes!), + ]) { + for (const name of ["success", "warning", "danger", "info"]) { + const color = String(tokens[`color.${name}`]!.value); + expect(color).toMatch(/^#([0-9a-f]{2})\1\1$/i); + } + } +}); diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 5905825e..869ef0a0 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/design-system", - "version": "0.1.1", + "version": "0.1.2", "description": "Typed, deterministic design tokens and legacy WrNexus theme integration.", "type": "module", "main": "src/index.ts", diff --git a/packages/design-system/src/catalog-recipes.ts b/packages/design-system/src/catalog-recipes.ts index 39756f27..53581a4b 100644 --- a/packages/design-system/src/catalog-recipes.ts +++ b/packages/design-system/src/catalog-recipes.ts @@ -23,18 +23,35 @@ export function catalogRecipes(): Record { const circle = ["circle", "round"].includes(axes.shape?.default ?? ""); const semanticGeometry = /thumb|track|spinner|progress|marker|port$|status|dot|star|srOnly/i.test(part); + // Joined surfaces share the outer component's corners. Rounding each + // child creates seams, scalloped panels and rounded splitter rails. + const joined = ( + { + StatsBar: ["item", "content"], + AuthSplitLayout: ["content"], + Collapse: ["trigger", "panel", "content"], + InputNumber: ["button", "content", "input"], + LayoutSplitter: ["handle"], + } as Record + )[name]?.includes(part); if ( descriptor.properties.includes("borderRadius") && !roundedFalse && !circle && - !semanticGeometry + !semanticGeometry && + !joined ) { styles.borderRadius = ref( descriptor.profile === "control" ? "shape.control" : "shape.panel", ); } // Only existing surfaces receive elevation. Layout wrappers and provider hosts are untouched. - if (descriptor.profile === "surface" && descriptor.properties.includes("boxShadow")) + if ( + descriptor.profile === "surface" && + descriptor.properties.includes("boxShadow") && + !joined && + !(name === "PreferenceSwitcher" && part === "menu") + ) styles.boxShadow = ref("elevation.panel"); if (Object.keys(styles).length) base[part] = styles; } diff --git a/packages/design-system/src/presets.ts b/packages/design-system/src/presets.ts index ba20e13b..6cb119da 100644 --- a/packages/design-system/src/presets.ts +++ b/packages/design-system/src/presets.ts @@ -53,7 +53,7 @@ function recipes(angular: boolean, outlined: boolean): Record { + for (const system of Object.values(CORE_DESIGN_SYSTEMS)) { + for (const [name, parts] of Object.entries({ + StatsBar: ["item", "content"], + AuthSplitLayout: ["content"], + Collapse: ["trigger", "panel", "content"], + InputNumber: ["button", "input", "content"], + LayoutSplitter: ["handle"], + })) { + for (const part of parts) { + expect(system.recipes![name]!.base?.[part]?.borderRadius).toBeUndefined(); + expect(system.recipes![name]!.base?.[part]?.boxShadow).toBeUndefined(); + } + } + } +}); diff --git a/packages/ui/components/Clipboard.wrn b/packages/ui/components/Clipboard.wrn index b5896ccf..75de3bea 100644 --- a/packages/ui/components/Clipboard.wrn +++ b/packages/ui/components/Clipboard.wrn @@ -40,6 +40,9 @@ component Clipboard { } status = "copying" output.copy({ value: value, sourceEvent: sourceEvent }) + if (window.wrnClipboard && window.wrnClipboard.writeText) { + return window.wrnClipboard.writeText(value).then(copySucceeded).catch(copyFailed) + } if (!window.navigator.clipboard || !window.navigator.clipboard.writeText) { copyFailed(new Error("Clipboard API is unavailable")) return diff --git a/packages/ui/components/Footer.wrn b/packages/ui/components/Footer.wrn index 844025d1..07ca8cb5 100644 --- a/packages/ui/components/Footer.wrn +++ b/packages/ui/components/Footer.wrn @@ -47,10 +47,7 @@ component Footer { {#if items.length > 0} @@ -260,10 +257,7 @@ component Footer {
- - +
@@ -273,10 +267,7 @@ component Footer { {#if copyright} @@ -290,10 +281,7 @@ component Footer { @@ -366,9 +354,11 @@ component Footer { font-weight: 400; } + .wrn-footer { container: wrn-footer / inline-size; } + .wrn-footer__grid { display: grid; - grid-template-columns: minmax(13rem, 1.2fr) minmax(0, 3fr) minmax(13rem, 1fr); + grid-template-columns: minmax(0, 1fr); width: min(100%, 90rem); margin-inline: auto; padding: clamp(2rem, 5vw, 4rem) clamp(1rem, 4vw, 3rem); @@ -380,6 +370,8 @@ component Footer { max-width: none; } + .wrn-footer__pre:has(> slot:empty), + .wrn-footer__post:has(> slot:empty), .wrn-footer__pre:empty, .wrn-footer__post:empty, .wrn-footer__pre:not(:has(*)), @@ -408,6 +400,7 @@ component Footer { .wrn-footer__links { display: grid; grid-template-columns: repeat(var(--wrn-footer-columns, 3), minmax(0, 1fr)); + min-width: 0; align-content: start; gap: 1.5rem; } @@ -435,6 +428,8 @@ component Footer { .wrn-footer__link { display: flex; width: fit-content; + max-width: 100%; + overflow-wrap: anywhere; min-height: 2rem; align-items: center; gap: 0.4rem; @@ -469,7 +464,7 @@ component Footer { } .wrn-footer__bottom--slots:not( - :has(.wrn-footer__copyright-left > *, .wrn-footer__copyright-right > *) + :has(.wrn-footer__copyright-left > slot:not(:empty), .wrn-footer__copyright-right > slot:not(:empty)) ) { display: none; } @@ -513,6 +508,8 @@ component Footer { text-align: right; } + .wrn-footer__copyright-left:has(> slot:empty), + .wrn-footer__copyright-right:has(> slot:empty), .wrn-footer__copyright-left:empty, .wrn-footer__copyright-right:empty, .wrn-footer__copyright-left:not(:has(*)), @@ -525,7 +522,7 @@ component Footer { grid-template-columns: 1fr; } .wrn-footer__links { - grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr)); } .wrn-footer__bottom--slots { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); @@ -566,5 +563,13 @@ component Footer { grid-template-columns: 1fr; } } + @container wrn-footer (max-width: 700px) { + .wrn-footer__links { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .wrn-footer__bottom { grid-template-columns: 1fr; gap: 1rem; } + .wrn-footer__copyright { grid-column: 1; white-space: normal; } + } + @container wrn-footer (max-width: 400px) { + .wrn-footer__links { grid-template-columns: 1fr; } + } } } diff --git a/packages/ui/components/List.wrn b/packages/ui/components/List.wrn index d62c15eb..05cc680d 100644 --- a/packages/ui/components/List.wrn +++ b/packages/ui/components/List.wrn @@ -107,6 +107,9 @@ component List { } .wrn-list__description { margin-top: 0.25rem; color: var(--wrn-color-text-muted); font-size: 0.875rem; line-height: 1.5rem; } .wrn-list__items { + list-style: none; + margin: 0; + padding: 0; overflow: hidden; } .wrn-list__items:is([data-variant="default"], [data-variant="divided"]) > .wrn-list__item + .wrn-list__item { diff --git a/packages/ui/components/PortalDashboard.wrn b/packages/ui/components/PortalDashboard.wrn index bc2a5bf4..87407db8 100644 --- a/packages/ui/components/PortalDashboard.wrn +++ b/packages/ui/components/PortalDashboard.wrn @@ -312,9 +312,10 @@ component PortalDashboard { } .wrn-portal-dashboard__list a { - border: 0; - border-bottom: 1px solid var(--wrn-color-border); - border-radius: 0; + align-items: flex-start; + border: 1px solid var(--wrn-color-border); + border-radius: var(--wrn-radius-sm); + background: var(--wrn-color-surface-2); } .wrn-portal-dashboard__status { @@ -326,6 +327,8 @@ component PortalDashboard { } .wrn-portal-dashboard__list em { + flex: none; + margin-inline-start: auto; color: var(--wrn-color-muted); font-size: 0.7rem; font-style: normal; diff --git a/packages/ui/components/PreferenceSwitcher.wrn b/packages/ui/components/PreferenceSwitcher.wrn index 3d5dcb1d..48fbbc76 100644 --- a/packages/ui/components/PreferenceSwitcher.wrn +++ b/packages/ui/components/PreferenceSwitcher.wrn @@ -117,6 +117,8 @@ size: string = "default" color: var(--wrn-color-muted); cursor: pointer; list-style: none; + background: var(--wrn-color-surface-2); + font: inherit; } .wrn-preferences__menu > [data-wrn-menu-toggle]:hover, @@ -185,21 +187,31 @@ size: string = "default" padding: 0.5rem 0.65rem; align-items: center; gap: 0.5rem; - border-radius: 0.55rem; + border: 1px solid transparent; + border-radius: var(--wrn-radius-sm, 0.55rem); + font-family: inherit; + text-align: start; + cursor: pointer; color: var(--wrn-color-text); background: var(--wrn-color-surface-2); font-size: 0.75rem; font-weight: 500; } + .wrn-preferences__panel button:focus-visible { + outline: 2px solid var(--wrn-color-focus, var(--wrn-color-primary)); + outline-offset: 2px; + } + .wrn-preferences__panel button:hover { - outline: 2px solid color-mix(in srgb, var(--wrn-color-primary) 40%, transparent); + border-color: var(--wrn-color-border); + background: var(--wrn-color-surface-2); } .wrn-preferences__panel button[aria-pressed="true"] { color: var(--wrn-color-primary); background: color-mix(in srgb, var(--wrn-color-primary) 12%, var(--wrn-color-surface)); - outline: 1px solid color-mix(in srgb, var(--wrn-color-primary) 55%, transparent); + border-color: color-mix(in srgb, var(--wrn-color-primary) 55%, transparent); } .wrn-preferences__swatches { diff --git a/packages/ui/package.json b/packages/ui/package.json index 560d6e1d..d99e56b1 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.8.121", + "version": "0.8.122", "private": true, "type": "module", "main": "src/index.ts",