Pre Release New Changes
This commit is contained in:
@@ -1177,6 +1177,15 @@ const MIGRATIONS: Migration[] = [
|
||||
// Runtime-only framework fix. No project files require migration.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.5.15",
|
||||
id: "wrn-style-block-head-pipeline",
|
||||
description:
|
||||
"Promotes page, layout, and component style blocks into the document head after global CSS with CSP, HMR, and CSR navigation support.",
|
||||
apply() {
|
||||
// Compiler, SSR, dev-server, and CSR runtime changes apply automatically.
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** Release tooling uses this to require an explicit migration entry per version. */
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* api="<name>" -> SSR/client data binding declared in a mode block
|
||||
* ssrGet/ssrText -> legacy server-side API fetch + render
|
||||
* csrGet/csrText -> legacy browser-side API fetch + render
|
||||
* style -> an inline page stylesheet
|
||||
* style -> tagged local stylesheet metadata promoted by SSR
|
||||
* functions -> server-only helpers for API/realtime code
|
||||
* api M /p {b} -> export const M = async (ctx) => { b }
|
||||
* realtime {..} -> export const websocket = { evt(ws, ...args) { b } }
|
||||
@@ -735,6 +735,36 @@ function hydrationId(ast: PageAst): string {
|
||||
return `${ast.name}:${stableHash(shape)}`;
|
||||
}
|
||||
|
||||
function localStyleId(ast: PageAst): string {
|
||||
return `wrn-${ast.kind}-${stableHash(`${ast.kind}:${ast.name}`)}`;
|
||||
}
|
||||
|
||||
function localStyleTag(ast: PageAst, styles: string[]): string {
|
||||
if (!styles.length) return "";
|
||||
|
||||
const id = localStyleId(ast);
|
||||
const css = styles.map(styleEscape).join("\n");
|
||||
|
||||
return `<style data-wrnexus-style="${attrEscape(ast.name)}" data-wrnexus-style-id="${attrEscape(id)}" data-wrnexus-style-owner="${attrEscape(ast.name)}" data-wrnexus-style-kind="${attrEscape(ast.kind)}">\n${css}\n</style>`;
|
||||
}
|
||||
|
||||
function localStyleExport(ast: PageAst, styles: string[]): string | null {
|
||||
if (!styles.length) return null;
|
||||
|
||||
return `export const __wrnexusStyles = ${JSON.stringify(
|
||||
[
|
||||
{
|
||||
id: localStyleId(ast),
|
||||
owner: ast.name,
|
||||
kind: ast.kind,
|
||||
css: styles.join("\n"),
|
||||
},
|
||||
],
|
||||
null,
|
||||
2,
|
||||
)};`;
|
||||
}
|
||||
|
||||
function isSafeGeneratedIdentifier(name: string): boolean {
|
||||
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
|
||||
}
|
||||
@@ -829,10 +859,12 @@ export function generate(ast: PageAst): string {
|
||||
html = `<div data-scope="${scopePlaceholder}"${behaviorAttribute(pageBehavior)}${hydrationAttribute(ast)}>${html}</div>`;
|
||||
}
|
||||
|
||||
if (styles.length > 0) {
|
||||
const css = styles.map(styleEscape).join("\n");
|
||||
html = `<style data-wrnexus-style="${attrEscape(ast.name)}">\n${css}\n</style>${html}`;
|
||||
const pageStyleTag = localStyleTag(ast, styles);
|
||||
if (pageStyleTag) {
|
||||
html = `${pageStyleTag}${html}`;
|
||||
}
|
||||
const pageStyleExport = localStyleExport(ast, styles);
|
||||
if (pageStyleExport) out.push(pageStyleExport);
|
||||
if (csrBindings.length > 0) {
|
||||
out.push(`export const __wrnexusCsr = ${JSON.stringify(csrBindings, null, 2)};`);
|
||||
}
|
||||
@@ -1653,12 +1685,7 @@ function generateComponent(ast: PageAst): string {
|
||||
)
|
||||
.join("");
|
||||
const styles = ast.styles.map((body) => body.trim()).filter(Boolean);
|
||||
const styleTag =
|
||||
styles.length > 0
|
||||
? escLit(
|
||||
`<style data-wrnexus-style="${attrEscape(ast.name)}">\n${styles.map(styleEscape).join("\n")}\n</style>`,
|
||||
)
|
||||
: "";
|
||||
const styleTag = escLit(localStyleTag(ast, styles));
|
||||
|
||||
// A component needs a reactive scope only when it has state or event handlers.
|
||||
// Prop-driven text/attributes are baked server-side, so static components ship
|
||||
@@ -1740,6 +1767,8 @@ function generateComponent(ast: PageAst): string {
|
||||
if (Object.keys(ast.security).length > 0) {
|
||||
out.push(`export const __wrnexusSecurity = ${JSON.stringify(ast.security, null, 2)};`);
|
||||
}
|
||||
const componentStyleExport = localStyleExport(ast, styles);
|
||||
if (componentStyleExport) out.push(componentStyleExport);
|
||||
|
||||
if (behavior) {
|
||||
out.push(`export const __wrnexusBehavior = ${JSON.stringify(behavior, null, 2)};`);
|
||||
|
||||
@@ -1343,3 +1343,26 @@ page Pricing {
|
||||
|
||||
expect(code).toContain('billingCycle === "annual"');
|
||||
});
|
||||
|
||||
test("style blocks compile with stable page/component/layout metadata", () => {
|
||||
const page = compileWireFile(`page StyledPage {
|
||||
style { .shared { color: red; } }
|
||||
view { <main class="shared">Page</main> }
|
||||
}`);
|
||||
const component = compileWireFile(`component StyledCard {
|
||||
style { .shared { color: blue; } }
|
||||
view { <article class="shared">Card</article> }
|
||||
}`);
|
||||
const layout = compileWireFile(`layout StyledLayout {
|
||||
style { .shared { color: green; } }
|
||||
view { <div class="shared"><slot></slot></div> }
|
||||
}`);
|
||||
|
||||
expect(page).toContain('data-wrnexus-style-kind="page"');
|
||||
expect(component).toContain('data-wrnexus-style-kind="component"');
|
||||
expect(layout).toContain('data-wrnexus-style-kind="layout"');
|
||||
expect(page).toContain("export const __wrnexusStyles");
|
||||
expect(component).toContain("export const __wrnexusStyles");
|
||||
expect(layout).toContain("export const __wrnexusStyles");
|
||||
expect(page).toMatch(/data-wrnexus-style-id="wrn-page-[a-z0-9]+"/);
|
||||
});
|
||||
|
||||
@@ -69,6 +69,59 @@ export const NAV_RUNTIME = String.raw`
|
||||
return rel.indexOf("external") === -1;
|
||||
}
|
||||
|
||||
function currentDocumentNonce() {
|
||||
var node = document.querySelector("script[nonce],style[nonce]");
|
||||
if (!node) return "";
|
||||
return node.nonce || node.getAttribute("nonce") || "";
|
||||
}
|
||||
|
||||
function syncWrnStyles(nextDocument) {
|
||||
var current = Array.prototype.slice.call(
|
||||
document.querySelectorAll("style[data-wrnexus-style-id]"),
|
||||
);
|
||||
var next = Array.prototype.slice.call(
|
||||
nextDocument.querySelectorAll("style[data-wrnexus-style-id]"),
|
||||
);
|
||||
var currentById = Object.create(null);
|
||||
var nextIds = Object.create(null);
|
||||
var nonce = currentDocumentNonce();
|
||||
|
||||
current.forEach(function (style) {
|
||||
var id = style.getAttribute("data-wrnexus-style-id");
|
||||
if (id) currentById[id] = style;
|
||||
});
|
||||
|
||||
next.forEach(function (nextStyle) {
|
||||
var id = nextStyle.getAttribute("data-wrnexus-style-id");
|
||||
if (!id) return;
|
||||
nextIds[id] = true;
|
||||
|
||||
var currentStyle = currentById[id];
|
||||
if (!currentStyle) {
|
||||
currentStyle = document.createElement("style");
|
||||
currentStyle.setAttribute("data-wrnexus-style-id", id);
|
||||
}
|
||||
|
||||
["data-wrnexus-style-owner", "data-wrnexus-style-kind"].forEach(function (name) {
|
||||
var value = nextStyle.getAttribute(name);
|
||||
if (value == null) currentStyle.removeAttribute(name);
|
||||
else currentStyle.setAttribute(name, value);
|
||||
});
|
||||
|
||||
if (nonce) currentStyle.setAttribute("nonce", nonce);
|
||||
if (currentStyle.textContent !== nextStyle.textContent) {
|
||||
currentStyle.textContent = nextStyle.textContent;
|
||||
}
|
||||
|
||||
document.head.appendChild(currentStyle);
|
||||
});
|
||||
|
||||
current.forEach(function (style) {
|
||||
var id = style.getAttribute("data-wrnexus-style-id");
|
||||
if (id && !nextIds[id]) style.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function loadedScriptPaths() {
|
||||
var loaded = {};
|
||||
|
||||
@@ -317,6 +370,8 @@ export const NAV_RUNTIME = String.raw`
|
||||
document.title = doc.title;
|
||||
}
|
||||
|
||||
syncWrnStyles(doc);
|
||||
|
||||
var importedNodes = [];
|
||||
|
||||
for (
|
||||
|
||||
@@ -126,3 +126,24 @@ test("unmounts and remounts package runtimes during client navigation", async ()
|
||||
expect(mounts).toBe(1);
|
||||
expect(win.document.getElementById("app").textContent).toContain("New");
|
||||
});
|
||||
|
||||
test("synchronizes page styles during client navigation and preserves the current nonce", async () => {
|
||||
install(
|
||||
`<script nonce="current-nonce"></script>` +
|
||||
`<style data-wrnexus-style-id="home" data-wrnexus-style-kind="page" nonce="current-nonce">.page{color:red}</style>` +
|
||||
`<div id="app"><a href="/about" id="lnk">About</a></div>`,
|
||||
);
|
||||
nextHtml =
|
||||
`<html><head>` +
|
||||
`<style data-wrnexus-style-id="about" data-wrnexus-style-owner="About" data-wrnexus-style-kind="page" nonce="response-nonce">.page{color:blue}</style>` +
|
||||
`</head><body><div id="app"><h1 class="page">About</h1></div></body></html>`;
|
||||
|
||||
win.document.getElementById("lnk").click();
|
||||
await flush();
|
||||
|
||||
expect(win.document.querySelector('style[data-wrnexus-style-id="home"]')).toBeNull();
|
||||
const style = win.document.querySelector('style[data-wrnexus-style-id="about"]');
|
||||
expect(style).not.toBeNull();
|
||||
expect(style.textContent).toContain("color:blue");
|
||||
expect(style.getAttribute("nonce")).toBe("current-nonce");
|
||||
});
|
||||
|
||||
@@ -333,3 +333,39 @@ test("data-for preserves arrays of objects from encoded component scope", () =>
|
||||
"Four",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
test("$emit inside component functions does not depend on a global browser event", () => {
|
||||
const behavior = Buffer.from(
|
||||
JSON.stringify({
|
||||
functions: `
|
||||
function showOverlay(sourceEvent) {
|
||||
visible = true
|
||||
$emit("open", { source: sourceEvent.type })
|
||||
}
|
||||
`,
|
||||
watches: [],
|
||||
lifecycle: {},
|
||||
}),
|
||||
).toString("base64");
|
||||
|
||||
delete (globalThis as Record<string, unknown>).event;
|
||||
|
||||
const win = mount(
|
||||
`<div data-scope="visible: false" data-wrn-events="open" data-wrn-behavior="${behavior}">
|
||||
<button data-on-click="showOverlay(event)">Open overlay</button>
|
||||
<span>{visible}</span>
|
||||
</div>`,
|
||||
);
|
||||
|
||||
const root = win.document.querySelector("[data-wrn-events]")!;
|
||||
let detail: Record<string, unknown> | undefined;
|
||||
root.addEventListener("open", (event) => {
|
||||
detail = (event as unknown as CustomEvent).detail;
|
||||
});
|
||||
|
||||
win.document.querySelector("button")!.click();
|
||||
|
||||
expect(win.document.querySelector("span")!.textContent).toBe("true");
|
||||
expect(detail).toEqual({ source: "click" });
|
||||
});
|
||||
|
||||
@@ -46,7 +46,9 @@ function jsResponse(code: string): Response {
|
||||
return new Response(code, {
|
||||
headers: {
|
||||
"content-type": "text/javascript; charset=utf-8",
|
||||
"cache-control": "no-cache",
|
||||
"cache-control": "no-store, max-age=0",
|
||||
pragma: "no-cache",
|
||||
expires: "0",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -607,6 +607,7 @@ export const HMR_CLIENT_JS = `
|
||||
var from = document.getElementById("app");
|
||||
var to = doc.getElementById("app");
|
||||
|
||||
syncWrnStyles(doc);
|
||||
if (from && to) {
|
||||
morph(from, to);
|
||||
}
|
||||
@@ -638,6 +639,62 @@ export const HMR_CLIENT_JS = `
|
||||
);
|
||||
}
|
||||
|
||||
function currentDocumentNonce() {
|
||||
var node = document.querySelector("script[nonce],style[nonce]");
|
||||
if (!node) return "";
|
||||
return node.nonce || node.getAttribute("nonce") || "";
|
||||
}
|
||||
|
||||
function syncWrnStyles(nextDocument) {
|
||||
var current = Array.prototype.slice.call(
|
||||
document.querySelectorAll("style[data-wrnexus-style-id]"),
|
||||
);
|
||||
var next = Array.prototype.slice.call(
|
||||
nextDocument.querySelectorAll("style[data-wrnexus-style-id]"),
|
||||
);
|
||||
var currentById = Object.create(null);
|
||||
var nextIds = Object.create(null);
|
||||
var nonce = currentDocumentNonce();
|
||||
|
||||
current.forEach(function (style) {
|
||||
var id = style.getAttribute("data-wrnexus-style-id");
|
||||
if (id) currentById[id] = style;
|
||||
});
|
||||
|
||||
next.forEach(function (nextStyle) {
|
||||
var id = nextStyle.getAttribute("data-wrnexus-style-id");
|
||||
if (!id) return;
|
||||
nextIds[id] = true;
|
||||
|
||||
var currentStyle = currentById[id];
|
||||
if (!currentStyle) {
|
||||
currentStyle = document.createElement("style");
|
||||
currentStyle.setAttribute("data-wrnexus-style-id", id);
|
||||
}
|
||||
|
||||
["data-wrnexus-style-owner", "data-wrnexus-style-kind"].forEach(function (name) {
|
||||
var value = nextStyle.getAttribute(name);
|
||||
if (value == null) currentStyle.removeAttribute(name);
|
||||
else currentStyle.setAttribute(name, value);
|
||||
});
|
||||
|
||||
if (nonce) currentStyle.setAttribute("nonce", nonce);
|
||||
if (currentStyle.textContent !== nextStyle.textContent) {
|
||||
currentStyle.textContent = nextStyle.textContent;
|
||||
}
|
||||
|
||||
// Appending an existing node moves it. This keeps every local style after
|
||||
// global stylesheets and in the exact layout -> page -> component order
|
||||
// produced by the incoming SSR document.
|
||||
document.head.appendChild(currentStyle);
|
||||
});
|
||||
|
||||
current.forEach(function (style) {
|
||||
var id = style.getAttribute("data-wrnexus-style-id");
|
||||
if (id && !nextIds[id]) style.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Minimal index-based DOM morph: preserve matching nodes (keeps state/focus),
|
||||
// patch text and attributes, clone genuinely new nodes, drop removed ones.
|
||||
// Preserve a hydrated subtree only while its server hydration signature and
|
||||
@@ -1401,6 +1458,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
.join("\n") || undefined,
|
||||
htmlAttrs,
|
||||
documentTemplate,
|
||||
styleNonce: (ctx.locals.cspNonce as string | undefined) ?? undefined,
|
||||
});
|
||||
// Conditional GET: hash the page CONTENT (`body`), not the assembled shell —
|
||||
// the shell carries a per-request CSP nonce in dev, which would otherwise make
|
||||
|
||||
@@ -26,3 +26,11 @@ test("HMR preserves the active browser theme and accent", () => {
|
||||
|
||||
expect(HMR_CLIENT_JS).toContain('new CustomEvent("wrnexus:hmr-updated"');
|
||||
});
|
||||
|
||||
test("HMR synchronizes local WRN styles with the current document nonce", () => {
|
||||
expect(HMR_CLIENT_JS).toContain("style[data-wrnexus-style-id]");
|
||||
expect(HMR_CLIENT_JS).toContain("function currentDocumentNonce()");
|
||||
expect(HMR_CLIENT_JS).toContain('currentStyle.setAttribute("nonce", nonce)');
|
||||
expect(HMR_CLIENT_JS).toContain("document.head.appendChild(currentStyle)");
|
||||
expect(HMR_CLIENT_JS).toContain("syncWrnStyles(doc)");
|
||||
});
|
||||
|
||||
+120
-6
@@ -41,6 +41,8 @@ export interface RenderOptions {
|
||||
defaultTitle?: string;
|
||||
/** Raw HTML injected at the end of `<head>` (trusted, framework-controlled). */
|
||||
extraHead?: string;
|
||||
/** CSP nonce applied to framework-promoted `.wrn` style blocks. */
|
||||
styleNonce?: string;
|
||||
/** Raw HTML injected at the end of `<body>` (trusted, framework-controlled). */
|
||||
extraBody?: string;
|
||||
/** Attributes for the `<html>` element, e.g. ` data-theme="dark"` (trusted). */
|
||||
@@ -64,6 +66,11 @@ export function renderDocument(opts: RenderOptions): string {
|
||||
const title = escapeHtml(seo.title);
|
||||
const scripts = normalizeScripts(opts.scripts ?? []);
|
||||
|
||||
const sourceDocument = opts.documentTemplate?.trim();
|
||||
const extracted = extractWrnexusStyles(sourceDocument ?? opts.body, opts.styleNonce);
|
||||
const renderedBody = sourceDocument ? opts.body : extracted.html;
|
||||
const renderedDocument = sourceDocument ? extracted.html : undefined;
|
||||
|
||||
const seoTags = renderSeoTags(seo);
|
||||
const preloadTags = scripts
|
||||
.filter((script) => (script.type ?? "module") === "module")
|
||||
@@ -71,15 +78,16 @@ export function renderDocument(opts: RenderOptions): string {
|
||||
.join("");
|
||||
const scriptTags = scripts.map((script) => `\n ${renderScriptTag(script)}`).join("");
|
||||
|
||||
const extraHead = opts.extraHead ? `\n ${opts.extraHead}` : "";
|
||||
const globalHead = opts.extraHead ? `\n ${opts.extraHead}` : "";
|
||||
const localStyles = extracted.styles ? `\n ${extracted.styles}` : "";
|
||||
const extraBody = opts.extraBody ? `\n ${opts.extraBody}` : "";
|
||||
|
||||
const htmlAttrs = /(?:^|\s)lang\s*=/.test(opts.htmlAttrs ?? "")
|
||||
? (opts.htmlAttrs ?? "")
|
||||
: `${opts.htmlAttrs ?? ""} lang="en"`;
|
||||
|
||||
if (opts.documentTemplate) {
|
||||
let document = opts.documentTemplate.trim();
|
||||
if (renderedDocument) {
|
||||
let document = renderedDocument;
|
||||
if (!/^<!doctype\s+html>/i.test(document)) document = `<!doctype html>\n${document}`;
|
||||
document = document.replace(/<html([^>]*)>/i, (_match, existing: string) => {
|
||||
const language = /(?:^|\s)lang\s*=/.test(`${existing}${opts.htmlAttrs ?? ""}`)
|
||||
@@ -91,8 +99,9 @@ export function renderDocument(opts: RenderOptions): string {
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<title>${title}</title>${seoTags}${preloadTags}${extraHead}`;
|
||||
<title>${title}</title>${seoTags}${preloadTags}${globalHead}`;
|
||||
document = document.replace(/<head([^>]*)>/i, `<head$1>${headContent}`);
|
||||
document = appendBeforeClosingTag(document, "head", localStyles);
|
||||
document = document.replace(/<\/body>/i, `${scriptTags}${extraBody}\n </body>`);
|
||||
return `${document}\n`;
|
||||
}
|
||||
@@ -103,15 +112,120 @@ export function renderDocument(opts: RenderOptions): string {
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<title>${title}</title>${seoTags}${preloadTags}${extraHead}
|
||||
<title>${title}</title>${seoTags}${preloadTags}${globalHead}${localStyles}
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">${opts.body}</div>${scriptTags}${extraBody}
|
||||
<div id="app">${renderedBody}</div>${scriptTags}${extraBody}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
interface ExtractedStyle {
|
||||
id: string;
|
||||
owner: string;
|
||||
kind: "layout" | "page" | "component";
|
||||
css: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
interface ExtractedWrnexusStyles {
|
||||
html: string;
|
||||
styles: string;
|
||||
}
|
||||
|
||||
const WRNEXUS_STYLE_RE = /<style\b([^>]*)>([\s\S]*?)<\/style\s*>/gi;
|
||||
|
||||
function appendBeforeClosingTag(document: string, tag: string, html: string): string {
|
||||
if (!html) return document;
|
||||
const closing = new RegExp(`</${tag}\\s*>`, "i");
|
||||
return closing.test(document)
|
||||
? document.replace(closing, `${html}\n </${tag}>`)
|
||||
: `${document}${html}`;
|
||||
}
|
||||
|
||||
function readHtmlAttribute(attributes: string, name: string): string | undefined {
|
||||
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const match = attributes.match(
|
||||
new RegExp(`(?:^|\\s)${escaped}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i"),
|
||||
);
|
||||
return match?.[1] ?? match?.[2] ?? match?.[3];
|
||||
}
|
||||
|
||||
function stableStyleHash(value: string): string {
|
||||
let hash = 0x811c9dc5;
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
hash ^= value.charCodeAt(index);
|
||||
hash = Math.imul(hash, 0x01000193);
|
||||
}
|
||||
return (hash >>> 0).toString(36);
|
||||
}
|
||||
|
||||
function normalizeStyleKind(value: string | undefined): ExtractedStyle["kind"] {
|
||||
return value === "layout" || value === "page" ? value : "component";
|
||||
}
|
||||
|
||||
function styleKindOrder(kind: ExtractedStyle["kind"]): number {
|
||||
return kind === "layout" ? 0 : kind === "page" ? 1 : 2;
|
||||
}
|
||||
|
||||
function escapeStyleAttribute(value: string): string {
|
||||
return escapeHtml(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Promote compiler-emitted `.wrn` style blocks out of rendered markup and into
|
||||
* the document head. They are emitted after global stylesheets, deduplicated by
|
||||
* stable id, ordered layout -> page -> component, and nonce-tagged for CSP.
|
||||
*/
|
||||
export function extractWrnexusStyles(html: string, nonce?: string): ExtractedWrnexusStyles {
|
||||
const found: ExtractedStyle[] = [];
|
||||
let sourceOrder = 0;
|
||||
|
||||
const cleaned = html.replace(
|
||||
WRNEXUS_STYLE_RE,
|
||||
(whole: string, attributes: string, css: string) => {
|
||||
const legacyOwner = readHtmlAttribute(attributes, "data-wrnexus-style");
|
||||
const explicitId = readHtmlAttribute(attributes, "data-wrnexus-style-id");
|
||||
if (!legacyOwner && !explicitId) return whole;
|
||||
|
||||
const owner =
|
||||
readHtmlAttribute(attributes, "data-wrnexus-style-owner") ?? legacyOwner ?? "anonymous";
|
||||
const kind = normalizeStyleKind(readHtmlAttribute(attributes, "data-wrnexus-style-kind"));
|
||||
const id = explicitId ?? `wrn-${kind}-${stableStyleHash(`${kind}:${owner}`)}`;
|
||||
|
||||
found.push({ id, owner, kind, css: css.trim(), order: sourceOrder++ });
|
||||
return "";
|
||||
},
|
||||
);
|
||||
|
||||
const byId = new Map<string, ExtractedStyle>();
|
||||
for (const style of found) {
|
||||
const current = byId.get(style.id);
|
||||
if (!current) {
|
||||
byId.set(style.id, style);
|
||||
continue;
|
||||
}
|
||||
// Repeated mounts of one component carry identical CSS. Keep the latest
|
||||
// body only when it differs while preserving the original cascade position.
|
||||
if (current.css !== style.css) byId.set(style.id, { ...style, order: current.order });
|
||||
}
|
||||
|
||||
const nonceAttribute = nonce ? ` nonce="${escapeStyleAttribute(nonce)}"` : "";
|
||||
const styles = [...byId.values()]
|
||||
.sort((left, right) => {
|
||||
const kind = styleKindOrder(left.kind) - styleKindOrder(right.kind);
|
||||
return kind || left.order - right.order;
|
||||
})
|
||||
.map(
|
||||
(style) =>
|
||||
`<style data-wrnexus-style-id="${escapeStyleAttribute(style.id)}" data-wrnexus-style-owner="${escapeStyleAttribute(style.owner)}" data-wrnexus-style-kind="${style.kind}"${nonceAttribute}>\n${style.css.replace(/<\/style/gi, "<\\/style")}\n</style>`,
|
||||
)
|
||||
.join("\n ");
|
||||
|
||||
return { html: cleaned, styles };
|
||||
}
|
||||
|
||||
function normalizeScripts(scripts: readonly RenderScript[]): ScriptAsset[] {
|
||||
const bySrc = new Map<string, ScriptAsset>();
|
||||
for (const script of scripts) {
|
||||
|
||||
@@ -88,3 +88,60 @@ test("renders rich script metadata and deduplicates by source", () => {
|
||||
expect(html).toContain('data-wrnexus-runtime-src="captcha"');
|
||||
expect(html).not.toContain('rel="modulepreload" href="/__wrnexus/assets/captcha.abc.js"');
|
||||
});
|
||||
|
||||
test("promotes WRN style blocks into head after global CSS with CSP nonce", () => {
|
||||
const html = renderDocument({
|
||||
meta: { title: "Styled" },
|
||||
extraHead: '<link rel="stylesheet" href="/global.css" />',
|
||||
styleNonce: "nonce-123",
|
||||
body:
|
||||
'<style data-wrnexus-style="Card" data-wrnexus-style-id="card" data-wrnexus-style-kind="component">.shared{color:blue}</style>' +
|
||||
'<style data-wrnexus-style="Page" data-wrnexus-style-id="page" data-wrnexus-style-kind="page">.shared{color:red}</style>' +
|
||||
'<style data-wrnexus-style="Layout" data-wrnexus-style-id="layout" data-wrnexus-style-kind="layout">.shared{color:green}</style>' +
|
||||
'<main class="shared">Styled</main>',
|
||||
});
|
||||
|
||||
expect(html).not.toContain('<div id="app"><style');
|
||||
expect(html).toContain('<main class="shared">Styled</main>');
|
||||
expect(html.match(/data-wrnexus-style-id=/g)).toHaveLength(3);
|
||||
expect(html.match(/nonce="nonce-123"/g)).toHaveLength(3);
|
||||
|
||||
const globalIndex = html.indexOf("/global.css");
|
||||
const layoutIndex = html.indexOf('data-wrnexus-style-id="layout"');
|
||||
const pageIndex = html.indexOf('data-wrnexus-style-id="page"');
|
||||
const componentIndex = html.indexOf('data-wrnexus-style-id="card"');
|
||||
expect(globalIndex).toBeLessThan(layoutIndex);
|
||||
expect(layoutIndex).toBeLessThan(pageIndex);
|
||||
expect(pageIndex).toBeLessThan(componentIndex);
|
||||
});
|
||||
|
||||
test("deduplicates repeated component style blocks", () => {
|
||||
const style =
|
||||
'<style data-wrnexus-style="Card" data-wrnexus-style-id="card" data-wrnexus-style-kind="component">.card{display:grid}</style>';
|
||||
const html = renderDocument({
|
||||
meta: { title: "Repeated" },
|
||||
body: `${style}<article>One</article>${style}<article>Two</article>`,
|
||||
});
|
||||
|
||||
expect(html.match(/data-wrnexus-style-id="card"/g)).toHaveLength(1);
|
||||
expect(html).toContain("<article>One</article><article>Two</article>");
|
||||
});
|
||||
|
||||
test("places document-layout WRN styles at the end of head", () => {
|
||||
const html = renderDocument({
|
||||
meta: { title: "Document styles" },
|
||||
styleNonce: "document-nonce",
|
||||
body: "",
|
||||
documentTemplate:
|
||||
'<html><head><link rel="stylesheet" href="/document-global.css" /></head><body>' +
|
||||
'<style data-wrnexus-style="Document" data-wrnexus-style-id="document-layout" data-wrnexus-style-kind="layout">.shell{display:block}</style>' +
|
||||
'<div id="app" class="shell">Page</div></body></html>',
|
||||
});
|
||||
|
||||
const globalIndex = html.indexOf("/document-global.css");
|
||||
const styleIndex = html.indexOf('data-wrnexus-style-id="document-layout"');
|
||||
const headEnd = html.indexOf("</head>");
|
||||
expect(globalIndex).toBeLessThan(styleIndex);
|
||||
expect(styleIndex).toBeLessThan(headEnd);
|
||||
expect(html).not.toContain("<body><style");
|
||||
});
|
||||
|
||||
+197
-193
@@ -49,15 +49,6 @@ Secure multi-cell PIN and verification-code input with regex and paste support.
|
||||
- Slots: None
|
||||
- Events: `input`, `change`, `complete`, `paste`, `clear`, `error`
|
||||
|
||||
### SearchBox
|
||||
|
||||
Theme-aware, responsive search box component.
|
||||
|
||||
- Mount: `data-component="SearchBox"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Search Box"`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "search"`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Events: None
|
||||
|
||||
### StrongPassword
|
||||
|
||||
Theme-aware, responsive strong password component.
|
||||
@@ -161,7 +152,7 @@ Theme-aware, responsive button group component.
|
||||
|
||||
### Card
|
||||
|
||||
Theme-aware, responsive card component.
|
||||
Group related content in a responsive themed surface with title, description, content, and supporting slots.
|
||||
|
||||
- Mount: `data-component="Card"`
|
||||
- Props: `title: string = "Card title"`, `subtitle: string = ""`, `description: string = ""`, `header: string = ""`, `footer: string = ""`, `imageSrc: string = ""`, `imageAlt: string = ""`, `imagePosition: string = "top"`, `actionLabel: string = ""`, `actionHref: string = ""`, `headerActions: string = []`, `navigation: string = []`, `activeNav: string = ""`, `mobileNavigation: boolean = false`, `alertTitle: string = ""`, `alertDescription: string = ""`, `empty: boolean = false`, `emptyTitle: string = "No data to show"`, `emptyIcon: string = "icon-[lucide--inbox]"`, `items: string = []`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `layout: string = "vertical"`, `align: string = "left"`, `hover: string = "none"`, `scrollable: boolean = false`, `maxHeight: string = "18rem"`, `dismissible: boolean = false`, `ariaLabel: string = ""`, `class: string = ""`
|
||||
@@ -222,15 +213,6 @@ Theme-aware, responsive file upload progress component.
|
||||
- Slots: None
|
||||
- Events: `cancel`, `retry`, `complete`
|
||||
|
||||
### HeroActions
|
||||
|
||||
Responsive action group for hero and call-to-action sections.
|
||||
|
||||
- Mount: `data-component="HeroActions"`
|
||||
- Props: `actions: string = []`, `align: string = "left"`, `orientation: string = "horizontal"`, `stackOnMobile: boolean = true`, `fullWidthMobile: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: None
|
||||
|
||||
### LegendIndicator
|
||||
|
||||
Theme-aware, responsive legend indicator component.
|
||||
@@ -242,7 +224,7 @@ Theme-aware, responsive legend indicator component.
|
||||
|
||||
### List
|
||||
|
||||
Theme-aware, responsive list component.
|
||||
Present structured responsive linked or status items with icons, descriptions, actions, and selection events.
|
||||
|
||||
- Mount: `data-component="List"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "List"`, `description: string = ""`, `items: string = []`, `variant: string = "default"`, `class: string = ""`
|
||||
@@ -260,7 +242,7 @@ Theme-aware, responsive list group component.
|
||||
|
||||
### Marquee
|
||||
|
||||
Theme-aware, responsive marquee component.
|
||||
Continuously present responsive labels, partners, notices, or capabilities with pause and resume behavior.
|
||||
|
||||
- Mount: `data-component="Marquee"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Marquee"`, `description: string = ""`, `items: string = []`, `variant: string = "default"`, `class: string = ""`
|
||||
@@ -314,7 +296,7 @@ Theme-aware, responsive styled icon component.
|
||||
|
||||
### Timeline
|
||||
|
||||
Theme-aware, responsive timeline component.
|
||||
Present responsive chronological activity, milestones, or workflow status with rich item metadata.
|
||||
|
||||
- Mount: `data-component="Timeline"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Timeline"`, `description: string = ""`, `items: string = []`, `variant: string = "default"`, `class: string = ""`
|
||||
@@ -339,101 +321,67 @@ Theme-aware, responsive tree view component.
|
||||
- Slots: `default`
|
||||
- Events: `select`, `toggle`, `expand`, `collapse`
|
||||
|
||||
## Blocks
|
||||
## Core
|
||||
|
||||
### AnnouncementBar
|
||||
### AuthForm
|
||||
|
||||
Accessible public announcement, emergency, maintenance, or status bar.
|
||||
Reusable auth form component.
|
||||
|
||||
- Mount: `data-component="AnnouncementBar"`
|
||||
- Props: `badge: string = ""`, `badgeIcon: string = ""`, `message: string = "Announcement"`, `description: string = ""`, `icon: string = "icon-[lucide--megaphone]"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Dismiss announcement"`, `sticky: boolean = false`, `compact: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "soft"`, `role: string = "status"`, `live: string = "polite"`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Events: None
|
||||
- Mount: `data-component="AuthForm"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `mode: string = "sign-in"`, `action: string = "/api/auth/login"`, `method: string = "post"`, `title: string = "Sign in"`, `description: string = ""`, `returnTo: string = ""`, `schema: string = ""`, `showRemember: boolean = true`, `showName: boolean = true`, `submitLabel: string = "Continue"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: `submit`, `change`, `input`, `focus`, `blur`
|
||||
|
||||
### CTASection
|
||||
### AuthSplitLayout
|
||||
|
||||
Reusable closing call-to-action section with primary and secondary actions.
|
||||
Reusable auth split layout component.
|
||||
|
||||
- Mount: `data-component="CTASection"`
|
||||
- Props: `eyebrow: string = ""`, `title: string = "Ready to get started?"`, `description: string = ""`, `icon: string = ""`, `align: string = "center"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `primaryLabel: string = "Get started"`, `primaryHref: string = "#"`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `backgroundImage: string = ""`, `maxWidth: string = "xl"`, `class: string = ""`
|
||||
- Slots: `default`, `actions`, `visual`, `footer`
|
||||
- Events: None
|
||||
|
||||
### FeatureCard
|
||||
|
||||
Reusable linked feature or service card with icon, badge, description, and action.
|
||||
|
||||
- Mount: `data-component="FeatureCard"`
|
||||
- Props: `icon: string = ""`, `title: string = "Feature"`, `description: string = ""`, `href: string = ""`, `actionLabel: string = "Learn more"`, `actionIcon: string = ""`, `badge: string = ""`, `badgeColor: string = "primary"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `hover: string = "lift"`, `align: string = "left"`, `disabled: boolean = false`, `class: string = ""`
|
||||
- Slots: `icon`, `default`, `footer`
|
||||
- Events: None
|
||||
|
||||
### FeatureIconCard
|
||||
|
||||
Feature card with a prominent themed icon treatment.
|
||||
|
||||
- Mount: `data-component="FeatureIconCard"`
|
||||
- Props: `icon: string = "icon-[lucide--sparkles]"`, `iconSize: string = "md"`, `iconVariant: string = "soft"`, `title: string = "Feature"`, `description: string = ""`, `href: string = ""`, `actionLabel: string = "Explore"`, `badge: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `align: string = "left"`, `hover: string = "lift"`, `class: string = ""`
|
||||
- Slots: `default`, `footer`
|
||||
- Events: None
|
||||
|
||||
### Hero
|
||||
|
||||
Reusable public-page hero with actions, trust items, badges, and visual slots.
|
||||
|
||||
- Mount: `data-component="Hero"`
|
||||
- Props: `eyebrow: string = ""`, `title: string = "Build something remarkable"`, `highlight: string = ""`, `description: string = ""`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `tertiaryLabel: string = ""`, `tertiaryHref: string = ""`, `badges: string = []`, `trustItems: string = []`, `maxWidth: string = "xl"`, `class: string = ""`
|
||||
- Slots: `eyebrow`, `actions`, `trust`, `default`, `visual`, `footer`
|
||||
- Events: None
|
||||
|
||||
### MarketingSectionHeader
|
||||
|
||||
Marketing section heading with an optional linked action.
|
||||
|
||||
- Mount: `data-component="MarketingSectionHeader"`
|
||||
- Props: `id: string = ""`, `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `align: string = "split"`, `size: string = "default"`, `color: string = "primary"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `actionExternal: boolean = false`, `class: string = ""`
|
||||
- Slots: `icon`, `actions`, `default`
|
||||
- Events: None
|
||||
|
||||
### MetricCard
|
||||
|
||||
Single statistic or operational metric with icon, trend, and optional action.
|
||||
|
||||
- Mount: `data-component="MetricCard"`
|
||||
- Props: `label: string = "Metric"`, `value: string = "0"`, `description: string = ""`, `icon: string = ""`, `prefix: string = ""`, `suffix: string = ""`, `trend: string = ""`, `trendLabel: string = ""`, `trendDirection: string = "neutral"`, `href: string = ""`, `actionLabel: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `align: string = "left"`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Events: None
|
||||
|
||||
### PageHeader
|
||||
|
||||
Public and application page header with breadcrumbs, metadata, and actions.
|
||||
|
||||
- Mount: `data-component="PageHeader"`
|
||||
- Props: `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `icon: string = ""`, `align: string = "left"`, `size: string = "default"`, `compact: boolean = false`, `showBreadcrumbs: boolean = false`, `breadcrumbs: string = []`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`
|
||||
- Slots: `meta`, `actions`, `default`
|
||||
- Mount: `data-component="AuthSplitLayout"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `eyebrow: string = "Secure identity"`, `title: string = "Welcome back"`, `description: string = ""`, `brand: string = "Police Management System"`, `features: string = []`, `class: string = ""`
|
||||
- Slots: `aside-extra`, `form`
|
||||
- Events: None
|
||||
|
||||
### PortalDashboard
|
||||
|
||||
Responsive portal dashboard with metrics, actions, tasks, and updates.
|
||||
Reusable portal dashboard component.
|
||||
|
||||
- Mount: `data-component="PortalDashboard"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `eyebrow: string = "Overview"`, `eyebrowKey: string = ""`, `title: string = "Dashboard"`, `titleKey: string = ""`, `description: string = ""`, `descriptionKey: string = ""`, `userName: string = ""`, `metrics: string = []`, `actions: string = []`, `updates: string = []`, `tasks: string = []`, `class: string = ""`
|
||||
- Slots: `hero-action`
|
||||
- Events: `action`, `navigate`
|
||||
|
||||
### SplitHero
|
||||
### PreferenceSwitcher
|
||||
|
||||
Two-column hero with configurable content and visual placement.
|
||||
Reusable preference switcher component.
|
||||
|
||||
- Mount: `data-component="SplitHero"`
|
||||
- Props: `eyebrow: string = ""`, `title: string = "A better digital experience"`, `highlight: string = ""`, `description: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `visualPosition: string = "right"`, `reverse: boolean = false`, `ratio: string = "balanced"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `trustItems: string = []`, `class: string = ""`
|
||||
- Slots: `actions`, `trust`, `default`, `visual`
|
||||
- Events: None
|
||||
- Mount: `data-component="PreferenceSwitcher"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `themeLabel: string = "Theme"`, `colorLabel: string = "Accent color"`, `languageLabel: string = "Language"`, `languages: string = [`, `colors: string = [`, `compact: boolean = true`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Events: `theme`, `color`, `language`
|
||||
|
||||
## Data
|
||||
|
||||
### MetricCard
|
||||
|
||||
Display one operational metric with value, suffix, description, icon, trend, progress, and optional action.
|
||||
|
||||
- Mount: `data-component="MetricCard"`
|
||||
- Props: `label: string = "Metric"`, `value: string = "0"`, `description: string = ""`, `icon: string = ""`, `iconStyle: string = "soft"`, `prefix: string = ""`, `suffix: string = ""`, `badge: string = ""`, `trend: string = ""`, `trendLabel: string = ""`, `trendDirection: string = "neutral"`, `progress: number = -1`, `progressLabel: string = ""`, `href: string = ""`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `actionLabel: string = ""`, `actionIcon: string = ""`, `showArrow: boolean = true`, `selectable: boolean = false`, `disabled: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `hover: string = "lift"`, `align: string = "left"`, `class: string = ""`
|
||||
- Slots: `default`, `footer`
|
||||
- Events: `select`, `action`
|
||||
|
||||
### MetricGrid
|
||||
|
||||
Arrange operational metrics, KPIs, public statistics, or service indicators in a responsive equal-height grid.
|
||||
|
||||
- Mount: `data-component="MetricGrid"`
|
||||
- Props: `items: string = []`, `columns: number = 4`, `tabletColumns: number = 2`, `mobileColumns: number = 1`, `gap: string = "md"`, `equalHeight: boolean = true`, `dividers: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `maxWidth: string = "full"`, `minItemWidth: string = ""`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: `select`, `action`
|
||||
|
||||
### StatsBar
|
||||
|
||||
Compact responsive statistics strip.
|
||||
Present a compact responsive strip of key facts, counts, performance indicators, or trust signals.
|
||||
|
||||
- Mount: `data-component="StatsBar"`
|
||||
- Props: `items: string = []`, `columns: number = 4`, `compact: boolean = true`, `dividers: boolean = true`, `icons: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `maxWidth: string = "xl"`, `class: string = ""`
|
||||
@@ -442,15 +390,6 @@ Compact responsive statistics strip.
|
||||
|
||||
## Forms
|
||||
|
||||
### AuthForm
|
||||
|
||||
Reusable authentication form for sign-in, registration, recovery, reset, and MFA.
|
||||
|
||||
- Mount: `data-component="AuthForm"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `mode: string = "sign-in"`, `action: string = "/api/auth/login"`, `method: string = "post"`, `title: string = "Sign in"`, `description: string = ""`, `returnTo: string = ""`, `schema: string = ""`, `showRemember: boolean = true`, `showName: boolean = true`, `submitLabel: string = "Continue"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: `submit`, `change`, `input`, `focus`, `blur`
|
||||
|
||||
### Checkbox
|
||||
|
||||
Theme-aware, responsive checkbox component.
|
||||
@@ -514,6 +453,15 @@ Theme-aware, responsive range slider component.
|
||||
- Slots: None
|
||||
- Events: `input`, `change`, `focus`, `blur`
|
||||
|
||||
### SearchBox
|
||||
|
||||
Provide an accessible responsive search field with labels, validation states, sizes, and input or change events.
|
||||
|
||||
- Mount: `data-component="SearchBox"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Search Box"`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "search"`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Events: None
|
||||
|
||||
### Select
|
||||
|
||||
Theme-aware, responsive select component.
|
||||
@@ -635,7 +583,7 @@ Theme-aware, responsive file upload component.
|
||||
|
||||
### Map
|
||||
|
||||
Theme-aware, responsive map component.
|
||||
Present responsive location information and markers with map-ready metadata and movement or marker events.
|
||||
|
||||
- Mount: `data-component="Map"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Map"`, `description: string = ""`, `items: string = []`, `variant: string = "default"`, `class: string = ""`
|
||||
@@ -662,18 +610,9 @@ Theme-aware, responsive wysiwyg editor component.
|
||||
|
||||
## Layout
|
||||
|
||||
### AuthSplitLayout
|
||||
|
||||
Responsive 50/50 authentication layout with content and form regions.
|
||||
|
||||
- Mount: `data-component="AuthSplitLayout"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `eyebrow: string = "Secure identity"`, `title: string = "Welcome back"`, `description: string = ""`, `brand: string = "Police Management System"`, `features: string = []`, `class: string = ""`
|
||||
- Slots: `aside-extra`, `form`
|
||||
- Events: None
|
||||
|
||||
### Columns
|
||||
|
||||
Theme-aware, responsive columns component.
|
||||
Create responsive balanced content columns with configurable count, gap, density, and maximum width.
|
||||
|
||||
- Mount: `data-component="Columns"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
|
||||
@@ -682,7 +621,7 @@ Theme-aware, responsive columns component.
|
||||
|
||||
### Container
|
||||
|
||||
Theme-aware, responsive container component.
|
||||
Constrain and align page content with responsive gutters and compact, wide, or full width options.
|
||||
|
||||
- Mount: `data-component="Container"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
|
||||
@@ -700,7 +639,7 @@ Theme-aware, responsive custom scrollbar component.
|
||||
|
||||
### Divider
|
||||
|
||||
Theme-aware, responsive divider component.
|
||||
Separate related horizontal or vertical content with optional labels, sizes, and semantic colors.
|
||||
|
||||
- Mount: `data-component="Divider"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
|
||||
@@ -709,16 +648,25 @@ Theme-aware, responsive divider component.
|
||||
|
||||
### FeatureGrid
|
||||
|
||||
Responsive equal-height grid for feature and service cards.
|
||||
Arrange feature cards, services, solutions, or benefits in a responsive equal-height grid.
|
||||
|
||||
- Mount: `data-component="FeatureGrid"`
|
||||
- Props: `color: string = "primary"`, `size: string = "default"`, `columns: number = 3`, `tabletColumns: number = 2`, `mobileColumns: number = 1`, `gap: string = "md"`, `minItemWidth: string = ""`, `equalHeight: boolean = true`, `align: string = "stretch"`, `maxWidth: string = "full"`, `class: string = ""`
|
||||
- Props: `items: string = []`, `columns: number = 3`, `tabletColumns: number = 2`, `mobileColumns: number = 1`, `gap: string = "md"`, `minItemWidth: string = ""`, `equalHeight: boolean = true`, `align: string = "stretch"`, `maxWidth: string = "full"`, `variant: string = "default"`, `label: string = "Features"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: None
|
||||
|
||||
### Footer
|
||||
|
||||
Render structured responsive footer navigation, pre and post content, copyright content, links, and public events.
|
||||
|
||||
- Mount: `data-component="Footer"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Footer navigation"`, `items: string = []`, `columns: number = 3`, `maxWidth: string = "compact"`, `copyright: string = ""`, `class: string = ""`
|
||||
- Slots: `pre-footer`, `post-footer`, `copyright-left`, `copyright-right`
|
||||
- Events: `select`, `action`
|
||||
|
||||
### Grid
|
||||
|
||||
Theme-aware, responsive grid component.
|
||||
Arrange arbitrary content in a responsive configurable CSS grid with stable columns, gaps, alignment, and width.
|
||||
|
||||
- Mount: `data-component="Grid"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
|
||||
@@ -727,7 +675,7 @@ Theme-aware, responsive grid component.
|
||||
|
||||
### Image
|
||||
|
||||
Theme-aware, responsive image component.
|
||||
Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment.
|
||||
|
||||
- Mount: `data-component="Image"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `src: string = ""`, `alt: string = ""`, `width: string = ""`, `height: string = ""`, `loading: string = "lazy"`, `rounded: boolean = false`, `class: string = ""`
|
||||
@@ -754,25 +702,16 @@ Theme-aware, responsive layout splitter component.
|
||||
|
||||
### Link
|
||||
|
||||
Theme-aware, responsive link component.
|
||||
Render an accessible internal or external link with target, relation, size, color, and public focus or click events.
|
||||
|
||||
- Mount: `data-component="Link"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Link"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: None
|
||||
|
||||
### MetricGrid
|
||||
|
||||
Responsive collection of metric cards.
|
||||
|
||||
- Mount: `data-component="MetricGrid"`
|
||||
- Props: `items: string = []`, `columns: number = 4`, `tabletColumns: number = 2`, `mobileColumns: number = 1`, `gap: string = "md"`, `equalHeight: boolean = true`, `dividers: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: None
|
||||
|
||||
### PublicPageShell
|
||||
|
||||
Consistent structural wrapper for public-facing pages.
|
||||
Provide the outer responsive structure, width, background, slots, overflow, and minimum-height behavior for public pages.
|
||||
|
||||
- Mount: `data-component="PublicPageShell"`
|
||||
- Props: `maxWidth: string = "full"`, `fullWidth: boolean = true`, `headerOffset: string = "none"`, `background: string = "default"`, `overflow: string = "clip"`, `minHeight: string = "screen"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`
|
||||
@@ -781,7 +720,7 @@ Consistent structural wrapper for public-facing pages.
|
||||
|
||||
### Section
|
||||
|
||||
Theme-aware page section with consistent spacing, width, background, and border controls.
|
||||
Create a responsive themed page section with controlled spacing, width, borders, and surface treatment.
|
||||
|
||||
- Mount: `data-component="Section"`
|
||||
- Props: `id: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `spacing: string = "lg"`, `maxWidth: string = "xl"`, `fullWidth: boolean = false`, `borderTop: boolean = false`, `borderBottom: boolean = false`, `class: string = ""`
|
||||
@@ -790,59 +729,133 @@ Theme-aware page section with consistent spacing, width, background, and border
|
||||
|
||||
### SectionHeader
|
||||
|
||||
Reusable accessible section heading with eyebrow, title, description, icon, and actions.
|
||||
Introduce a section with an eyebrow, title, description, alignment, and responsive heading hierarchy.
|
||||
|
||||
- Mount: `data-component="SectionHeader"`
|
||||
- Props: `id: string = ""`, `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `headingLevel: number = 2`, `maxWidth: string = "3xl"`, `class: string = ""`
|
||||
- Slots: `icon`, `default`, `actions`
|
||||
- Events: None
|
||||
|
||||
### TextLink
|
||||
|
||||
Styled inline action link with icons, arrows, external state, and disabled handling.
|
||||
|
||||
- Mount: `data-component="TextLink"`
|
||||
- Props: `label: string = "Learn more"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `icon: string = ""`, `iconPosition: string = "start"`, `showArrow: boolean = true`, `underline: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `disabled: boolean = false`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Events: `click`, `focus`, `blur`
|
||||
|
||||
### Typography
|
||||
|
||||
Theme-aware, responsive typography component.
|
||||
Apply consistent readable responsive typography, widths, columns, spacing, and editorial hierarchy.
|
||||
|
||||
- Mount: `data-component="Typography"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: None
|
||||
|
||||
## Marketing
|
||||
|
||||
### AnnouncementBar
|
||||
|
||||
Publish a responsive notice with badge, icon, supporting copy, action, dismiss behavior, and width controls.
|
||||
|
||||
- Mount: `data-component="AnnouncementBar"`
|
||||
- Props: `badge: string = ""`, `badgeIcon: string = ""`, `message: string = "Announcement"`, `description: string = ""`, `icon: string = "icon-[lucide--megaphone]"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Dismiss announcement"`, `sticky: boolean = false`, `compact: boolean = false`, `size: string = "default"`, `width: string = "default"`, `color: string = "primary"`, `variant: string = "soft"`, `role: string = "status"`, `live: string = "polite"`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Events: `dismiss`
|
||||
|
||||
### CTASection
|
||||
|
||||
Close a page or major section with conversion-focused copy, actions, and optional supporting visual content.
|
||||
|
||||
- Mount: `data-component="CTASection"`
|
||||
- Props: `eyebrow: string = ""`, `title: string = "Ready to get started?"`, `description: string = ""`, `icon: string = ""`, `align: string = "center"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `primaryLabel: string = "Get started"`, `primaryHref: string = "#"`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: string = []`, `visualPosition: string = "right"`, `maxWidth: string = "xl"`, `fullBleed: boolean = false`, `class: string = ""`
|
||||
- Slots: `default`, `actions`, `visual`, `footer`
|
||||
- Events: None
|
||||
|
||||
### FeatureCard
|
||||
|
||||
Present one linked feature or service with media, icon, badge, description, and action.
|
||||
|
||||
- Mount: `data-component="FeatureCard"`
|
||||
- Props: `icon: string = ""`, `iconStyle: string = "soft"`, `iconSize: string = "default"`, `eyebrow: string = ""`, `title: string = "Feature"`, `description: string = ""`, `image: string = ""`, `imageAlt: string = ""`, `imagePosition: string = "top"`, `imageAspect: string = "wide"`, `imageLoading: string = "lazy"`, `href: string = ""`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `actionLabel: string = "Learn more"`, `actionIcon: string = ""`, `showArrow: boolean = true`, `stretchedLink: boolean = true`, `badge: string = ""`, `badgeColor: string = "primary"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `hover: string = "lift"`, `align: string = "left"`, `disabled: boolean = false`, `class: string = ""`
|
||||
- Slots: `media`, `icon`, `default`, `footer`
|
||||
- Events: None
|
||||
|
||||
### FeatureIconCard
|
||||
|
||||
Present a compact feature or benefit with a styled icon, title, description, badge, and optional link.
|
||||
|
||||
- Mount: `data-component="FeatureIconCard"`
|
||||
- Props: `icon: string = "icon-[lucide--sparkles]"`, `iconSize: string = "md"`, `iconVariant: string = "soft"`, `title: string = "Feature"`, `description: string = ""`, `href: string = ""`, `actionLabel: string = "Explore"`, `badge: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `align: string = "left"`, `hover: string = "lift"`, `class: string = ""`
|
||||
- Slots: `default`, `footer`
|
||||
- Events: None
|
||||
|
||||
### Hero
|
||||
|
||||
Build a full-width responsive hero with constrained content, actions, trust signals, and a structured or custom visual panel.
|
||||
|
||||
- Mount: `data-component="Hero"`
|
||||
- Props: `eyebrow: string = ""`, `eyebrowIcon: string = ""`, `icon: string = ""`, `title: string = "Build something remarkable"`, `highlight: string = ""`, `description: string = ""`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `layout: string = "split"`, `visualPosition: string = "right"`, `visualStyle: string = "plain"`, `showDecorations: boolean = true`, `fullBleed: boolean = true`, `visualEyebrow: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualIcon: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualItems: string = []`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `primaryTarget: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `secondaryTarget: string = ""`, `tertiaryLabel: string = ""`, `tertiaryHref: string = ""`, `tertiaryIcon: string = ""`, `tertiaryTarget: string = ""`, `badges: string = []`, `trustItems: string = []`, `maxWidth: string = "xl"`, `class: string = ""`
|
||||
- Slots: `eyebrow`, `actions`, `trust`, `default`, `visual`, `footer`
|
||||
- Events: None
|
||||
|
||||
### HeroActions
|
||||
|
||||
Group hero and campaign actions with consistent alignment, orientation, sizing, and responsive mobile stacking.
|
||||
|
||||
- Mount: `data-component="HeroActions"`
|
||||
- Props: `actions: string = []`, `align: string = "left"`, `orientation: string = "horizontal"`, `stackOnMobile: boolean = true`, `fullWidthMobile: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: None
|
||||
|
||||
### MarketingSectionHeader
|
||||
|
||||
Introduce marketing content with an eyebrow, title, description, and optional linked action.
|
||||
|
||||
- Mount: `data-component="MarketingSectionHeader"`
|
||||
- Props: `id: string = ""`, `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `align: string = "split"`, `size: string = "default"`, `color: string = "primary"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `actionExternal: boolean = false`, `class: string = ""`
|
||||
- Slots: `icon`, `actions`, `default`
|
||||
- Events: None
|
||||
|
||||
### PageHeader
|
||||
|
||||
Introduce an internal or public page with breadcrumbs, icon, title, description, and primary or secondary actions.
|
||||
|
||||
- Mount: `data-component="PageHeader"`
|
||||
- Props: `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `icon: string = ""`, `id: string = "page-title"`, `align: string = "left"`, `centered: boolean = false`, `compact: boolean = false`, `size: string = "default"`, `maxWidth: string = "xl"`, `showBreadcrumbs: boolean = false`, `breadcrumbs: string = []`, `breadcrumbParent: string = ""`, `breadcrumbParentHref: string = ""`, `breadcrumbCurrent: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `color: string = "primary"`, `variant: string = "default"`, `borderBottom: boolean = true`, `class: string = ""`
|
||||
- Slots: `meta`, `actions`, `default`
|
||||
- Events: None
|
||||
|
||||
### SplitHero
|
||||
|
||||
Build a responsive two-column introduction balancing descriptive content with a visual, image, or structured data panel.
|
||||
|
||||
- Mount: `data-component="SplitHero"`
|
||||
- Props: `eyebrow: string = ""`, `eyebrowIcon: string = "icon-[lucide--sparkles]"`, `title: string = "A better digital experience"`, `highlight: string = ""`, `description: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `visualPosition: string = "right"`, `reverse: boolean = false`, `ratio: string = "balanced"`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `maxWidth: string = "xl"`, `fullBleed: boolean = true`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualEyebrow: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: string = []`, `visualStyle: string = "panel"`, `trustItems: string = []`, `class: string = ""`
|
||||
- Slots: `default`, `actions`, `trust`, `visual`
|
||||
- Events: None
|
||||
|
||||
### TextLink
|
||||
|
||||
Render an accessible text action with optional icon, arrow, underline, external state, and semantic styling.
|
||||
|
||||
- Mount: `data-component="TextLink"`
|
||||
- Props: `label: string = "Learn more"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `icon: string = ""`, `iconPosition: string = "start"`, `showArrow: boolean = true`, `underline: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `disabled: boolean = false`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Events: `click`, `focus`, `blur`
|
||||
|
||||
## Navigation
|
||||
|
||||
### BackToTop
|
||||
|
||||
Accessible scroll-to-top control with visibility threshold.
|
||||
Provide a responsive floating control that returns long pages to the top and can show scroll progress.
|
||||
|
||||
- Mount: `data-component="BackToTop"`
|
||||
- Props: `threshold: number = 500`, `label: string = "Back to top"`, `ariaLabel: string = "Scroll back to top"`, `icon: string = "icon-[lucide--arrow-up]"`, `position: string = "right"`, `offset: string = "md"`, `behavior: string = "smooth"`, `showProgress: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `class: string = ""`
|
||||
- Props: `threshold: number = 500`, `label: string = "Back to top"`, `ariaLabel: string = "Scroll back to top"`, `icon: string = "icon-[lucide--arrow-up]"`, `position: string = "right"`, `offset: string = "md"`, `behavior: string = "smooth"`, `showProgress: boolean = false`, `showLabel: boolean = false`, `alwaysVisible: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `shape: string = "round"`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Events: None
|
||||
|
||||
### Breadcrumb
|
||||
|
||||
Theme-aware, responsive breadcrumb component.
|
||||
Show responsive hierarchical navigation with home support, separators, current-page state, sizes, and selection events.
|
||||
|
||||
- Mount: `data-component="Breadcrumb"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Breadcrumb"`, `items: string = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: `navigate`, `click`
|
||||
|
||||
### Footer
|
||||
|
||||
Responsive application footer with structured links and pre/post content slots.
|
||||
|
||||
- Mount: `data-component="Footer"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Footer navigation"`, `items: string = []`, `columns: number = 3`, `maxWidth: string = "compact"`, `copyright: string = ""`, `class: string = ""`
|
||||
- Slots: `pre-footer`, `post-footer`, `copyright-left`, `copyright-right`
|
||||
- Events: `select`, `action`
|
||||
- Props: `label: string = "Breadcrumb"`, `items: string = []`, `active: string = ""`, `separator: string = "chevron"`, `showHome: boolean = false`, `homeLabel: string = "Home"`, `homeHref: string = "/"`, `homeIcon: string = "icon-[lucide--house]"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "minimal"`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Events: `select`
|
||||
|
||||
### MegaMenu
|
||||
|
||||
@@ -880,15 +893,6 @@ Theme-aware, responsive pagination component.
|
||||
- Slots: `default`
|
||||
- Events: `change`, `previous`, `next`
|
||||
|
||||
### PreferenceSwitcher
|
||||
|
||||
Accessible theme, accent color, and language preference controls.
|
||||
|
||||
- Mount: `data-component="PreferenceSwitcher"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `themeLabel: string = "Theme"`, `colorLabel: string = "Accent color"`, `languageLabel: string = "Language"`, `languages: string = [`, `colors: string = [`, `compact: boolean = true`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Events: `theme`, `color`, `language`
|
||||
|
||||
### Scrollspy
|
||||
|
||||
Theme-aware, responsive scrollspy component.
|
||||
@@ -918,7 +922,7 @@ Theme-aware, responsive stepper component.
|
||||
|
||||
### Tabs
|
||||
|
||||
Theme-aware, responsive tabs component.
|
||||
Switch between related responsive content panels with horizontal or vertical orientation and selection events.
|
||||
|
||||
- Mount: `data-component="Tabs"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Tabs"`, `items: string = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
|
||||
@@ -929,57 +933,57 @@ Theme-aware, responsive tabs component.
|
||||
|
||||
### ContextMenu
|
||||
|
||||
Theme-aware, responsive context menu component.
|
||||
Open an accessible keyboard-aware action menu from pointer or keyboard context interactions.
|
||||
|
||||
- Mount: `data-component="ContextMenu"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Context Menu"`, `description: string = ""`, `open: boolean = false`, `placement: string = "bottom"`, `closeLabel: string = "Close"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: `open`, `close`, `select`
|
||||
- Props: `items: string = []`, `open: boolean = false`, `defaultOpen: boolean = false`, `trigger: string = "contextmenu"`, `placement: string = "pointer"`, `align: string = "start"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `title: string = ""`, `description: string = ""`, `label: string = "Context menu"`, `closeOnSelect: boolean = true`, `closeOnOutside: boolean = true`, `disabled: boolean = false`, `minWidth: string = "14rem"`, `maxWidth: string = "20rem"`, `class: string = ""`
|
||||
- Slots: `trigger`, `header`, `default`, `footer`
|
||||
- Events: `open`, `close`, `select`, `action`
|
||||
|
||||
### Drawer
|
||||
|
||||
Theme-aware, responsive drawer component.
|
||||
Present responsive modal side or bottom content with focus management, backdrop behavior, slots, and close events.
|
||||
|
||||
- Mount: `data-component="Drawer"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Drawer"`, `description: string = ""`, `open: boolean = false`, `placement: string = "bottom"`, `closeLabel: string = "Close"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: `open`, `close`
|
||||
- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `placement: string = "right"`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `title: string = "Drawer"`, `description: string = ""`, `icon: string = ""`, `label: string = "Drawer"`, `closeLabel: string = "Close drawer"`, `showClose: boolean = true`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `overlay: boolean = true`, `scrollable: boolean = true`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `class: string = ""`
|
||||
- Slots: `trigger`, `header`, `default`, `footer`
|
||||
- Events: `open`, `close`, `cancel`
|
||||
|
||||
### Dropdown
|
||||
|
||||
Theme-aware, responsive dropdown component.
|
||||
Open an accessible anchored menu with keyboard navigation, item selection, actions, and responsive placement.
|
||||
|
||||
- Mount: `data-component="Dropdown"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Open menu"`, `items: string = []`, `placement: string = "end"`, `class: string = ""`
|
||||
- Slots: `trigger`, `header`, `footer`
|
||||
- Events: `toggle`, `open`, `close`, `select`
|
||||
- Props: `items: string = []`, `open: boolean = false`, `defaultOpen: boolean = false`, `label: string = "Open menu"`, `icon: string = ""`, `showChevron: boolean = true`, `menuLabel: string = "Dropdown menu"`, `placement: string = "bottom-start"`, `width: string = "md"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `closeOnSelect: boolean = true`, `closeOnOutside: boolean = true`, `disabled: boolean = false`, `emptyLabel: string = "No menu items"`, `class: string = ""`
|
||||
- Slots: `trigger`, `header`, `default`, `footer`
|
||||
- Events: `toggle`, `open`, `close`, `select`, `action`
|
||||
|
||||
### Modal
|
||||
|
||||
Theme-aware, responsive modal component.
|
||||
Present an accessible modal dialog with focus management, confirmation, cancellation, slots, and responsive sizing.
|
||||
|
||||
- Mount: `data-component="Modal"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Modal"`, `description: string = ""`, `open: boolean = false`, `placement: string = "bottom"`, `closeLabel: string = "Close"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `title: string = "Modal"`, `description: string = ""`, `icon: string = ""`, `label: string = "Modal dialog"`, `size: string = "md"`, `placement: string = "center"`, `color: string = "primary"`, `variant: string = "default"`, `showClose: boolean = true`, `closeLabel: string = "Close modal"`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `closeOnCancel: boolean = true`, `closeOnConfirm: boolean = false`, `showFooter: boolean = true`, `cancelLabel: string = "Cancel"`, `cancelIcon: string = ""`, `confirmLabel: string = "Confirm"`, `confirmIcon: string = ""`, `confirmDisabled: boolean = false`, `confirmLoading: boolean = false`, `destructive: boolean = false`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `scrollable: boolean = true`, `class: string = ""`
|
||||
- Slots: `trigger`, `header`, `default`, `footer`
|
||||
- Events: `open`, `close`, `cancel`, `confirm`
|
||||
|
||||
### Popover
|
||||
|
||||
Theme-aware, responsive popover component.
|
||||
Display anchored supporting content with configurable trigger, placement, responsive sizing, and open or close events.
|
||||
|
||||
- Mount: `data-component="Popover"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Popover"`, `description: string = ""`, `open: boolean = false`, `placement: string = "bottom"`, `closeLabel: string = "Close"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: `open`, `close`
|
||||
- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `triggerLabel: string = "Open popover"`, `triggerIcon: string = ""`, `title: string = ""`, `description: string = ""`, `icon: string = ""`, `placement: string = "bottom-start"`, `width: string = "md"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `showArrow: boolean = true`, `showClose: boolean = false`, `closeLabel: string = "Close popover"`, `closeOnOutside: boolean = true`, `closeOnEscape: boolean = true`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `closeOnAction: boolean = true`, `disabled: boolean = false`, `class: string = ""`
|
||||
- Slots: `trigger`, `header`, `default`, `footer`
|
||||
- Events: `toggle`, `open`, `close`, `action`
|
||||
|
||||
### Tooltip
|
||||
|
||||
Theme-aware, responsive tooltip component.
|
||||
Show concise accessible contextual help on hover, focus, click, or controlled open state.
|
||||
|
||||
- Mount: `data-component="Tooltip"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Tooltip"`, `description: string = ""`, `open: boolean = false`, `placement: string = "bottom"`, `closeLabel: string = "Close"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Events: `open`, `close`
|
||||
- Props: `id: string = ""`, `open: boolean = false`, `defaultOpen: boolean = false`, `title: string = ""`, `description: string = ""`, `content: string = "Tooltip"`, `trigger: string = "hover"`, `placement: string = "top"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "dark"`, `maxWidth: string = "18rem"`, `offset: number = 10`, `showArrow: boolean = true`, `interactive: boolean = false`, `disabled: boolean = false`, `class: string = ""`
|
||||
- Slots: `trigger`, `default`
|
||||
- Events: `toggle`, `open`, `close`
|
||||
|
||||
## Tables
|
||||
|
||||
|
||||
+418
-407
@@ -1,47 +1,91 @@
|
||||
{
|
||||
"sourceDocuments": ["Screenshot-directed WRNexus UI catalog reset"],
|
||||
"sourceDocuments": [
|
||||
"Screenshot-directed WRNexus UI catalog reset"
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"name": "AdvancedSelect",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive advanced select component.",
|
||||
"events": [
|
||||
"search",
|
||||
"select",
|
||||
"change",
|
||||
"clear",
|
||||
"open",
|
||||
"close",
|
||||
"load",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ComboBox",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Editable autocomplete combobox with local and remote suggestions.",
|
||||
"events": [
|
||||
"search",
|
||||
"select",
|
||||
"change",
|
||||
"clear",
|
||||
"open",
|
||||
"close",
|
||||
"load",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CopyMarkup",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive copy markup component."
|
||||
},
|
||||
{
|
||||
"name": "InputNumber",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive input number component."
|
||||
},
|
||||
{
|
||||
"name": "PinInput",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Secure multi-cell PIN and verification-code input with regex and paste support.",
|
||||
"events": [
|
||||
"input",
|
||||
"change",
|
||||
"complete",
|
||||
"paste",
|
||||
"clear",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "StrongPassword",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive strong password component."
|
||||
},
|
||||
{
|
||||
"name": "ToggleCount",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive toggle count component."
|
||||
},
|
||||
{
|
||||
"name": "TogglePassword",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Accessible password field with optional show and hide controls.",
|
||||
"events": [
|
||||
"input",
|
||||
"change",
|
||||
"toggle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Accordion",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive accordion component."
|
||||
},
|
||||
{
|
||||
"name": "AdvancedDatePicker",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive advanced date picker component."
|
||||
},
|
||||
{
|
||||
"name": "AdvancedRangeSlider",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive advanced range slider component."
|
||||
},
|
||||
{
|
||||
"name": "AdvancedSelect",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive advanced select component.",
|
||||
"events": ["search", "select", "change", "clear", "open", "close", "load", "error"]
|
||||
},
|
||||
{
|
||||
"name": "Alert",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive alert component."
|
||||
},
|
||||
{
|
||||
"name": "AnnouncementBar",
|
||||
"category": "blocks",
|
||||
"purpose": "Accessible public announcement, emergency, maintenance, or status bar."
|
||||
},
|
||||
{
|
||||
"name": "AuthForm",
|
||||
"category": "forms",
|
||||
"purpose": "Reusable authentication form for sign-in, registration, recovery, reset, and MFA."
|
||||
},
|
||||
{
|
||||
"name": "AuthSplitLayout",
|
||||
"category": "layout",
|
||||
"purpose": "Responsive 50/50 authentication layout with content and form regions."
|
||||
},
|
||||
{
|
||||
"name": "Avatar",
|
||||
"category": "base",
|
||||
@@ -52,11 +96,6 @@
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive avatar group component."
|
||||
},
|
||||
{
|
||||
"name": "BackToTop",
|
||||
"category": "navigation",
|
||||
"purpose": "Accessible scroll-to-top control with visibility threshold."
|
||||
},
|
||||
{
|
||||
"name": "Badge",
|
||||
"category": "base",
|
||||
@@ -67,11 +106,6 @@
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive blockquote component."
|
||||
},
|
||||
{
|
||||
"name": "Breadcrumb",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive breadcrumb component."
|
||||
},
|
||||
{
|
||||
"name": "Button",
|
||||
"category": "base",
|
||||
@@ -85,94 +119,23 @@
|
||||
{
|
||||
"name": "Card",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive card component."
|
||||
"purpose": "Group related content in a responsive themed surface with title, description, content, and supporting slots."
|
||||
},
|
||||
{
|
||||
"name": "Carousel",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive carousel component."
|
||||
},
|
||||
{
|
||||
"name": "Chart",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive chart component."
|
||||
},
|
||||
{
|
||||
"name": "ChatBubble",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive chat bubble component."
|
||||
},
|
||||
{
|
||||
"name": "Checkbox",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive checkbox component."
|
||||
},
|
||||
{
|
||||
"name": "Clipboard",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive clipboard component."
|
||||
},
|
||||
{
|
||||
"name": "Collapse",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive collapse component."
|
||||
},
|
||||
{
|
||||
"name": "ColorPicker",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive color picker component."
|
||||
},
|
||||
{
|
||||
"name": "Columns",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive columns component."
|
||||
},
|
||||
{
|
||||
"name": "ComboBox",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Editable autocomplete combobox with local and remote suggestions.",
|
||||
"events": ["search", "select", "change", "clear", "open", "close", "load", "error"]
|
||||
},
|
||||
{
|
||||
"name": "Confetti",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive confetti component."
|
||||
},
|
||||
{
|
||||
"name": "Container",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive container component."
|
||||
},
|
||||
{
|
||||
"name": "ContextMenu",
|
||||
"category": "overlays",
|
||||
"purpose": "Theme-aware, responsive context menu component."
|
||||
},
|
||||
{
|
||||
"name": "CopyMarkup",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive copy markup component."
|
||||
},
|
||||
{
|
||||
"name": "CTASection",
|
||||
"category": "blocks",
|
||||
"purpose": "Reusable closing call-to-action section with primary and secondary actions."
|
||||
},
|
||||
{
|
||||
"name": "CustomScrollbar",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive custom scrollbar component."
|
||||
},
|
||||
{
|
||||
"name": "DataMap",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive data map component."
|
||||
},
|
||||
{
|
||||
"name": "DataTable",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive data table component."
|
||||
},
|
||||
{
|
||||
"name": "DatePicker",
|
||||
"category": "base",
|
||||
@@ -183,257 +146,41 @@
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive device frame component."
|
||||
},
|
||||
{
|
||||
"name": "Divider",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive divider component."
|
||||
},
|
||||
{
|
||||
"name": "DragAndDrop",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive drag and drop component."
|
||||
},
|
||||
{
|
||||
"name": "Drawer",
|
||||
"category": "overlays",
|
||||
"purpose": "Theme-aware, responsive drawer component."
|
||||
},
|
||||
{
|
||||
"name": "Dropdown",
|
||||
"category": "overlays",
|
||||
"purpose": "Theme-aware, responsive dropdown component."
|
||||
},
|
||||
{
|
||||
"name": "FeatureCard",
|
||||
"category": "blocks",
|
||||
"purpose": "Reusable linked feature or service card with icon, badge, description, and action."
|
||||
},
|
||||
{
|
||||
"name": "FeatureGrid",
|
||||
"category": "layout",
|
||||
"purpose": "Responsive equal-height grid for feature and service cards."
|
||||
},
|
||||
{
|
||||
"name": "FeatureIconCard",
|
||||
"category": "blocks",
|
||||
"purpose": "Feature card with a prominent themed icon treatment."
|
||||
},
|
||||
{
|
||||
"name": "FileInput",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive file input component."
|
||||
},
|
||||
{
|
||||
"name": "FileUpload",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive file upload component."
|
||||
},
|
||||
{
|
||||
"name": "FileUploadProgress",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive file upload progress component."
|
||||
},
|
||||
{
|
||||
"name": "Footer",
|
||||
"category": "navigation",
|
||||
"purpose": "Responsive application footer with structured links and pre/post content slots."
|
||||
},
|
||||
{
|
||||
"name": "Grid",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive grid component."
|
||||
},
|
||||
{
|
||||
"name": "Hero",
|
||||
"category": "blocks",
|
||||
"purpose": "Reusable public-page hero with actions, trust items, badges, and visual slots."
|
||||
},
|
||||
{
|
||||
"name": "HeroActions",
|
||||
"category": "base",
|
||||
"purpose": "Responsive action group for hero and call-to-action sections."
|
||||
},
|
||||
{
|
||||
"name": "Image",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive image component."
|
||||
},
|
||||
{
|
||||
"name": "Input",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive input component."
|
||||
},
|
||||
{
|
||||
"name": "InputGroup",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive input group component."
|
||||
},
|
||||
{
|
||||
"name": "InputNumber",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive input number component."
|
||||
},
|
||||
{
|
||||
"name": "Kbd",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive kbd component."
|
||||
},
|
||||
{
|
||||
"name": "LayoutSplitter",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive layout splitter component."
|
||||
},
|
||||
{
|
||||
"name": "LegendIndicator",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive legend indicator component."
|
||||
},
|
||||
{
|
||||
"name": "Link",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive link component."
|
||||
},
|
||||
{
|
||||
"name": "List",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive list component."
|
||||
"purpose": "Present structured responsive linked or status items with icons, descriptions, actions, and selection events."
|
||||
},
|
||||
{
|
||||
"name": "ListGroup",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive list group component."
|
||||
},
|
||||
{
|
||||
"name": "Map",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive map component."
|
||||
},
|
||||
{
|
||||
"name": "MarketingSectionHeader",
|
||||
"category": "blocks",
|
||||
"purpose": "Marketing section heading with an optional linked action."
|
||||
},
|
||||
{
|
||||
"name": "Marquee",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive marquee component."
|
||||
},
|
||||
{
|
||||
"name": "MegaMenu",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive mega menu component."
|
||||
},
|
||||
{
|
||||
"name": "MetricCard",
|
||||
"category": "blocks",
|
||||
"purpose": "Single statistic or operational metric with icon, trend, and optional action."
|
||||
},
|
||||
{
|
||||
"name": "MetricGrid",
|
||||
"category": "layout",
|
||||
"purpose": "Responsive collection of metric cards."
|
||||
},
|
||||
{
|
||||
"name": "Modal",
|
||||
"category": "overlays",
|
||||
"purpose": "Theme-aware, responsive modal component."
|
||||
},
|
||||
{
|
||||
"name": "Nav",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive nav component."
|
||||
},
|
||||
{
|
||||
"name": "Navbar",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive navbar component."
|
||||
},
|
||||
{
|
||||
"name": "PageHeader",
|
||||
"category": "blocks",
|
||||
"purpose": "Public and application page header with breadcrumbs, metadata, and actions."
|
||||
},
|
||||
{
|
||||
"name": "Pagination",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive pagination component."
|
||||
},
|
||||
{
|
||||
"name": "PinInput",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Secure multi-cell PIN and verification-code input with regex and paste support.",
|
||||
"events": ["input", "change", "complete", "paste", "clear", "error"]
|
||||
},
|
||||
{
|
||||
"name": "Popover",
|
||||
"category": "overlays",
|
||||
"purpose": "Theme-aware, responsive popover component."
|
||||
},
|
||||
{
|
||||
"name": "PortalDashboard",
|
||||
"category": "blocks",
|
||||
"purpose": "Responsive portal dashboard with metrics, actions, tasks, and updates."
|
||||
},
|
||||
{
|
||||
"name": "PreferenceSwitcher",
|
||||
"category": "navigation",
|
||||
"purpose": "Accessible theme, accent color, and language preference controls."
|
||||
"purpose": "Continuously present responsive labels, partners, notices, or capabilities with pause and resume behavior."
|
||||
},
|
||||
{
|
||||
"name": "Progress",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive progress component."
|
||||
},
|
||||
{
|
||||
"name": "PublicPageShell",
|
||||
"category": "layout",
|
||||
"purpose": "Consistent structural wrapper for public-facing pages."
|
||||
},
|
||||
{
|
||||
"name": "Radio",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive radio component."
|
||||
},
|
||||
{
|
||||
"name": "RangeSlider",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive range slider component."
|
||||
},
|
||||
{
|
||||
"name": "Rating",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive rating component."
|
||||
},
|
||||
{
|
||||
"name": "Scrollspy",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive scrollspy component."
|
||||
},
|
||||
{
|
||||
"name": "SearchBox",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive search box component."
|
||||
},
|
||||
{
|
||||
"name": "Section",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware page section with consistent spacing, width, background, and border controls."
|
||||
},
|
||||
{
|
||||
"name": "SectionHeader",
|
||||
"category": "layout",
|
||||
"purpose": "Reusable accessible section heading with eyebrow, title, description, icon, and actions."
|
||||
},
|
||||
{
|
||||
"name": "Select",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive select component."
|
||||
},
|
||||
{
|
||||
"name": "Sidebar",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive sidebar component."
|
||||
},
|
||||
{
|
||||
"name": "Skeleton",
|
||||
"category": "base",
|
||||
@@ -444,106 +191,370 @@
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive spinner component."
|
||||
},
|
||||
{
|
||||
"name": "SplitHero",
|
||||
"category": "blocks",
|
||||
"purpose": "Two-column hero with configurable content and visual placement."
|
||||
},
|
||||
{
|
||||
"name": "StatsBar",
|
||||
"category": "blocks",
|
||||
"purpose": "Compact responsive statistics strip."
|
||||
},
|
||||
{
|
||||
"name": "Stepper",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive stepper component."
|
||||
},
|
||||
{
|
||||
"name": "StrongPassword",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive strong password component."
|
||||
},
|
||||
{
|
||||
"name": "StyledIcon",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive styled icon component."
|
||||
},
|
||||
{
|
||||
"name": "Switch",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive switch component."
|
||||
},
|
||||
{
|
||||
"name": "Table",
|
||||
"category": "tables",
|
||||
"purpose": "Theme-aware, responsive table component."
|
||||
},
|
||||
{
|
||||
"name": "Tabs",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive tabs component."
|
||||
},
|
||||
{
|
||||
"name": "Textarea",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive textarea component."
|
||||
},
|
||||
{
|
||||
"name": "TextLink",
|
||||
"category": "layout",
|
||||
"purpose": "Styled inline action link with icons, arrows, external state, and disabled handling."
|
||||
},
|
||||
{
|
||||
"name": "Timeline",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive timeline component."
|
||||
},
|
||||
{
|
||||
"name": "TimePicker",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive time picker component."
|
||||
"purpose": "Present responsive chronological activity, milestones, or workflow status with rich item metadata."
|
||||
},
|
||||
{
|
||||
"name": "Toast",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive toast component."
|
||||
},
|
||||
{
|
||||
"name": "ToastNotifications",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive toast notifications component."
|
||||
},
|
||||
{
|
||||
"name": "ToggleCount",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive toggle count component."
|
||||
},
|
||||
{
|
||||
"name": "TogglePassword",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Accessible password field with optional show and hide controls.",
|
||||
"events": ["input", "change", "toggle"]
|
||||
},
|
||||
{
|
||||
"name": "Tooltip",
|
||||
"category": "overlays",
|
||||
"purpose": "Theme-aware, responsive tooltip component."
|
||||
},
|
||||
{
|
||||
"name": "TreeView",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive tree view component."
|
||||
},
|
||||
{
|
||||
"name": "Typography",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive typography component."
|
||||
"name": "MetricCard",
|
||||
"category": "data",
|
||||
"purpose": "Display one operational metric with value, suffix, description, icon, trend, progress, and optional action."
|
||||
},
|
||||
{
|
||||
"name": "MetricGrid",
|
||||
"category": "data",
|
||||
"purpose": "Arrange operational metrics, KPIs, public statistics, or service indicators in a responsive equal-height grid."
|
||||
},
|
||||
{
|
||||
"name": "StatsBar",
|
||||
"category": "data",
|
||||
"purpose": "Present a compact responsive strip of key facts, counts, performance indicators, or trust signals."
|
||||
},
|
||||
{
|
||||
"name": "Checkbox",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive checkbox component."
|
||||
},
|
||||
{
|
||||
"name": "ColorPicker",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive color picker component."
|
||||
},
|
||||
{
|
||||
"name": "FileInput",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive file input component."
|
||||
},
|
||||
{
|
||||
"name": "Input",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive input component."
|
||||
},
|
||||
{
|
||||
"name": "InputGroup",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive input group component."
|
||||
},
|
||||
{
|
||||
"name": "Radio",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive radio component."
|
||||
},
|
||||
{
|
||||
"name": "RangeSlider",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive range slider component."
|
||||
},
|
||||
{
|
||||
"name": "SearchBox",
|
||||
"category": "forms",
|
||||
"purpose": "Provide an accessible responsive search field with labels, validation states, sizes, and input or change events."
|
||||
},
|
||||
{
|
||||
"name": "Select",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive select component."
|
||||
},
|
||||
{
|
||||
"name": "Switch",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive switch component."
|
||||
},
|
||||
{
|
||||
"name": "Textarea",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive textarea component."
|
||||
},
|
||||
{
|
||||
"name": "TimePicker",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive time picker component."
|
||||
},
|
||||
{
|
||||
"name": "AdvancedDatePicker",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive advanced date picker component."
|
||||
},
|
||||
{
|
||||
"name": "AdvancedRangeSlider",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive advanced range slider component."
|
||||
},
|
||||
{
|
||||
"name": "Chart",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive chart component."
|
||||
},
|
||||
{
|
||||
"name": "Clipboard",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive clipboard component."
|
||||
},
|
||||
{
|
||||
"name": "Confetti",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive confetti component."
|
||||
},
|
||||
{
|
||||
"name": "DataMap",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive data map component."
|
||||
},
|
||||
{
|
||||
"name": "DataTable",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive data table component."
|
||||
},
|
||||
{
|
||||
"name": "DragAndDrop",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive drag and drop component."
|
||||
},
|
||||
{
|
||||
"name": "FileUpload",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive file upload component."
|
||||
},
|
||||
{
|
||||
"name": "Map",
|
||||
"category": "integrations",
|
||||
"purpose": "Present responsive location information and markers with map-ready metadata and movement or marker events."
|
||||
},
|
||||
{
|
||||
"name": "ToastNotifications",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive toast notifications component."
|
||||
},
|
||||
{
|
||||
"name": "WysiwygEditor",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive wysiwyg editor component."
|
||||
},
|
||||
{
|
||||
"name": "Columns",
|
||||
"category": "layout",
|
||||
"purpose": "Create responsive balanced content columns with configurable count, gap, density, and maximum width."
|
||||
},
|
||||
{
|
||||
"name": "Container",
|
||||
"category": "layout",
|
||||
"purpose": "Constrain and align page content with responsive gutters and compact, wide, or full width options."
|
||||
},
|
||||
{
|
||||
"name": "CustomScrollbar",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive custom scrollbar component."
|
||||
},
|
||||
{
|
||||
"name": "Divider",
|
||||
"category": "layout",
|
||||
"purpose": "Separate related horizontal or vertical content with optional labels, sizes, and semantic colors."
|
||||
},
|
||||
{
|
||||
"name": "FeatureGrid",
|
||||
"category": "layout",
|
||||
"purpose": "Arrange feature cards, services, solutions, or benefits in a responsive equal-height grid."
|
||||
},
|
||||
{
|
||||
"name": "Footer",
|
||||
"category": "layout",
|
||||
"purpose": "Render structured responsive footer navigation, pre and post content, copyright content, links, and public events."
|
||||
},
|
||||
{
|
||||
"name": "Grid",
|
||||
"category": "layout",
|
||||
"purpose": "Arrange arbitrary content in a responsive configurable CSS grid with stable columns, gaps, alignment, and width."
|
||||
},
|
||||
{
|
||||
"name": "Image",
|
||||
"category": "layout",
|
||||
"purpose": "Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment."
|
||||
},
|
||||
{
|
||||
"name": "Kbd",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive kbd component."
|
||||
},
|
||||
{
|
||||
"name": "LayoutSplitter",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive layout splitter component."
|
||||
},
|
||||
{
|
||||
"name": "Link",
|
||||
"category": "layout",
|
||||
"purpose": "Render an accessible internal or external link with target, relation, size, color, and public focus or click events."
|
||||
},
|
||||
{
|
||||
"name": "PublicPageShell",
|
||||
"category": "layout",
|
||||
"purpose": "Provide the outer responsive structure, width, background, slots, overflow, and minimum-height behavior for public pages."
|
||||
},
|
||||
{
|
||||
"name": "Section",
|
||||
"category": "layout",
|
||||
"purpose": "Create a responsive themed page section with controlled spacing, width, borders, and surface treatment."
|
||||
},
|
||||
{
|
||||
"name": "SectionHeader",
|
||||
"category": "layout",
|
||||
"purpose": "Introduce a section with an eyebrow, title, description, alignment, and responsive heading hierarchy."
|
||||
},
|
||||
{
|
||||
"name": "Typography",
|
||||
"category": "layout",
|
||||
"purpose": "Apply consistent readable responsive typography, widths, columns, spacing, and editorial hierarchy."
|
||||
},
|
||||
{
|
||||
"name": "AnnouncementBar",
|
||||
"category": "marketing",
|
||||
"purpose": "Publish a responsive notice with badge, icon, supporting copy, action, dismiss behavior, and width controls."
|
||||
},
|
||||
{
|
||||
"name": "CTASection",
|
||||
"category": "marketing",
|
||||
"purpose": "Close a page or major section with conversion-focused copy, actions, and optional supporting visual content."
|
||||
},
|
||||
{
|
||||
"name": "FeatureCard",
|
||||
"category": "marketing",
|
||||
"purpose": "Present one linked feature or service with media, icon, badge, description, and action."
|
||||
},
|
||||
{
|
||||
"name": "FeatureIconCard",
|
||||
"category": "marketing",
|
||||
"purpose": "Present a compact feature or benefit with a styled icon, title, description, badge, and optional link."
|
||||
},
|
||||
{
|
||||
"name": "Hero",
|
||||
"category": "marketing",
|
||||
"purpose": "Build a full-width responsive hero with constrained content, actions, trust signals, and a structured or custom visual panel."
|
||||
},
|
||||
{
|
||||
"name": "HeroActions",
|
||||
"category": "marketing",
|
||||
"purpose": "Group hero and campaign actions with consistent alignment, orientation, sizing, and responsive mobile stacking."
|
||||
},
|
||||
{
|
||||
"name": "MarketingSectionHeader",
|
||||
"category": "marketing",
|
||||
"purpose": "Introduce marketing content with an eyebrow, title, description, and optional linked action."
|
||||
},
|
||||
{
|
||||
"name": "PageHeader",
|
||||
"category": "marketing",
|
||||
"purpose": "Introduce an internal or public page with breadcrumbs, icon, title, description, and primary or secondary actions."
|
||||
},
|
||||
{
|
||||
"name": "SplitHero",
|
||||
"category": "marketing",
|
||||
"purpose": "Build a responsive two-column introduction balancing descriptive content with a visual, image, or structured data panel."
|
||||
},
|
||||
{
|
||||
"name": "TextLink",
|
||||
"category": "marketing",
|
||||
"purpose": "Render an accessible text action with optional icon, arrow, underline, external state, and semantic styling."
|
||||
},
|
||||
{
|
||||
"name": "BackToTop",
|
||||
"category": "navigation",
|
||||
"purpose": "Provide a responsive floating control that returns long pages to the top and can show scroll progress."
|
||||
},
|
||||
{
|
||||
"name": "Breadcrumb",
|
||||
"category": "navigation",
|
||||
"purpose": "Show responsive hierarchical navigation with home support, separators, current-page state, sizes, and selection events."
|
||||
},
|
||||
{
|
||||
"name": "MegaMenu",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive mega menu component."
|
||||
},
|
||||
{
|
||||
"name": "Nav",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive nav component."
|
||||
},
|
||||
{
|
||||
"name": "Navbar",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive navbar component."
|
||||
},
|
||||
{
|
||||
"name": "Pagination",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive pagination component."
|
||||
},
|
||||
{
|
||||
"name": "Scrollspy",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive scrollspy component."
|
||||
},
|
||||
{
|
||||
"name": "Sidebar",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive sidebar component."
|
||||
},
|
||||
{
|
||||
"name": "Stepper",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive stepper component."
|
||||
},
|
||||
{
|
||||
"name": "Tabs",
|
||||
"category": "navigation",
|
||||
"purpose": "Switch between related responsive content panels with horizontal or vertical orientation and selection events."
|
||||
},
|
||||
{
|
||||
"name": "ContextMenu",
|
||||
"category": "overlays",
|
||||
"purpose": "Open an accessible keyboard-aware action menu from pointer or keyboard context interactions."
|
||||
},
|
||||
{
|
||||
"name": "Drawer",
|
||||
"category": "overlays",
|
||||
"purpose": "Present responsive modal side or bottom content with focus management, backdrop behavior, slots, and close events."
|
||||
},
|
||||
{
|
||||
"name": "Dropdown",
|
||||
"category": "overlays",
|
||||
"purpose": "Open an accessible anchored menu with keyboard navigation, item selection, actions, and responsive placement."
|
||||
},
|
||||
{
|
||||
"name": "Modal",
|
||||
"category": "overlays",
|
||||
"purpose": "Present an accessible modal dialog with focus management, confirmation, cancellation, slots, and responsive sizing."
|
||||
},
|
||||
{
|
||||
"name": "Popover",
|
||||
"category": "overlays",
|
||||
"purpose": "Display anchored supporting content with configurable trigger, placement, responsive sizing, and open or close events."
|
||||
},
|
||||
{
|
||||
"name": "Tooltip",
|
||||
"category": "overlays",
|
||||
"purpose": "Show concise accessible contextual help on hover, focus, click, or controlled open state."
|
||||
},
|
||||
{
|
||||
"name": "Table",
|
||||
"category": "tables",
|
||||
"purpose": "Theme-aware, responsive table component."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+1123
-169
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
component AnnouncementBar {
|
||||
props {
|
||||
@event dismiss = function
|
||||
|
||||
badge = ""
|
||||
badgeIcon = ""
|
||||
message = "Announcement"
|
||||
@@ -13,6 +15,7 @@ component AnnouncementBar {
|
||||
sticky = false
|
||||
compact = false
|
||||
size = "default"
|
||||
width = "default"
|
||||
color = "primary"
|
||||
variant = "soft"
|
||||
role = "status"
|
||||
@@ -24,108 +27,590 @@ component AnnouncementBar {
|
||||
|
||||
view {
|
||||
<aside
|
||||
{...attrs}
|
||||
data-ui-component="AnnouncementBar"
|
||||
data-size='{size}'
|
||||
data-width='{width}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
data-compact='{compact}'
|
||||
data-sticky='{sticky}'
|
||||
data-show='{!dismissed}'
|
||||
aria-hidden='{dismissed ? "true" : "false"}'
|
||||
role='{role}'
|
||||
aria-live='{live}'
|
||||
data-show='!dismissed'
|
||||
class='z-40 w-full border-y border-[var(--wire-color-border)] {class}'
|
||||
class:sticky='sticky'
|
||||
class:top-0='sticky'
|
||||
class:bg-[var(--wire-color-primary-soft)]='variant === "soft" && color === "primary"'
|
||||
class:border-[var(--wire-color-primary-muted)]='variant === "soft" && color === "primary"'
|
||||
class:bg-[var(--wire-color-info-soft)]='variant === "soft" && color === "info"'
|
||||
class:border-[var(--wire-color-info-muted)]='variant === "soft" && color === "info"'
|
||||
class:bg-[var(--wire-color-success-soft)]='variant === "soft" && color === "success"'
|
||||
class:border-[var(--wire-color-success-muted)]='variant === "soft" && color === "success"'
|
||||
class:bg-[var(--wire-color-warning-soft)]='variant === "soft" && color === "warning"'
|
||||
class:border-[var(--wire-color-warning-muted)]='variant === "soft" && color === "warning"'
|
||||
class:bg-[var(--wire-color-danger-soft)]='variant === "soft" && color === "danger"'
|
||||
class:border-[var(--wire-color-danger-muted)]='variant === "soft" && color === "danger"'
|
||||
class:bg-[var(--wire-color-primary)]='variant === "solid" && color === "primary"'
|
||||
class:text-[var(--wire-color-on-primary)]='variant === "solid" && color === "primary"'
|
||||
class:bg-[var(--wire-color-danger)]='variant === "solid" && color === "danger"'
|
||||
class:text-[var(--wire-color-on-danger)]='variant === "solid" && color === "danger"'
|
||||
class:bg-[var(--wire-color-surface-raised)]='variant === "default"'
|
||||
class='wire-announcement {class}'
|
||||
>
|
||||
<Container columns="1" maxWidth="xl" size="default">
|
||||
<div
|
||||
class="flex flex-col gap-4 py-4 sm:flex-row sm:items-center sm:justify-between"
|
||||
class:py-2.5='compact'
|
||||
>
|
||||
<div class="flex min-w-0 items-start gap-3 sm:items-center">
|
||||
<div class="wire-announcement__surface">
|
||||
<span
|
||||
class="wire-announcement__accent"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
|
||||
<span
|
||||
class="wire-announcement__glow wire-announcement__glow--one"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
|
||||
<span
|
||||
class="wire-announcement__glow wire-announcement__glow--two"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
|
||||
<div class="wire-announcement__inner">
|
||||
<div class="wire-announcement__content">
|
||||
{#if icon}
|
||||
<span
|
||||
class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-primary)] shadow-sm"
|
||||
class:size-8='compact'
|
||||
class:text-[var(--wire-color-danger)]='color === "danger"'
|
||||
class:text-[var(--wire-color-warning-text)]='color === "warning"'
|
||||
class:text-[var(--wire-color-success)]='color === "success"'
|
||||
class:text-[var(--wire-color-info)]='color === "info"'
|
||||
>
|
||||
<span class='{icon + " size-5"}' aria-hidden="true"></span>
|
||||
<span class="wire-announcement__icon">
|
||||
<span
|
||||
class='{icon}'
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<div class="min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="wire-announcement__copy">
|
||||
<div class="wire-announcement__headline">
|
||||
{#if badge}
|
||||
<Badge
|
||||
label='{badge}'
|
||||
icon='{badgeIcon}'
|
||||
size="sm"
|
||||
color='{color}'
|
||||
variant='{variant === "solid" ? "outline" : "solid"}'
|
||||
/>
|
||||
<span class="wire-announcement__badge">
|
||||
{#if badgeIcon}
|
||||
<span
|
||||
class='{badgeIcon}'
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{/if}
|
||||
|
||||
<span>{badge}</span>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<p
|
||||
class="font-semibold"
|
||||
class:text-sm='compact || size === "sm"'
|
||||
class:text-base='!compact && size !== "sm"'
|
||||
class:text-[var(--wire-color-text)]='variant !== "solid"'
|
||||
>
|
||||
<p class="wire-announcement__message">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if description && !compact}
|
||||
<p
|
||||
class="mt-1 text-sm leading-6"
|
||||
class:text-[var(--wire-color-text-muted)]='variant !== "solid"'
|
||||
class:opacity-85='variant === "solid"'
|
||||
>
|
||||
<p class="wire-announcement__description">
|
||||
{description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2 sm:justify-end">
|
||||
{#if actionLabel}
|
||||
<TextLink
|
||||
label='{actionLabel}'
|
||||
href='{actionHref}'
|
||||
icon='{actionIcon}'
|
||||
iconPosition="start"
|
||||
showArrow="true"
|
||||
color='{variant === "solid" ? "neutral" : color}'
|
||||
variant='{variant === "solid" ? "button" : "default"}'
|
||||
/>
|
||||
{/if}
|
||||
{#if actionLabel || dismissible}
|
||||
<div class="wire-announcement__actions">
|
||||
{#if actionLabel}
|
||||
<a
|
||||
href='{actionHref || "#"}'
|
||||
class="wire-announcement__action"
|
||||
>
|
||||
<span>{actionLabel}</span>
|
||||
|
||||
{#if dismissible}
|
||||
<button
|
||||
type="button"
|
||||
aria-label='{dismissLabel}'
|
||||
class="inline-flex size-9 items-center justify-center rounded-lg text-[var(--wire-color-text-muted)] transition hover:bg-[var(--wire-color-surface-raised)] hover:text-[var(--wire-color-text)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
|
||||
@click='dismissed = true; event.currentTarget.dispatchEvent(new CustomEvent("dismiss", { bubbles: true }))'
|
||||
>
|
||||
<span class="icon-[lucide--x] size-4" aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if actionIcon}
|
||||
<span
|
||||
class='{actionIcon}'
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{:else}
|
||||
<span
|
||||
class="icon-[lucide--arrow-right]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{/if}
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if dismissible}
|
||||
<button
|
||||
type="button"
|
||||
aria-label='{dismissLabel}'
|
||||
title='{dismissLabel}'
|
||||
class="wire-announcement__dismiss"
|
||||
@click='dismissed = true; event.currentTarget.dispatchEvent(new CustomEvent("dismiss", { bubbles: true, detail: { message: message } }))'
|
||||
>
|
||||
<span
|
||||
class="icon-[lucide--x]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-announcement {
|
||||
--announcement-accent: var(--wire-color-primary);
|
||||
--announcement-contrast: var(--wire-color-primary-contrast, #ffffff);
|
||||
--announcement-soft: var(--wire-color-primary-soft);
|
||||
--announcement-muted: var(--wire-color-primary-muted);
|
||||
|
||||
position: relative;
|
||||
z-index: 40;
|
||||
display: block;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
margin-inline: calc(50% - 50vw);
|
||||
color: var(--wire-color-text);
|
||||
}
|
||||
|
||||
.wire-announcement__surface {
|
||||
width: min(calc(100% - 2rem), 72rem);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.wire-announcement[data-width="wide"] .wire-announcement__surface {
|
||||
width: min(calc(100% - 2rem), 90rem);
|
||||
}
|
||||
|
||||
.wire-announcement[data-width="full"] .wire-announcement__surface {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin-inline: 0;
|
||||
border-inline: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.wire-announcement[data-sticky="true"] {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.wire-announcement[data-color="secondary"] {
|
||||
--announcement-accent: var(--wire-color-secondary);
|
||||
--announcement-contrast: var(--wire-color-secondary-contrast, #ffffff);
|
||||
--announcement-soft: var(--wire-color-secondary-soft);
|
||||
--announcement-muted: var(--wire-color-secondary-muted);
|
||||
}
|
||||
|
||||
.wire-announcement[data-color="info"] {
|
||||
--announcement-accent: var(--wire-color-info);
|
||||
--announcement-contrast: var(--wire-color-info-contrast, #ffffff);
|
||||
--announcement-soft: var(--wire-color-info-soft);
|
||||
--announcement-muted: var(--wire-color-info-muted);
|
||||
}
|
||||
|
||||
.wire-announcement[data-color="success"] {
|
||||
--announcement-accent: var(--wire-color-success);
|
||||
--announcement-contrast: var(--wire-color-success-contrast, #ffffff);
|
||||
--announcement-soft: var(--wire-color-success-soft);
|
||||
--announcement-muted: var(--wire-color-success-muted);
|
||||
}
|
||||
|
||||
.wire-announcement[data-color="warning"] {
|
||||
--announcement-accent: var(--wire-color-warning);
|
||||
--announcement-contrast: var(--wire-color-warning-contrast, #111827);
|
||||
--announcement-soft: var(--wire-color-warning-soft);
|
||||
--announcement-muted: var(--wire-color-warning-muted);
|
||||
}
|
||||
|
||||
.wire-announcement[data-color="danger"] {
|
||||
--announcement-accent: var(--wire-color-danger);
|
||||
--announcement-contrast: var(--wire-color-danger-contrast, #ffffff);
|
||||
--announcement-soft: var(--wire-color-danger-soft);
|
||||
--announcement-muted: var(--wire-color-danger-muted);
|
||||
}
|
||||
|
||||
.wire-announcement__surface {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
background: var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 1.25rem;
|
||||
box-shadow:
|
||||
0 1px 2px rgb(0 0 0 / 0.04),
|
||||
0 12px 32px rgb(0 0 0 / 0.08);
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="soft"] .wire-announcement__surface {
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--announcement-soft) 86%, var(--wire-color-surface-raised)),
|
||||
var(--wire-color-surface-raised)
|
||||
);
|
||||
border-color: color-mix(in srgb, var(--announcement-accent) 28%, var(--wire-color-border));
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="outline"] .wire-announcement__surface {
|
||||
background: transparent;
|
||||
border-color: color-mix(in srgb, var(--announcement-accent) 46%, var(--wire-color-border));
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="minimal"] .wire-announcement__surface {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="solid"] {
|
||||
color: var(--announcement-contrast);
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="solid"] .wire-announcement__surface {
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 8% 15%,
|
||||
color-mix(in srgb, var(--announcement-contrast) 14%, transparent),
|
||||
transparent 30%
|
||||
),
|
||||
linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--announcement-accent) 96%, white 4%),
|
||||
color-mix(in srgb, var(--announcement-accent) 76%, black 24%)
|
||||
);
|
||||
border-color: color-mix(in srgb, var(--announcement-contrast) 28%, transparent);
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, var(--announcement-contrast) 20%, transparent) inset,
|
||||
0 18px 46px color-mix(in srgb, var(--announcement-accent) 30%, transparent);
|
||||
}
|
||||
|
||||
.wire-announcement__accent {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset-block: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 0.25rem;
|
||||
background: var(--announcement-accent);
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="solid"] .wire-announcement__accent,
|
||||
.wire-announcement[data-variant="minimal"] .wire-announcement__accent {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-announcement__glow {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
display: block;
|
||||
border-radius: 999px;
|
||||
pointer-events: none;
|
||||
filter: blur(3rem);
|
||||
opacity: 0.28;
|
||||
}
|
||||
|
||||
.wire-announcement__glow--one {
|
||||
width: 16rem;
|
||||
height: 16rem;
|
||||
inset-block-start: -10rem;
|
||||
inset-inline-end: -4rem;
|
||||
background: var(--announcement-muted);
|
||||
}
|
||||
|
||||
.wire-announcement__glow--two {
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
inset-block-end: -7rem;
|
||||
inset-inline-start: 28%;
|
||||
background: var(--announcement-soft);
|
||||
opacity: 0.18;
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="solid"] .wire-announcement__glow {
|
||||
background: var(--announcement-contrast);
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="minimal"] .wire-announcement__glow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-announcement__inner {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
padding: 1rem 1.125rem;
|
||||
}
|
||||
|
||||
.wire-announcement[data-size="sm"] .wire-announcement__inner,
|
||||
.wire-announcement[data-compact="true"] .wire-announcement__inner {
|
||||
padding-block: 0.75rem;
|
||||
}
|
||||
|
||||
.wire-announcement[data-size="lg"] .wire-announcement__inner {
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
|
||||
.wire-announcement__content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.875rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-announcement__icon {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
color: var(--announcement-accent);
|
||||
background: var(--announcement-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--announcement-accent) 20%, transparent);
|
||||
border-radius: 0.875rem;
|
||||
box-shadow: 0 8px 20px color-mix(in srgb, var(--announcement-accent) 12%, transparent);
|
||||
}
|
||||
|
||||
.wire-announcement__icon > span {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
}
|
||||
|
||||
.wire-announcement[data-size="sm"] .wire-announcement__icon,
|
||||
.wire-announcement[data-compact="true"] .wire-announcement__icon {
|
||||
width: 2.35rem;
|
||||
height: 2.35rem;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.wire-announcement[data-size="lg"] .wire-announcement__icon {
|
||||
width: 3.15rem;
|
||||
height: 3.15rem;
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="solid"] .wire-announcement__icon {
|
||||
color: var(--announcement-contrast);
|
||||
background: color-mix(in srgb, var(--announcement-contrast) 12%, transparent);
|
||||
border-color: color-mix(in srgb, var(--announcement-contrast) 22%, transparent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-announcement__copy {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
padding-top: 0.1rem;
|
||||
}
|
||||
|
||||
.wire-announcement__headline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.55rem 0.8rem;
|
||||
}
|
||||
|
||||
.wire-announcement__badge {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
min-height: 1.6rem;
|
||||
padding: 0.25rem 0.65rem;
|
||||
color: var(--announcement-accent);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
background: var(--announcement-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--announcement-accent) 22%, transparent);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.wire-announcement__badge > span:first-child:not(:last-child) {
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="solid"] .wire-announcement__badge {
|
||||
color: var(--announcement-contrast);
|
||||
background: color-mix(in srgb, var(--announcement-contrast) 13%, transparent);
|
||||
border-color: color-mix(in srgb, var(--announcement-contrast) 24%, transparent);
|
||||
}
|
||||
|
||||
.wire-announcement__message {
|
||||
margin: 0;
|
||||
color: var(--wire-color-text);
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.wire-announcement[data-size="sm"] .wire-announcement__message,
|
||||
.wire-announcement[data-compact="true"] .wire-announcement__message {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.wire-announcement[data-size="lg"] .wire-announcement__message {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="solid"] .wire-announcement__message {
|
||||
color: var(--announcement-contrast);
|
||||
}
|
||||
|
||||
.wire-announcement__description {
|
||||
max-width: 52rem;
|
||||
margin: 0.28rem 0 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.wire-announcement[data-size="lg"] .wire-announcement__description {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="solid"] .wire-announcement__description {
|
||||
color: color-mix(in srgb, var(--announcement-contrast) 76%, transparent);
|
||||
}
|
||||
|
||||
.wire-announcement__actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
padding-inline-start: 3.625rem;
|
||||
}
|
||||
|
||||
.wire-announcement__action,
|
||||
.wire-announcement__dismiss {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 2.5rem;
|
||||
color: var(--wire-color-text);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 650;
|
||||
text-decoration: none;
|
||||
background: var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 5px 14px rgb(0 0 0 / 0.06);
|
||||
transition:
|
||||
transform 160ms ease,
|
||||
border-color 160ms ease,
|
||||
background-color 160ms ease,
|
||||
box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.wire-announcement__action {
|
||||
gap: 0.45rem;
|
||||
padding: 0.55rem 0.9rem;
|
||||
}
|
||||
|
||||
.wire-announcement__action > span:last-child {
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.wire-announcement__dismiss {
|
||||
width: 2.5rem;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wire-announcement__dismiss > span {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.wire-announcement__action:hover,
|
||||
.wire-announcement__dismiss:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: color-mix(in srgb, var(--announcement-accent) 42%, var(--wire-color-border));
|
||||
box-shadow: 0 8px 20px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.wire-announcement__action:hover > span:last-child {
|
||||
transform: translateX(0.16rem);
|
||||
}
|
||||
|
||||
.wire-announcement__action:focus-visible,
|
||||
.wire-announcement__dismiss:focus-visible {
|
||||
outline: 2px solid var(--wire-color-focus, var(--announcement-accent));
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="solid"] .wire-announcement__action {
|
||||
color: var(--announcement-accent);
|
||||
background: var(--announcement-contrast);
|
||||
border-color: transparent;
|
||||
box-shadow: 0 8px 22px color-mix(in srgb, black 18%, transparent);
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="solid"] .wire-announcement__dismiss {
|
||||
color: var(--announcement-contrast);
|
||||
background: color-mix(in srgb, var(--announcement-contrast) 13%, transparent);
|
||||
border-color: color-mix(in srgb, var(--announcement-contrast) 24%, transparent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="solid"] .wire-announcement__action:hover {
|
||||
background: color-mix(in srgb, var(--announcement-contrast) 92%, transparent);
|
||||
}
|
||||
|
||||
.wire-announcement[data-variant="solid"] .wire-announcement__dismiss:hover {
|
||||
background: color-mix(in srgb, var(--announcement-contrast) 20%, transparent);
|
||||
border-color: color-mix(in srgb, var(--announcement-contrast) 34%, transparent);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.wire-announcement__inner {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.wire-announcement__content {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.wire-announcement__actions {
|
||||
justify-content: flex-end;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-announcement__surface,
|
||||
.wire-announcement[data-width="wide"] .wire-announcement__surface {
|
||||
width: calc(100% - 1rem);
|
||||
}
|
||||
|
||||
.wire-announcement[data-width="full"] .wire-announcement__surface {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.wire-announcement__surface {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.wire-announcement__inner {
|
||||
padding-inline: 0.9rem;
|
||||
}
|
||||
|
||||
.wire-announcement__actions {
|
||||
width: 100%;
|
||||
padding-inline-start: 3.625rem;
|
||||
}
|
||||
|
||||
.wire-announcement__action {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-announcement__action,
|
||||
.wire-announcement__dismiss,
|
||||
.wire-announcement__action > span:last-child {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.wire-announcement__action:hover,
|
||||
.wire-announcement__dismiss:hover,
|
||||
.wire-announcement__action:hover > span:last-child {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,12 @@ component BackToTop {
|
||||
offset = "md"
|
||||
behavior = "smooth"
|
||||
showProgress = false
|
||||
showLabel = false
|
||||
alwaysVisible = false
|
||||
size = "default"
|
||||
color = "primary"
|
||||
variant = "solid"
|
||||
shape = "round"
|
||||
class = ""
|
||||
}
|
||||
|
||||
@@ -19,51 +22,316 @@ component BackToTop {
|
||||
|
||||
view {
|
||||
<button
|
||||
{...attrs}
|
||||
data-ui-component="BackToTop"
|
||||
type="button"
|
||||
aria-label='{ariaLabel}'
|
||||
title='{label}'
|
||||
data-show='visible'
|
||||
class='fixed z-50 inline-flex items-center justify-center rounded-full border border-[var(--wire-color-border)] shadow-xl backdrop-blur transition duration-200 hover:-translate-y-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--wire-color-background)] {class}'
|
||||
class:bottom-4='offset === "sm"'
|
||||
class:bottom-6='offset === "md"'
|
||||
class:bottom-8='offset === "lg"'
|
||||
class:right-4='position === "right" && offset === "sm"'
|
||||
class:right-6='position === "right" && offset === "md"'
|
||||
class:right-8='position === "right" && offset === "lg"'
|
||||
class:left-4='position === "left" && offset === "sm"'
|
||||
class:left-6='position === "left" && offset === "md"'
|
||||
class:left-8='position === "left" && offset === "lg"'
|
||||
class:left-1/2='position === "center"'
|
||||
class:-translate-x-1/2='position === "center"'
|
||||
class:size-10='size === "sm"'
|
||||
class:size-12='size === "default" || size === "md"'
|
||||
class:size-14='size === "lg"'
|
||||
class:bg-[var(--wire-color-primary)]='variant === "solid" && color === "primary"'
|
||||
class:text-[var(--wire-color-on-primary)]='variant === "solid" && color === "primary"'
|
||||
class:bg-[var(--wire-color-danger)]='variant === "solid" && color === "danger"'
|
||||
class:text-[var(--wire-color-on-danger)]='variant === "solid" && color === "danger"'
|
||||
class:bg-[var(--wire-color-surface-raised)]='variant === "outline" || variant === "soft"'
|
||||
class:text-[var(--wire-color-primary)]='variant === "outline" || variant === "soft"'
|
||||
data-position='{position}'
|
||||
data-offset='{offset}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
data-shape='{shape}'
|
||||
data-show-label='{showLabel ? "true" : "false"}'
|
||||
data-progress='{showProgress ? "true" : "false"}'
|
||||
data-show='{alwaysVisible || visible}'
|
||||
class='wire-back-to-top {class}'
|
||||
style='--wire-back-to-top-progress: {progress}%;'
|
||||
@window:scroll='visible = window.scrollY >= threshold; progress = Math.min(100, Math.round((window.scrollY / Math.max(1, document.documentElement.scrollHeight - window.innerHeight)) * 100))'
|
||||
@window:scroll='visible = window.scrollY >= threshold; progress = Math.min(100, Math.max(0, Math.round((window.scrollY / Math.max(1, document.documentElement.scrollHeight - window.innerHeight)) * 100)))'
|
||||
@click='window.scrollTo({ top: 0, behavior: behavior })'
|
||||
>
|
||||
{#if showProgress}
|
||||
<span class="wire-back-to-top-progress absolute inset-0 rounded-full" aria-hidden="true"></span>
|
||||
<span class="wire-back-to-top__progress" aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class='{icon + " relative z-10 size-5"}' aria-hidden="true"></span>
|
||||
<span class="sr-only">{label}</span>
|
||||
|
||||
<span class="wire-back-to-top__surface">
|
||||
<span class='wire-back-to-top__icon {icon}' aria-hidden="true"></span>
|
||||
|
||||
{#if showLabel}
|
||||
<span class="wire-back-to-top__label">{label}</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span class="wire-back-to-top__sr">{label}</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-back-to-top-progress {
|
||||
.wire-back-to-top {
|
||||
--back-accent: var(--wire-color-primary);
|
||||
--back-accent-hover: var(--wire-color-primary-hover);
|
||||
--back-accent-soft: var(--wire-color-primary-soft);
|
||||
--back-contrast: var(--wire-color-primary-contrast);
|
||||
|
||||
position: fixed;
|
||||
z-index: 70;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 3rem;
|
||||
min-height: 3rem;
|
||||
padding: 0;
|
||||
color: var(--back-contrast);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
filter: drop-shadow(0 14px 30px color-mix(in srgb, black 22%, transparent));
|
||||
transition:
|
||||
transform 180ms ease,
|
||||
opacity 180ms ease,
|
||||
filter 180ms ease;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-color="secondary"] {
|
||||
--back-accent: var(--wire-color-secondary);
|
||||
--back-accent-hover: var(--wire-color-secondary-hover);
|
||||
--back-accent-soft: var(--wire-color-secondary-soft);
|
||||
--back-contrast: var(--wire-color-secondary-contrast);
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-color="info"] {
|
||||
--back-accent: var(--wire-color-info);
|
||||
--back-accent-hover: var(--wire-color-info-hover);
|
||||
--back-accent-soft: var(--wire-color-info-soft);
|
||||
--back-contrast: var(--wire-color-info-contrast);
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-color="success"] {
|
||||
--back-accent: var(--wire-color-success);
|
||||
--back-accent-hover: var(--wire-color-success-hover);
|
||||
--back-accent-soft: var(--wire-color-success-soft);
|
||||
--back-contrast: var(--wire-color-success-contrast);
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-color="warning"] {
|
||||
--back-accent: var(--wire-color-warning);
|
||||
--back-accent-hover: var(--wire-color-warning-hover);
|
||||
--back-accent-soft: var(--wire-color-warning-soft);
|
||||
--back-contrast: var(--wire-color-warning-contrast);
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-color="danger"] {
|
||||
--back-accent: var(--wire-color-danger);
|
||||
--back-accent-hover: var(--wire-color-danger-hover);
|
||||
--back-accent-soft: var(--wire-color-danger-soft);
|
||||
--back-contrast: var(--wire-color-danger-contrast);
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-position="right"] {
|
||||
right: 1.5rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-position="left"] {
|
||||
left: 1.5rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-position="center"] {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-position="center"]:hover {
|
||||
transform: translateX(-50%) translateY(-3px);
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-offset="sm"] {
|
||||
bottom: 1rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-offset="md"] {
|
||||
bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-offset="lg"] {
|
||||
bottom: 2rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-offset="xl"] {
|
||||
bottom: 3rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-size="sm"] {
|
||||
min-width: 2.5rem;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-size="lg"] {
|
||||
min-width: 3.5rem;
|
||||
min-height: 3.5rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-show-label="true"] {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-back-to-top:hover {
|
||||
transform: translateY(-3px);
|
||||
filter: drop-shadow(0 18px 36px color-mix(in srgb, var(--back-accent) 24%, transparent));
|
||||
}
|
||||
|
||||
.wire-back-to-top:active {
|
||||
transform: translateY(-1px) scale(0.98);
|
||||
}
|
||||
|
||||
.wire-back-to-top__surface {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.55rem;
|
||||
min-width: 3rem;
|
||||
min-height: 3rem;
|
||||
padding: 0.75rem;
|
||||
color: var(--back-contrast);
|
||||
background:
|
||||
linear-gradient(145deg, color-mix(in srgb, white 10%, transparent), transparent 56%),
|
||||
var(--back-accent);
|
||||
border: 1px solid color-mix(in srgb, white 18%, transparent);
|
||||
border-radius: inherit;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 20%, transparent) inset,
|
||||
0 10px 24px color-mix(in srgb, black 18%, transparent);
|
||||
backdrop-filter: blur(12px);
|
||||
transition:
|
||||
background-color 160ms ease,
|
||||
border-color 160ms ease,
|
||||
color 160ms ease;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-size="sm"] .wire-back-to-top__surface {
|
||||
min-width: 2.5rem;
|
||||
min-height: 2.5rem;
|
||||
padding: 0.58rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-size="lg"] .wire-back-to-top__surface {
|
||||
min-width: 3.5rem;
|
||||
min-height: 3.5rem;
|
||||
padding: 0.9rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-show-label="true"] .wire-back-to-top__surface {
|
||||
min-width: 0;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-variant="outline"] .wire-back-to-top__surface {
|
||||
color: var(--back-accent);
|
||||
background: color-mix(in srgb, var(--wire-color-surface-raised) 88%, transparent);
|
||||
border-color: color-mix(in srgb, var(--back-accent) 46%, var(--wire-color-border));
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-variant="soft"] .wire-back-to-top__surface {
|
||||
color: var(--back-accent);
|
||||
background: color-mix(in srgb, var(--back-accent-soft) 88%, var(--wire-color-surface-raised));
|
||||
border-color: color-mix(in srgb, var(--back-accent) 22%, var(--wire-color-border));
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-variant="ghost"] .wire-back-to-top__surface {
|
||||
color: var(--back-accent);
|
||||
background: color-mix(in srgb, var(--wire-color-surface-raised) 68%, transparent);
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-variant="minimal"] .wire-back-to-top__surface {
|
||||
color: var(--back-accent);
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-shape="rounded"] {
|
||||
border-radius: 0.9rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-shape="pill"] {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.wire-back-to-top__progress {
|
||||
position: absolute;
|
||||
inset: -0.22rem;
|
||||
z-index: 1;
|
||||
border-radius: inherit;
|
||||
background: conic-gradient(
|
||||
currentColor var(--wire-back-to-top-progress),
|
||||
transparent var(--wire-back-to-top-progress)
|
||||
var(--back-accent) var(--wire-back-to-top-progress),
|
||||
color-mix(in srgb, var(--wire-color-border) 72%, transparent) var(--wire-back-to-top-progress)
|
||||
);
|
||||
opacity: 0.22;
|
||||
mask: radial-gradient(farthest-side, transparent calc(100% - 0.18rem), #000 calc(100% - 0.17rem));
|
||||
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 0.18rem), #000 calc(100% - 0.17rem));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wire-back-to-top__icon {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-size="sm"] .wire-back-to-top__icon {
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-size="lg"] .wire-back-to-top__icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top__label {
|
||||
white-space: nowrap;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wire-back-to-top__sr {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.wire-back-to-top:focus-visible {
|
||||
outline: 2px solid var(--wire-color-focus);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-back-to-top[data-position="right"] {
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-position="left"] {
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
.wire-back-to-top[data-offset="lg"],
|
||||
.wire-back-to-top[data-offset="xl"] {
|
||||
bottom: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-back-to-top,
|
||||
.wire-back-to-top__surface {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.wire-back-to-top:hover,
|
||||
.wire-back-to-top[data-position="center"]:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,321 @@
|
||||
component Breadcrumb {
|
||||
props {
|
||||
@event navigate = function
|
||||
@event click = function
|
||||
size = "default"
|
||||
color = "primary"
|
||||
@event select = function
|
||||
|
||||
label = "Breadcrumb"
|
||||
items = []
|
||||
active = ""
|
||||
orientation = "horizontal"
|
||||
separator = "chevron"
|
||||
showHome = false
|
||||
homeLabel = "Home"
|
||||
homeHref = "/"
|
||||
homeIcon = "icon-[lucide--house]"
|
||||
size = "default"
|
||||
color = "primary"
|
||||
variant = "minimal"
|
||||
class = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--breadcrumb wire-next--{orientation} {class}" aria-label="{label}">
|
||||
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
|
||||
<slot />
|
||||
<nav
|
||||
{...attrs}
|
||||
data-ui-component="Breadcrumb"
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
aria-label='{label}'
|
||||
class='wire-breadcrumb {class}'
|
||||
>
|
||||
<ol class="wire-breadcrumb__list">
|
||||
{#if showHome}
|
||||
<li class="wire-breadcrumb__item wire-breadcrumb__item--home">
|
||||
<a
|
||||
href='{homeHref || "/"}'
|
||||
class="wire-breadcrumb__link wire-breadcrumb__home"
|
||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: { label: homeLabel, href: homeHref || "/", value: "home" }, itemIndex: -1 } }))'
|
||||
>
|
||||
{#if homeIcon === "icon-[lucide--house]"}
|
||||
<span class="icon-[lucide--house] wire-breadcrumb__icon" aria-hidden="true"></span>
|
||||
{:else if homeIcon}
|
||||
<span class='{homeIcon} wire-breadcrumb__icon' aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
<span class="wire-breadcrumb__home-label">{homeLabel}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/if}
|
||||
|
||||
{#each items as item, itemIndex}
|
||||
<li
|
||||
class="wire-breadcrumb__item"
|
||||
data-current='{item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1) ? "true" : "false"}'
|
||||
>
|
||||
{#if showHome || itemIndex > 0}
|
||||
<span class="wire-breadcrumb__separator" aria-hidden="true">
|
||||
{#if separator === "slash"}
|
||||
<span>/</span>
|
||||
{:else if separator === "dot"}
|
||||
<span>•</span>
|
||||
{:else if separator === "arrow"}
|
||||
<span>→</span>
|
||||
{:else}
|
||||
<span class="icon-[lucide--chevron-right] wire-breadcrumb__separator-icon"></span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if item.href && !item.disabled && !(item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1))}
|
||||
<a
|
||||
href='{item.href}'
|
||||
target='{item.target || ""}'
|
||||
rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
|
||||
class="wire-breadcrumb__link"
|
||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, itemIndex: itemIndex } }))'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
<span class="wire-breadcrumb__label">{item.label || item.title}</span>
|
||||
|
||||
{#if item.external}
|
||||
<span class="icon-[lucide--arrow-up-right] wire-breadcrumb__external" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<span
|
||||
class="wire-breadcrumb__current"
|
||||
aria-current='{item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1) ? "page" : ""}'
|
||||
aria-disabled='{item.disabled ? "true" : "false"}'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
<span class="wire-breadcrumb__label">{item.label || item.title}</span>
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</nav>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-breadcrumb {
|
||||
--breadcrumb-accent: var(--wire-color-primary);
|
||||
--breadcrumb-soft: var(--wire-color-primary-soft);
|
||||
--breadcrumb-muted: var(--wire-color-primary-muted);
|
||||
--breadcrumb-text: var(--wire-color-primary-text);
|
||||
|
||||
min-width: 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-color="secondary"] {
|
||||
--breadcrumb-accent: var(--wire-color-secondary);
|
||||
--breadcrumb-soft: var(--wire-color-secondary-soft);
|
||||
--breadcrumb-muted: var(--wire-color-secondary-muted);
|
||||
--breadcrumb-text: var(--wire-color-secondary-text);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-color="info"] {
|
||||
--breadcrumb-accent: var(--wire-color-info);
|
||||
--breadcrumb-soft: var(--wire-color-info-soft);
|
||||
--breadcrumb-muted: var(--wire-color-info-muted);
|
||||
--breadcrumb-text: var(--wire-color-info-text);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-color="success"] {
|
||||
--breadcrumb-accent: var(--wire-color-success);
|
||||
--breadcrumb-soft: var(--wire-color-success-soft);
|
||||
--breadcrumb-muted: var(--wire-color-success-muted);
|
||||
--breadcrumb-text: var(--wire-color-success-text);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-color="warning"] {
|
||||
--breadcrumb-accent: var(--wire-color-warning);
|
||||
--breadcrumb-soft: var(--wire-color-warning-soft);
|
||||
--breadcrumb-muted: var(--wire-color-warning-muted);
|
||||
--breadcrumb-text: var(--wire-color-warning-text);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-color="danger"] {
|
||||
--breadcrumb-accent: var(--wire-color-danger);
|
||||
--breadcrumb-soft: var(--wire-color-danger-soft);
|
||||
--breadcrumb-muted: var(--wire-color-danger-muted);
|
||||
--breadcrumb-text: var(--wire-color-danger-text);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="soft"] {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
padding: 0.45rem 0.65rem;
|
||||
background: var(--breadcrumb-soft);
|
||||
border: 1px solid var(--breadcrumb-muted);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="outline"] {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
padding: 0.45rem 0.65rem;
|
||||
background: var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: var(--wire-shadow-sm);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="contrast"] {
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.wire-breadcrumb__list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: auto;
|
||||
list-style: none;
|
||||
scrollbar-width: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__list::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__separator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-inline: 0.45rem;
|
||||
color: var(--wire-color-text-subtle);
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__separator-icon {
|
||||
width: 0.9rem;
|
||||
height: 0.9rem;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__link,
|
||||
.wire-breadcrumb__current {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
min-width: 0;
|
||||
min-height: 2rem;
|
||||
padding: 0.25rem 0.4rem;
|
||||
border-radius: 0.55rem;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.25rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-size="sm"] .wire-breadcrumb__link,
|
||||
.wire-breadcrumb[data-size="sm"] .wire-breadcrumb__current {
|
||||
min-height: 1.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-size="lg"] .wire-breadcrumb__link,
|
||||
.wire-breadcrumb[data-size="lg"] .wire-breadcrumb__current {
|
||||
min-height: 2.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__link {
|
||||
color: var(--wire-color-text-muted);
|
||||
font-weight: 600;
|
||||
transition:
|
||||
color 160ms ease,
|
||||
background 160ms ease;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__link:hover {
|
||||
color: var(--breadcrumb-accent);
|
||||
background: var(--breadcrumb-soft);
|
||||
}
|
||||
|
||||
.wire-breadcrumb__link:focus-visible {
|
||||
color: var(--breadcrumb-accent);
|
||||
outline: 2px solid var(--breadcrumb-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__current {
|
||||
max-width: 22rem;
|
||||
color: var(--wire-color-text);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__item[data-current="true"] .wire-breadcrumb__current {
|
||||
color: var(--breadcrumb-text);
|
||||
background: var(--breadcrumb-soft);
|
||||
}
|
||||
|
||||
.wire-breadcrumb__current[aria-disabled="true"] {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__icon,
|
||||
.wire-breadcrumb__external {
|
||||
flex: 0 0 auto;
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__home {
|
||||
color: var(--breadcrumb-accent);
|
||||
}
|
||||
|
||||
.wire-breadcrumb__label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__link,
|
||||
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__current,
|
||||
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__home {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__link:hover,
|
||||
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__item[data-current="true"] .wire-breadcrumb__current {
|
||||
color: #ffffff;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__separator {
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-breadcrumb__home-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__current {
|
||||
max-width: 13rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-breadcrumb__link {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,136 +15,684 @@ component CTASection {
|
||||
secondaryHref = ""
|
||||
secondaryIcon = ""
|
||||
backgroundImage = ""
|
||||
visualImage = ""
|
||||
visualAlt = ""
|
||||
visualIcon = ""
|
||||
visualTitle = ""
|
||||
visualDescription = ""
|
||||
visualItems = []
|
||||
visualPosition = "right"
|
||||
maxWidth = "xl"
|
||||
fullBleed = false
|
||||
class = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<Section
|
||||
spacing='{size === "compact" ? "md" : "lg"}'
|
||||
variant="default"
|
||||
color='{color}'
|
||||
maxWidth='{maxWidth}'
|
||||
class='{class}'
|
||||
<section
|
||||
{...attrs}
|
||||
data-ui-component="CTASection"
|
||||
data-align='{align}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
data-max-width='{maxWidth}'
|
||||
data-full-bleed='{fullBleed ? "true" : "false"}'
|
||||
data-visual-position='{visualPosition}'
|
||||
class='wire-cta-section {class}'
|
||||
>
|
||||
<div
|
||||
data-ui-component="CTASection"
|
||||
class="relative isolate overflow-hidden rounded-3xl border border-[var(--wire-color-border)] p-8 shadow-xl sm:p-10 lg:p-14"
|
||||
class:bg-[var(--wire-color-primary)]='variant === "solid" && color === "primary"'
|
||||
class:text-[var(--wire-color-on-primary)]='variant === "solid" && color === "primary"'
|
||||
class:bg-[var(--wire-color-danger)]='variant === "solid" && color === "danger"'
|
||||
class:text-[var(--wire-color-on-danger)]='variant === "solid" && color === "danger"'
|
||||
class:bg-[var(--wire-color-primary-soft)]='variant === "soft" && color === "primary"'
|
||||
class:bg-[var(--wire-color-success-soft)]='variant === "soft" && color === "success"'
|
||||
class:bg-[var(--wire-color-warning-soft)]='variant === "soft" && color === "warning"'
|
||||
class:bg-[var(--wire-color-danger-soft)]='variant === "soft" && color === "danger"'
|
||||
class:bg-[var(--wire-color-surface-raised)]='variant === "default" || variant === "outline"'
|
||||
class:bg-gradient-to-br='variant === "gradient"'
|
||||
class:from-[var(--wire-color-primary)]='variant === "gradient"'
|
||||
class:to-[var(--wire-color-secondary)]='variant === "gradient"'
|
||||
class:text-[var(--wire-color-on-primary)]='variant === "gradient"'
|
||||
class:text-center='align === "center"'
|
||||
>
|
||||
{#if backgroundImage}
|
||||
<img
|
||||
src='{backgroundImage}'
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="absolute inset-0 -z-20 h-full w-full object-cover opacity-15"
|
||||
/>
|
||||
{/if}
|
||||
<div class="wire-cta-section__outer">
|
||||
<div class="wire-cta-section__surface">
|
||||
{#if backgroundImage}
|
||||
<img
|
||||
src='{backgroundImage}'
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="wire-cta-section__background-image"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="pointer-events-none absolute -right-20 -top-20 -z-10 size-72 rounded-full bg-white/15 blur-3xl"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<div
|
||||
class="pointer-events-none absolute -bottom-24 -left-20 -z-10 size-72 rounded-full bg-black/10 blur-3xl"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<div class="wire-cta-section__glow wire-cta-section__glow--one" aria-hidden="true"></div>
|
||||
<div class="wire-cta-section__glow wire-cta-section__glow--two" aria-hidden="true"></div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-8"
|
||||
class:items-center='align === "center"'
|
||||
class:lg:flex-row='align === "split"'
|
||||
class:lg:items-center='align === "split"'
|
||||
class:lg:justify-between='align === "split"'
|
||||
>
|
||||
<div class="max-w-3xl">
|
||||
{#if icon}
|
||||
<span
|
||||
class="mb-5 inline-flex size-12 items-center justify-center rounded-2xl bg-white/15"
|
||||
class:bg-[var(--wire-color-primary-soft)]='variant !== "solid" && variant !== "gradient"'
|
||||
class:text-[var(--wire-color-primary)]='variant !== "solid" && variant !== "gradient"'
|
||||
>
|
||||
<span class='{icon + " size-6"}' aria-hidden="true"></span>
|
||||
</span>
|
||||
{/if}
|
||||
<div class="wire-cta-section__layout">
|
||||
<div class="wire-cta-section__content">
|
||||
{#if icon}
|
||||
<div class="wire-cta-section__icon" aria-hidden="true">
|
||||
<span class='{icon}'></span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if eyebrow}
|
||||
<p
|
||||
class="text-xs font-bold uppercase tracking-[0.18em]"
|
||||
class:text-[var(--wire-color-primary)]='variant !== "solid" && variant !== "gradient"'
|
||||
class:opacity-80='variant === "solid" || variant === "gradient"'
|
||||
>
|
||||
{eyebrow}
|
||||
</p>
|
||||
{/if}
|
||||
{#if eyebrow}
|
||||
<p class="wire-cta-section__eyebrow">{eyebrow}</p>
|
||||
{/if}
|
||||
|
||||
<h2
|
||||
class="mt-3 text-3xl font-bold leading-tight tracking-tight sm:text-4xl lg:text-5xl"
|
||||
class:text-[var(--wire-color-text)]='variant !== "solid" && variant !== "gradient"'
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<h2 class="wire-cta-section__title">{title}</h2>
|
||||
|
||||
{#if description}
|
||||
<p
|
||||
class="mt-4 text-base leading-7 sm:text-lg"
|
||||
class:text-[var(--wire-color-text-muted)]='variant !== "solid" && variant !== "gradient"'
|
||||
class:opacity-85='variant === "solid" || variant === "gradient"'
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p class="wire-cta-section__description">{description}</p>
|
||||
{/if}
|
||||
|
||||
<slot></slot>
|
||||
<slot></slot>
|
||||
|
||||
{#if primaryLabel || secondaryLabel}
|
||||
<div class="wire-cta-section__actions">
|
||||
{#if primaryLabel}
|
||||
<a href='{primaryHref || "#"}' class="wire-cta-section__action wire-cta-section__action--primary">
|
||||
{#if primaryIcon}
|
||||
<span class='wire-cta-section__action-icon {primaryIcon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{primaryLabel}</span>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if secondaryLabel}
|
||||
<a href='{secondaryHref || "#"}' class="wire-cta-section__action wire-cta-section__action--secondary">
|
||||
{#if secondaryIcon}
|
||||
<span class='wire-cta-section__action-icon {secondaryIcon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{secondaryLabel}</span>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="wire-cta-section__actions wire-cta-section__actions--slot">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="wire-cta-section__visual-column">
|
||||
<slot name="visual"></slot>
|
||||
|
||||
{#if visualImage || visualIcon || visualTitle || visualDescription || visualItems.length > 0}
|
||||
<div class="wire-cta-section__visual">
|
||||
{#if visualImage}
|
||||
<div class="wire-cta-section__visual-media">
|
||||
<img src='{visualImage}' alt='{visualAlt}' class="wire-cta-section__visual-image" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if visualIcon || visualTitle || visualDescription}
|
||||
<div class="wire-cta-section__visual-header">
|
||||
{#if visualIcon}
|
||||
<div class="wire-cta-section__visual-icon" aria-hidden="true">
|
||||
<span class='{visualIcon}'></span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="wire-cta-section__visual-copy">
|
||||
{#if visualTitle}
|
||||
<h3 class="wire-cta-section__visual-title">{visualTitle}</h3>
|
||||
{/if}
|
||||
|
||||
{#if visualDescription}
|
||||
<p class="wire-cta-section__visual-description">{visualDescription}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if visualItems.length > 0}
|
||||
<div class="wire-cta-section__visual-list">
|
||||
{#each visualItems as item}
|
||||
<div class="wire-cta-section__visual-item">
|
||||
{#if item.icon}
|
||||
<span class='wire-cta-section__visual-item-icon {item.icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<div>
|
||||
<strong>{item.label || item.title}</strong>
|
||||
{#if item.description}
|
||||
<p>{item.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HeroActions
|
||||
actions='{[
|
||||
{
|
||||
label: primaryLabel,
|
||||
href: primaryHref,
|
||||
icon: primaryIcon,
|
||||
variant: "default",
|
||||
color: color,
|
||||
controlClass: variant === "solid" || variant === "gradient" ? "!bg-white !text-[var(--wire-color-primary)] hover:!bg-white/90" : ""
|
||||
},
|
||||
{
|
||||
label: secondaryLabel,
|
||||
href: secondaryHref,
|
||||
icon: secondaryIcon,
|
||||
variant: "outline",
|
||||
color: color,
|
||||
controlClass: variant === "solid" || variant === "gradient" ? "!border-white/50 !text-white hover:!bg-white/10" : ""
|
||||
}
|
||||
]}'
|
||||
align='{align === "center" ? "center" : "right"}'
|
||||
stackOnMobile="true"
|
||||
fullWidthMobile="true"
|
||||
size="lg"
|
||||
color='{color}'
|
||||
class="shrink-0"
|
||||
>
|
||||
<slot name="actions"></slot>
|
||||
</HeroActions>
|
||||
|
||||
<slot name="visual"></slot>
|
||||
<div class="wire-cta-section__footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</Section>
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-cta-section {
|
||||
--cta-accent: var(--wire-color-primary);
|
||||
--cta-accent-hover: var(--wire-color-primary-hover);
|
||||
--cta-accent-soft: var(--wire-color-primary-soft);
|
||||
--cta-accent-muted: var(--wire-color-primary-muted);
|
||||
--cta-contrast: var(--wire-color-primary-contrast);
|
||||
--cta-border: color-mix(in srgb, var(--cta-accent) 15%, var(--wire-color-border));
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
color: var(--wire-color-text);
|
||||
}
|
||||
|
||||
.wire-cta-section[data-color="secondary"] {
|
||||
--cta-accent: var(--wire-color-secondary);
|
||||
--cta-accent-hover: var(--wire-color-secondary-hover);
|
||||
--cta-accent-soft: var(--wire-color-secondary-soft);
|
||||
--cta-accent-muted: var(--wire-color-secondary-muted);
|
||||
--cta-contrast: var(--wire-color-secondary-contrast);
|
||||
}
|
||||
|
||||
.wire-cta-section[data-color="info"] {
|
||||
--cta-accent: var(--wire-color-info);
|
||||
--cta-accent-hover: var(--wire-color-info-hover);
|
||||
--cta-accent-soft: var(--wire-color-info-soft);
|
||||
--cta-accent-muted: var(--wire-color-info-muted);
|
||||
--cta-contrast: var(--wire-color-info-contrast);
|
||||
}
|
||||
|
||||
.wire-cta-section[data-color="success"] {
|
||||
--cta-accent: var(--wire-color-success);
|
||||
--cta-accent-hover: var(--wire-color-success-hover);
|
||||
--cta-accent-soft: var(--wire-color-success-soft);
|
||||
--cta-accent-muted: var(--wire-color-success-muted);
|
||||
--cta-contrast: var(--wire-color-success-contrast);
|
||||
}
|
||||
|
||||
.wire-cta-section[data-color="warning"] {
|
||||
--cta-accent: var(--wire-color-warning);
|
||||
--cta-accent-hover: var(--wire-color-warning-hover);
|
||||
--cta-accent-soft: var(--wire-color-warning-soft);
|
||||
--cta-accent-muted: var(--wire-color-warning-muted);
|
||||
--cta-contrast: var(--wire-color-warning-contrast);
|
||||
}
|
||||
|
||||
.wire-cta-section[data-color="danger"] {
|
||||
--cta-accent: var(--wire-color-danger);
|
||||
--cta-accent-hover: var(--wire-color-danger-hover);
|
||||
--cta-accent-soft: var(--wire-color-danger-soft);
|
||||
--cta-accent-muted: var(--wire-color-danger-muted);
|
||||
--cta-contrast: var(--wire-color-danger-contrast);
|
||||
}
|
||||
|
||||
.wire-cta-section__outer {
|
||||
width: min(calc(100% - 2rem), 80rem);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-max-width="compact"] .wire-cta-section__outer {
|
||||
width: min(calc(100% - 2rem), 64rem);
|
||||
}
|
||||
|
||||
.wire-cta-section[data-max-width="lg"] .wire-cta-section__outer {
|
||||
width: min(calc(100% - 2rem), 72rem);
|
||||
}
|
||||
|
||||
.wire-cta-section[data-max-width="wide"] .wire-cta-section__outer,
|
||||
.wire-cta-section[data-max-width="2xl"] .wire-cta-section__outer {
|
||||
width: min(calc(100% - 2rem), 90rem);
|
||||
}
|
||||
|
||||
.wire-cta-section[data-max-width="full"] .wire-cta-section__outer {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-full-bleed="true"] .wire-cta-section__outer {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.wire-cta-section__surface {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
padding: 3rem;
|
||||
background:
|
||||
radial-gradient(circle at 92% 12%, color-mix(in srgb, var(--cta-accent) 10%, transparent), transparent 28%),
|
||||
var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--cta-border);
|
||||
border-radius: 1.5rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 24px 70px color-mix(in srgb, black 15%, transparent);
|
||||
}
|
||||
|
||||
.wire-cta-section[data-size="compact"] .wire-cta-section__surface,
|
||||
.wire-cta-section[data-size="sm"] .wire-cta-section__surface {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-size="lg"] .wire-cta-section__surface {
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-full-bleed="true"] .wire-cta-section__surface {
|
||||
border-inline: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-variant="soft"] .wire-cta-section__surface {
|
||||
background:
|
||||
radial-gradient(circle at 92% 12%, color-mix(in srgb, var(--cta-accent) 14%, transparent), transparent 30%),
|
||||
linear-gradient(135deg, var(--cta-accent-soft), transparent 64%),
|
||||
var(--wire-color-surface-raised);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-variant="outline"] .wire-cta-section__surface {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-variant="gradient"] .wire-cta-section__surface {
|
||||
color: var(--cta-contrast);
|
||||
background:
|
||||
radial-gradient(circle at 90% 8%, color-mix(in srgb, white 16%, transparent), transparent 30%),
|
||||
linear-gradient(135deg, var(--cta-accent), color-mix(in srgb, var(--cta-accent) 70%, var(--wire-color-secondary)));
|
||||
border-color: color-mix(in srgb, white 22%, transparent);
|
||||
}
|
||||
|
||||
.wire-cta-section[data-variant="solid"] .wire-cta-section__surface {
|
||||
color: var(--cta-contrast);
|
||||
background:
|
||||
radial-gradient(circle at 90% 8%, color-mix(in srgb, white 14%, transparent), transparent 30%),
|
||||
var(--cta-accent);
|
||||
border-color: color-mix(in srgb, white 22%, transparent);
|
||||
box-shadow: 0 30px 80px color-mix(in srgb, var(--cta-accent) 28%, transparent);
|
||||
}
|
||||
|
||||
.wire-cta-section__background-image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 0.14;
|
||||
}
|
||||
|
||||
.wire-cta-section__glow {
|
||||
position: absolute;
|
||||
z-index: -2;
|
||||
width: 22rem;
|
||||
height: 22rem;
|
||||
pointer-events: none;
|
||||
background: var(--cta-accent);
|
||||
border-radius: 999px;
|
||||
filter: blur(110px);
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.wire-cta-section__glow--one {
|
||||
top: -12rem;
|
||||
right: -7rem;
|
||||
}
|
||||
|
||||
.wire-cta-section__glow--two {
|
||||
bottom: -14rem;
|
||||
left: -8rem;
|
||||
opacity: 0.06;
|
||||
}
|
||||
|
||||
.wire-cta-section__layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
.wire-cta-section__content,
|
||||
.wire-cta-section__visual-column {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-cta-section__content {
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-align="center"] .wire-cta-section__content {
|
||||
margin-inline: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-align="right"] .wire-cta-section__content {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.wire-cta-section__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin-bottom: 1.1rem;
|
||||
color: var(--cta-accent);
|
||||
background: var(--cta-accent-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--cta-accent) 20%, transparent);
|
||||
border-radius: 0.9rem;
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-variant="solid"] .wire-cta-section__icon,
|
||||
.wire-cta-section[data-variant="gradient"] .wire-cta-section__icon {
|
||||
color: currentColor;
|
||||
background: color-mix(in srgb, white 14%, transparent);
|
||||
border-color: color-mix(in srgb, white 22%, transparent);
|
||||
}
|
||||
|
||||
.wire-cta-section__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--cta-accent);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-variant="solid"] .wire-cta-section__eyebrow,
|
||||
.wire-cta-section[data-variant="gradient"] .wire-cta-section__eyebrow {
|
||||
color: currentColor;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.wire-cta-section__title {
|
||||
max-width: 18ch;
|
||||
margin: 0.75rem 0 0;
|
||||
color: var(--wire-color-text);
|
||||
font-size: clamp(2rem, 5vw, 4rem);
|
||||
font-weight: 650;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.045em;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-align="center"] .wire-cta-section__title,
|
||||
.wire-cta-section[data-align="right"] .wire-cta-section__title {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-align="right"] .wire-cta-section__title {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-variant="solid"] .wire-cta-section__title,
|
||||
.wire-cta-section[data-variant="gradient"] .wire-cta-section__title {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-cta-section__description {
|
||||
max-width: 44rem;
|
||||
margin: 1rem 0 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.96rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-align="center"] .wire-cta-section__description,
|
||||
.wire-cta-section[data-align="right"] .wire-cta-section__description {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-align="right"] .wire-cta-section__description {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-variant="solid"] .wire-cta-section__description,
|
||||
.wire-cta-section[data-variant="gradient"] .wire-cta-section__description {
|
||||
color: color-mix(in srgb, currentColor 82%, transparent);
|
||||
}
|
||||
|
||||
.wire-cta-section__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
.wire-cta-section__actions--slot:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-align="center"] .wire-cta-section__actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-align="right"] .wire-cta-section__actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.wire-cta-section__action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.55rem;
|
||||
min-height: 2.85rem;
|
||||
padding: 0.78rem 1.1rem;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
transform 160ms ease,
|
||||
background-color 160ms ease,
|
||||
color 160ms ease,
|
||||
border-color 160ms ease;
|
||||
}
|
||||
|
||||
.wire-cta-section__action:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.wire-cta-section__action--primary {
|
||||
color: var(--cta-contrast);
|
||||
background: var(--cta-accent);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.wire-cta-section__action--primary:hover {
|
||||
background: var(--cta-accent-hover);
|
||||
}
|
||||
|
||||
.wire-cta-section__action--secondary {
|
||||
color: var(--cta-accent);
|
||||
background: transparent;
|
||||
border: 1px solid color-mix(in srgb, var(--cta-accent) 42%, var(--wire-color-border));
|
||||
}
|
||||
|
||||
.wire-cta-section__action--secondary:hover {
|
||||
background: var(--cta-accent-soft);
|
||||
border-color: var(--cta-accent);
|
||||
}
|
||||
|
||||
.wire-cta-section[data-variant="solid"] .wire-cta-section__action--primary,
|
||||
.wire-cta-section[data-variant="gradient"] .wire-cta-section__action--primary {
|
||||
color: var(--cta-accent);
|
||||
background: var(--cta-contrast);
|
||||
}
|
||||
|
||||
.wire-cta-section[data-variant="solid"] .wire-cta-section__action--secondary,
|
||||
.wire-cta-section[data-variant="gradient"] .wire-cta-section__action--secondary {
|
||||
color: currentColor;
|
||||
border-color: color-mix(in srgb, white 46%, transparent);
|
||||
}
|
||||
|
||||
.wire-cta-section[data-variant="solid"] .wire-cta-section__action--secondary:hover,
|
||||
.wire-cta-section[data-variant="gradient"] .wire-cta-section__action--secondary:hover {
|
||||
background: color-mix(in srgb, white 12%, transparent);
|
||||
border-color: color-mix(in srgb, white 68%, transparent);
|
||||
}
|
||||
|
||||
.wire-cta-section__action-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.wire-cta-section__action:focus-visible {
|
||||
outline: 2px solid var(--wire-color-focus);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.wire-cta-section__visual-column {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.wire-cta-section__visual {
|
||||
overflow: hidden;
|
||||
color: var(--wire-color-text);
|
||||
background: var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 1.2rem;
|
||||
box-shadow: 0 20px 54px color-mix(in srgb, black 18%, transparent);
|
||||
}
|
||||
|
||||
.wire-cta-section__visual-media {
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
background: var(--wire-color-surface-soft);
|
||||
}
|
||||
|
||||
.wire-cta-section__visual-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.wire-cta-section__visual-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 0.9rem;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.wire-cta-section__visual-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.6rem;
|
||||
height: 2.6rem;
|
||||
color: var(--cta-accent);
|
||||
background: var(--cta-accent-soft);
|
||||
border-radius: 0.82rem;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.wire-cta-section__visual-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-cta-section__visual-title {
|
||||
margin: 0;
|
||||
color: var(--wire-color-text);
|
||||
font-size: 1rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.wire-cta-section__visual-description {
|
||||
margin: 0.42rem 0 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.wire-cta-section__visual-list {
|
||||
display: grid;
|
||||
border-top: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-cta-section__visual-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 0.7rem;
|
||||
padding: 0.95rem 1.25rem;
|
||||
}
|
||||
|
||||
.wire-cta-section__visual-item + .wire-cta-section__visual-item {
|
||||
border-top: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-cta-section__visual-item-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-top: 0.12rem;
|
||||
color: var(--cta-accent);
|
||||
}
|
||||
|
||||
.wire-cta-section__visual-item strong {
|
||||
color: var(--wire-color-text);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.wire-cta-section__visual-item p {
|
||||
margin: 0.3rem 0 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.73rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.wire-cta-section__footer:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-cta-section__footer:not(:empty) {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.25rem;
|
||||
border-top: 1px solid color-mix(in srgb, currentColor 14%, transparent);
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.wire-cta-section[data-align="split"] .wire-cta-section__layout {
|
||||
grid-template-columns: minmax(0, 1.25fr) minmax(18rem, 0.75fr);
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-align="split"][data-visual-position="left"] .wire-cta-section__content {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-align="split"][data-visual-position="left"] .wire-cta-section__visual-column {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-align="split"] .wire-cta-section__visual-column:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-cta-section[data-align="split"] .wire-cta-section__visual-column:empty + * {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-cta-section__surface {
|
||||
padding: 1.6rem;
|
||||
border-radius: 1.2rem;
|
||||
}
|
||||
|
||||
.wire-cta-section__actions {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.wire-cta-section__action {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-cta-section__action {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.wire-cta-section__action:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,516 @@ component ContextMenu {
|
||||
@event open = function
|
||||
@event close = function
|
||||
@event select = function
|
||||
@event action = function
|
||||
|
||||
items = []
|
||||
open = false
|
||||
defaultOpen = false
|
||||
trigger = "contextmenu"
|
||||
placement = "pointer"
|
||||
align = "start"
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Context Menu"
|
||||
variant = "raised"
|
||||
title = ""
|
||||
description = ""
|
||||
open = false
|
||||
placement = "bottom"
|
||||
closeLabel = "Close"
|
||||
label = "Context menu"
|
||||
closeOnSelect = true
|
||||
closeOnOutside = true
|
||||
disabled = false
|
||||
minWidth = "14rem"
|
||||
maxWidth = "20rem"
|
||||
class = ""
|
||||
}
|
||||
|
||||
state visible = defaultOpen
|
||||
state positionX = 16
|
||||
state positionY = 16
|
||||
|
||||
functions {
|
||||
function isOpen() {
|
||||
return open || visible
|
||||
}
|
||||
|
||||
function showMenu(sourceEvent) {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
if (sourceEvent && sourceEvent.type === "contextmenu") {
|
||||
sourceEvent.preventDefault()
|
||||
}
|
||||
if (placement === "pointer" && sourceEvent) {
|
||||
positionX = Math.max(12, Math.min(sourceEvent.clientX || 12, window.innerWidth - 340))
|
||||
positionY = Math.max(12, Math.min(sourceEvent.clientY || 12, window.innerHeight - 420))
|
||||
}
|
||||
visible = true
|
||||
$emit("open", {
|
||||
x: positionX,
|
||||
y: positionY,
|
||||
trigger: trigger,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
|
||||
function hideMenu(reason, sourceEvent) {
|
||||
visible = false
|
||||
$emit("close", {
|
||||
reason: reason,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
|
||||
function toggleMenu(sourceEvent) {
|
||||
if (isOpen()) {
|
||||
hideMenu("toggle", sourceEvent)
|
||||
} else {
|
||||
showMenu(sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
function chooseItem(item, itemIndex, sourceEvent) {
|
||||
if (disabled || item.disabled || item.type === "header" || item.type === "divider") {
|
||||
sourceEvent.preventDefault()
|
||||
return
|
||||
}
|
||||
$emit("select", {
|
||||
item: item,
|
||||
itemIndex: itemIndex,
|
||||
value: item.value || "",
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
if (item.action) {
|
||||
$emit("action", {
|
||||
item: item,
|
||||
itemIndex: itemIndex,
|
||||
value: item.value || "",
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
if (closeOnSelect) {
|
||||
hideMenu("select", sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
function moveFocus(sourceEvent, direction) {
|
||||
const root = sourceEvent.currentTarget.closest(".wire-context-menu") || sourceEvent.currentTarget
|
||||
const options = [...root.querySelectorAll("[data-context-menu-item]:not([disabled])")]
|
||||
if (!options.length) {
|
||||
return
|
||||
}
|
||||
const activeIndex = options.indexOf(document.activeElement)
|
||||
const nextIndex = (activeIndex + direction + options.length) % options.length
|
||||
options[nextIndex].focus()
|
||||
}
|
||||
|
||||
function handleKeydown(sourceEvent) {
|
||||
if (sourceEvent.key === "Escape") {
|
||||
sourceEvent.preventDefault()
|
||||
hideMenu("escape", sourceEvent)
|
||||
} else if (sourceEvent.key === "ArrowDown") {
|
||||
sourceEvent.preventDefault()
|
||||
moveFocus(sourceEvent, 1)
|
||||
} else if (sourceEvent.key === "ArrowUp") {
|
||||
sourceEvent.preventDefault()
|
||||
moveFocus(sourceEvent, -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div data-show="{open}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--context-menu wire-next--placement-{placement} {class}" role="region" aria-modal="false" aria-label="{title}">
|
||||
<header><strong>{title}</strong><button type="button" aria-label="{closeLabel}">×</button></header>
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
<slot />
|
||||
<div
|
||||
{...attrs}
|
||||
data-ui-component="ContextMenu"
|
||||
data-open='{open || visible ? "true" : "false"}'
|
||||
data-trigger='{trigger}'
|
||||
data-placement='{placement}'
|
||||
data-align='{align}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
class='wire-context-menu {class}'
|
||||
style='--wire-context-x:{positionX}px;--wire-context-y:{positionY}px;--wire-context-min-width:{minWidth};--wire-context-max-width:{maxWidth};'
|
||||
@keydown='handleKeydown(event)'
|
||||
>
|
||||
<div
|
||||
class="wire-context-menu__trigger"
|
||||
tabindex='{disabled ? "-1" : "0"}'
|
||||
aria-haspopup="menu"
|
||||
aria-expanded='{open || visible ? "true" : "false"}'
|
||||
@contextmenu='if (trigger === "contextmenu" || trigger === "both") { showMenu(event) }'
|
||||
@click='if (trigger === "click" || trigger === "both") { toggleMenu(event) }'
|
||||
@keydown='if (event.key === "Enter" || event.key === " ") { event.preventDefault(); toggleMenu(event) }'
|
||||
>
|
||||
<slot name="trigger"></slot>
|
||||
</div>
|
||||
|
||||
{#if closeOnOutside}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-context-menu__dismiss-layer"
|
||||
data-show='{open || visible}'
|
||||
aria-label="Close context menu"
|
||||
@click='hideMenu("outside", event)'
|
||||
></button>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-context-menu__panel"
|
||||
data-show='{open || visible}'
|
||||
role="menu"
|
||||
aria-label='{label}'
|
||||
>
|
||||
{#if title || description}
|
||||
<div class="wire-context-menu__header">
|
||||
{#if title}
|
||||
<strong>{title}</strong>
|
||||
{/if}
|
||||
{#if description}
|
||||
<span>{description}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<slot name="header"></slot>
|
||||
|
||||
<div class="wire-context-menu__items">
|
||||
{#each items as item, itemIndex}
|
||||
{#if item.type === "divider"}
|
||||
<div class="wire-context-menu__divider" role="separator"></div>
|
||||
{:else if item.type === "header"}
|
||||
<div class="wire-context-menu__section-label">{item.label || item.title}</div>
|
||||
{:else if item.href}
|
||||
<a
|
||||
href='{item.href}'
|
||||
target='{item.target || ""}'
|
||||
rel='{item.external || item.target === "_blank" ? "noopener noreferrer" : (item.rel || "")}'
|
||||
role="menuitem"
|
||||
data-context-menu-item
|
||||
data-danger='{item.danger ? "true" : "false"}'
|
||||
data-selected='{item.selected || item.checked ? "true" : "false"}'
|
||||
aria-disabled='{item.disabled ? "true" : "false"}'
|
||||
class="wire-context-menu__item"
|
||||
@click='chooseItem(item, itemIndex, event)'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='wire-context-menu__icon {item.icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="wire-context-menu__copy">
|
||||
<strong>{item.label || item.title}</strong>
|
||||
{#if item.description}
|
||||
<small>{item.description}</small>
|
||||
{/if}
|
||||
</span>
|
||||
{#if item.shortcut}
|
||||
<kbd>{item.shortcut}</kbd>
|
||||
{:else if item.checked || item.selected}
|
||||
<span class="icon-[lucide--check] wire-context-menu__status" aria-hidden="true"></span>
|
||||
{:else if item.external}
|
||||
<span class="icon-[lucide--arrow-up-right] wire-context-menu__status" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-context-menu-item
|
||||
data-danger='{item.danger ? "true" : "false"}'
|
||||
data-selected='{item.selected || item.checked ? "true" : "false"}'
|
||||
disabled='{item.disabled}'
|
||||
class="wire-context-menu__item"
|
||||
@click='chooseItem(item, itemIndex, event)'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='wire-context-menu__icon {item.icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="wire-context-menu__copy">
|
||||
<strong>{item.label || item.title}</strong>
|
||||
{#if item.description}
|
||||
<small>{item.description}</small>
|
||||
{/if}
|
||||
</span>
|
||||
{#if item.shortcut}
|
||||
<kbd>{item.shortcut}</kbd>
|
||||
{:else if item.checked || item.selected}
|
||||
<span class="icon-[lucide--check] wire-context-menu__status" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-context-menu {
|
||||
--context-accent: var(--wire-color-primary);
|
||||
--context-soft: var(--wire-color-primary-soft);
|
||||
position: relative;
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-color="secondary"] {
|
||||
--context-accent: var(--wire-color-secondary);
|
||||
--context-soft: var(--wire-color-secondary-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-color="info"] {
|
||||
--context-accent: var(--wire-color-info);
|
||||
--context-soft: var(--wire-color-info-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-color="success"] {
|
||||
--context-accent: var(--wire-color-success);
|
||||
--context-soft: var(--wire-color-success-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-color="warning"] {
|
||||
--context-accent: var(--wire-color-warning);
|
||||
--context-soft: var(--wire-color-warning-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-color="danger"] {
|
||||
--context-accent: var(--wire-color-danger);
|
||||
--context-soft: var(--wire-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu__trigger {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-context-menu__trigger:focus-visible {
|
||||
border-radius: 0.6rem;
|
||||
outline: 2px solid var(--wire-color-focus);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.wire-context-menu__dismiss-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1090;
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.wire-context-menu__panel {
|
||||
position: absolute;
|
||||
z-index: 1091;
|
||||
top: calc(100% + 0.5rem);
|
||||
left: 0;
|
||||
width: max-content;
|
||||
min-width: var(--wire-context-min-width);
|
||||
max-width: min(var(--wire-context-max-width), calc(100vw - 1.5rem));
|
||||
padding: 0.45rem;
|
||||
color: var(--wire-color-text);
|
||||
background:
|
||||
linear-gradient(145deg, color-mix(in srgb, var(--context-accent) 5%, transparent), transparent 58%),
|
||||
color-mix(in srgb, var(--wire-color-surface-raised) 96%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--context-accent) 18%, var(--wire-color-border));
|
||||
border-radius: 1rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 24px 64px color-mix(in srgb, black 24%, transparent);
|
||||
backdrop-filter: blur(18px);
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-placement="pointer"] .wire-context-menu__panel {
|
||||
position: fixed;
|
||||
top: var(--wire-context-y);
|
||||
left: var(--wire-context-x);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-placement="bottom-end"] .wire-context-menu__panel {
|
||||
right: 0;
|
||||
left: auto;
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-placement="top-start"] .wire-context-menu__panel {
|
||||
top: auto;
|
||||
bottom: calc(100% + 0.5rem);
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-placement="top-end"] .wire-context-menu__panel {
|
||||
top: auto;
|
||||
right: 0;
|
||||
bottom: calc(100% + 0.5rem);
|
||||
left: auto;
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-variant="soft"] .wire-context-menu__panel {
|
||||
background:
|
||||
linear-gradient(145deg, var(--context-soft), transparent 72%),
|
||||
var(--wire-color-surface-raised);
|
||||
box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-variant="outline"] .wire-context-menu__panel {
|
||||
background: var(--wire-color-surface-raised);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-context-menu__header {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
padding: 0.65rem 0.75rem 0.75rem;
|
||||
border-bottom: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-context-menu__header strong {
|
||||
font-size: 0.86rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.wire-context-menu__header span {
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.wire-context-menu__items {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
padding-block: 0.25rem;
|
||||
}
|
||||
|
||||
.wire-context-menu__section-label {
|
||||
padding: 0.55rem 0.75rem 0.3rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wire-context-menu__divider {
|
||||
height: 1px;
|
||||
margin: 0.3rem 0.45rem;
|
||||
background: var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-context-menu__item {
|
||||
appearance: none;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0.65rem 0.7rem;
|
||||
color: var(--wire-color-text);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0.72rem;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms ease, color 150ms ease, transform 150ms ease;
|
||||
}
|
||||
|
||||
.wire-context-menu__item:hover,
|
||||
.wire-context-menu__item:focus-visible,
|
||||
.wire-context-menu__item[data-selected="true"] {
|
||||
color: var(--context-accent);
|
||||
background: var(--context-soft);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-context-menu__item:active {
|
||||
transform: scale(0.985);
|
||||
}
|
||||
|
||||
.wire-context-menu__item[data-danger="true"] {
|
||||
color: var(--wire-color-danger);
|
||||
}
|
||||
|
||||
.wire-context-menu__item[data-danger="true"]:hover,
|
||||
.wire-context-menu__item[data-danger="true"]:focus-visible {
|
||||
background: var(--wire-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu__item[disabled],
|
||||
.wire-context-menu__item[aria-disabled="true"] {
|
||||
opacity: 0.48;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wire-context-menu__icon,
|
||||
.wire-context-menu__status {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-context-menu__copy {
|
||||
display: grid;
|
||||
gap: 0.12rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-context-menu__copy strong {
|
||||
overflow: hidden;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wire-context-menu__copy small {
|
||||
overflow: hidden;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wire-context-menu kbd {
|
||||
padding: 0.16rem 0.38rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
background: var(--wire-color-surface-soft);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.36rem;
|
||||
font-size: 0.64rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-size="sm"] .wire-context-menu__item {
|
||||
padding: 0.52rem 0.6rem;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-size="lg"] .wire-context-menu__item {
|
||||
padding: 0.78rem 0.82rem;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-context-menu[data-placement="pointer"] .wire-context-menu__panel {
|
||||
right: 0.75rem;
|
||||
bottom: 0.75rem;
|
||||
left: 0.75rem;
|
||||
top: auto;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-context-menu__item {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,450 @@ component Drawer {
|
||||
props {
|
||||
@event open = function
|
||||
@event close = function
|
||||
size = "default"
|
||||
@event cancel = function
|
||||
|
||||
open = false
|
||||
defaultOpen = false
|
||||
placement = "right"
|
||||
size = "md"
|
||||
color = "primary"
|
||||
variant = "default"
|
||||
title = "Drawer"
|
||||
description = ""
|
||||
open = false
|
||||
placement = "bottom"
|
||||
closeLabel = "Close"
|
||||
icon = ""
|
||||
label = "Drawer"
|
||||
closeLabel = "Close drawer"
|
||||
showClose = true
|
||||
closeOnBackdrop = true
|
||||
closeOnEscape = true
|
||||
overlay = true
|
||||
scrollable = true
|
||||
triggerLabel = ""
|
||||
triggerIcon = ""
|
||||
class = ""
|
||||
}
|
||||
|
||||
state visible = defaultOpen
|
||||
|
||||
functions {
|
||||
function isOpen() {
|
||||
return open || visible
|
||||
}
|
||||
|
||||
function showDrawer(sourceEvent) {
|
||||
visible = true
|
||||
$emit("open", {
|
||||
placement: placement,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
|
||||
function hideDrawer(reason, sourceEvent) {
|
||||
visible = false
|
||||
$emit("close", {
|
||||
reason: reason,
|
||||
placement: placement,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
|
||||
function cancelDrawer(sourceEvent) {
|
||||
$emit("cancel", {
|
||||
placement: placement,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
hideDrawer("cancel", sourceEvent)
|
||||
}
|
||||
|
||||
function handleKeydown(sourceEvent) {
|
||||
if (closeOnEscape && sourceEvent.key === "Escape") {
|
||||
sourceEvent.preventDefault()
|
||||
cancelDrawer(sourceEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div data-show="{open}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--drawer wire-next--placement-{placement} {class}" role="dialog" aria-modal="true" aria-label="{title}">
|
||||
<header><strong>{title}</strong><button type="button" aria-label="{closeLabel}">×</button></header>
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
<slot />
|
||||
<div
|
||||
{...attrs}
|
||||
data-ui-component="Drawer"
|
||||
data-open='{open || visible ? "true" : "false"}'
|
||||
data-placement='{placement}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
data-overlay='{overlay ? "true" : "false"}'
|
||||
data-scrollable='{scrollable ? "true" : "false"}'
|
||||
class='wire-drawer {class}'
|
||||
>
|
||||
{#if triggerLabel}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-drawer__trigger"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded='{open || visible ? "true" : "false"}'
|
||||
@click='showDrawer(event)'
|
||||
>
|
||||
{#if triggerIcon}
|
||||
<span class='{triggerIcon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{triggerLabel}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<span class="wire-drawer__trigger-slot" @click='showDrawer(event)'>
|
||||
<slot name="trigger"></slot>
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="wire-drawer__layer"
|
||||
data-show='{open || visible}'
|
||||
role="presentation"
|
||||
@keydown='handleKeydown(event)'
|
||||
>
|
||||
{#if overlay}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-drawer__backdrop"
|
||||
aria-label='{closeLabel}'
|
||||
@click='if (closeOnBackdrop) { cancelDrawer(event) }'
|
||||
></button>
|
||||
{/if}
|
||||
|
||||
<section
|
||||
class="wire-drawer__panel"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label='{label || title}'
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="wire-drawer__handle" aria-hidden="true"></div>
|
||||
|
||||
<header class="wire-drawer__header">
|
||||
<div class="wire-drawer__heading">
|
||||
{#if icon}
|
||||
<span class='wire-drawer__icon {icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
<div class="wire-drawer__heading-copy">
|
||||
<slot name="header"></slot>
|
||||
{#if title}
|
||||
<h2>{title}</h2>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p>{description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showClose}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-drawer__close"
|
||||
aria-label='{closeLabel}'
|
||||
@click='hideDrawer("close-button", event)'
|
||||
>
|
||||
<span class="icon-[lucide--x]" aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<div class="wire-drawer__body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<footer class="wire-drawer__footer">
|
||||
<slot name="footer"></slot>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-drawer {
|
||||
--drawer-accent: var(--wire-color-primary);
|
||||
--drawer-soft: var(--wire-color-primary-soft);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.wire-drawer[data-color="secondary"] {
|
||||
--drawer-accent: var(--wire-color-secondary);
|
||||
--drawer-soft: var(--wire-color-secondary-soft);
|
||||
}
|
||||
|
||||
.wire-drawer[data-color="info"] {
|
||||
--drawer-accent: var(--wire-color-info);
|
||||
--drawer-soft: var(--wire-color-info-soft);
|
||||
}
|
||||
|
||||
.wire-drawer[data-color="success"] {
|
||||
--drawer-accent: var(--wire-color-success);
|
||||
--drawer-soft: var(--wire-color-success-soft);
|
||||
}
|
||||
|
||||
.wire-drawer[data-color="warning"] {
|
||||
--drawer-accent: var(--wire-color-warning);
|
||||
--drawer-soft: var(--wire-color-warning-soft);
|
||||
}
|
||||
|
||||
.wire-drawer[data-color="danger"] {
|
||||
--drawer-accent: var(--wire-color-danger);
|
||||
--drawer-soft: var(--wire-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-drawer__trigger,
|
||||
.wire-drawer__trigger-slot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.wire-drawer__trigger {
|
||||
appearance: none;
|
||||
min-height: 2.55rem;
|
||||
padding: 0.65rem 1rem;
|
||||
color: var(--wire-color-primary-contrast);
|
||||
background: var(--drawer-accent);
|
||||
border: 0;
|
||||
border-radius: 0.8rem;
|
||||
font: inherit;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wire-drawer__layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1200;
|
||||
display: flex;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wire-drawer__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
background: color-mix(in srgb, black 56%, transparent);
|
||||
border: 0;
|
||||
backdrop-filter: blur(7px);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.wire-drawer[data-overlay="false"] .wire-drawer__backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-drawer__panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(28rem, calc(100vw - 1rem));
|
||||
max-height: 100%;
|
||||
margin-left: auto;
|
||||
color: var(--wire-color-text);
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 100% 0%,
|
||||
color-mix(in srgb, var(--drawer-accent) 8%, transparent),
|
||||
transparent 34%
|
||||
),
|
||||
var(--wire-color-surface-raised);
|
||||
border-left: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
|
||||
box-shadow: -28px 0 80px color-mix(in srgb, black 28%, transparent);
|
||||
pointer-events: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wire-drawer[data-size="sm"] .wire-drawer__panel {
|
||||
width: min(22rem, calc(100vw - 1rem));
|
||||
}
|
||||
|
||||
.wire-drawer[data-size="lg"] .wire-drawer__panel {
|
||||
width: min(38rem, calc(100vw - 1rem));
|
||||
}
|
||||
|
||||
.wire-drawer[data-size="xl"] .wire-drawer__panel {
|
||||
width: min(52rem, calc(100vw - 1rem));
|
||||
}
|
||||
|
||||
.wire-drawer[data-size="full"] .wire-drawer__panel {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="left"] .wire-drawer__panel {
|
||||
margin-right: auto;
|
||||
margin-left: 0;
|
||||
border-right: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
|
||||
border-left: 0;
|
||||
box-shadow: 28px 0 80px color-mix(in srgb, black 28%, transparent);
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="top"] .wire-drawer__layer,
|
||||
.wire-drawer[data-placement="bottom"] .wire-drawer__layer {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="top"] .wire-drawer__panel,
|
||||
.wire-drawer[data-placement="bottom"] .wire-drawer__panel {
|
||||
width: 100%;
|
||||
max-height: min(80vh, 44rem);
|
||||
margin: 0;
|
||||
border: 0;
|
||||
box-shadow: 0 24px 80px color-mix(in srgb, black 28%, transparent);
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="top"] .wire-drawer__panel {
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="bottom"] .wire-drawer__layer {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="bottom"] .wire-drawer__panel {
|
||||
border-top: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
|
||||
border-radius: 1.3rem 1.3rem 0 0;
|
||||
}
|
||||
|
||||
.wire-drawer[data-variant="soft"] .wire-drawer__panel {
|
||||
background:
|
||||
linear-gradient(145deg, var(--drawer-soft), transparent 65%),
|
||||
var(--wire-color-surface-raised);
|
||||
}
|
||||
|
||||
.wire-drawer[data-variant="solid"] .wire-drawer__panel {
|
||||
color: var(--wire-color-primary-contrast);
|
||||
background: var(--drawer-accent);
|
||||
border-color: color-mix(in srgb, white 18%, transparent);
|
||||
}
|
||||
|
||||
.wire-drawer__handle {
|
||||
display: none;
|
||||
width: 2.75rem;
|
||||
height: 0.28rem;
|
||||
margin: 0.55rem auto 0;
|
||||
background: color-mix(in srgb, var(--wire-color-text-muted) 48%, transparent);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="bottom"] .wire-drawer__handle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wire-drawer__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1.35rem 1.35rem 1.1rem;
|
||||
border-bottom: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-drawer__heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.85rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-drawer__icon {
|
||||
flex: 0 0 auto;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-top: 0.15rem;
|
||||
color: var(--drawer-accent);
|
||||
}
|
||||
|
||||
.wire-drawer[data-variant="solid"] .wire-drawer__icon {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-drawer__heading-copy {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-drawer__heading-copy h2,
|
||||
.wire-drawer__heading-copy p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wire-drawer__heading-copy h2 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.wire-drawer__heading-copy p {
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.wire-drawer[data-variant="solid"] .wire-drawer__heading-copy p {
|
||||
color: color-mix(in srgb, currentColor 76%, transparent);
|
||||
}
|
||||
|
||||
.wire-drawer__close {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
width: 2.35rem;
|
||||
height: 2.35rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
background: var(--wire-color-surface-soft);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wire-drawer__close:hover,
|
||||
.wire-drawer__close:focus-visible {
|
||||
color: var(--drawer-accent);
|
||||
border-color: color-mix(in srgb, var(--drawer-accent) 34%, var(--wire-color-border));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-drawer__body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: 1.35rem;
|
||||
}
|
||||
|
||||
.wire-drawer[data-scrollable="true"] .wire-drawer__body {
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.wire-drawer__footer {
|
||||
padding: 1rem 1.35rem 1.35rem;
|
||||
border-top: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-drawer__footer:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-drawer[data-placement="right"] .wire-drawer__panel,
|
||||
.wire-drawer[data-placement="left"] .wire-drawer__panel {
|
||||
width: min(24rem, 100vw);
|
||||
}
|
||||
|
||||
.wire-drawer__header,
|
||||
.wire-drawer__body,
|
||||
.wire-drawer__footer {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,50 +4,567 @@ component Dropdown {
|
||||
@event open = function
|
||||
@event close = function
|
||||
@event select = function
|
||||
@event action = function
|
||||
|
||||
items = []
|
||||
open = false
|
||||
defaultOpen = false
|
||||
label = "Open menu"
|
||||
icon = ""
|
||||
showChevron = true
|
||||
menuLabel = "Dropdown menu"
|
||||
placement = "bottom-start"
|
||||
width = "md"
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Open menu"
|
||||
items = []
|
||||
placement = "end"
|
||||
variant = "raised"
|
||||
closeOnSelect = true
|
||||
closeOnOutside = true
|
||||
disabled = false
|
||||
emptyLabel = "No menu items"
|
||||
class = ""
|
||||
}
|
||||
|
||||
state visible = defaultOpen
|
||||
|
||||
functions {
|
||||
function toggleMenu(event) {
|
||||
$emit("toggle", { open: event.currentTarget.open })
|
||||
$emit(event.currentTarget.open ? "open" : "close", { source: "trigger" })
|
||||
function isOpen() {
|
||||
return open || visible
|
||||
}
|
||||
|
||||
function selectItem(item) {
|
||||
$emit("select", { item: item, value: item.value || "" })
|
||||
function showMenu(sourceEvent) {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
visible = true
|
||||
$emit("open", { sourceEvent: sourceEvent })
|
||||
$emit("toggle", { open: true, sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
function hideMenu(reason, sourceEvent) {
|
||||
visible = false
|
||||
$emit("close", { reason: reason, sourceEvent: sourceEvent })
|
||||
$emit("toggle", { open: false, reason: reason, sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
function toggleMenu(sourceEvent) {
|
||||
if (isOpen()) {
|
||||
hideMenu("toggle", sourceEvent)
|
||||
} else {
|
||||
showMenu(sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
function chooseItem(item, itemIndex, sourceEvent) {
|
||||
if (disabled || item.disabled || item.type === "divider" || item.type === "header") {
|
||||
sourceEvent.preventDefault()
|
||||
return
|
||||
}
|
||||
$emit("select", {
|
||||
item: item,
|
||||
itemIndex: itemIndex,
|
||||
value: item.value || "",
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
if (item.action) {
|
||||
$emit("action", {
|
||||
item: item,
|
||||
itemIndex: itemIndex,
|
||||
value: item.value || "",
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
if (closeOnSelect) {
|
||||
hideMenu("select", sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
function moveFocus(sourceEvent, direction) {
|
||||
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
|
||||
const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
|
||||
if (!options.length) {
|
||||
return
|
||||
}
|
||||
const activeIndex = options.indexOf(document.activeElement)
|
||||
const nextIndex = (activeIndex + direction + options.length) % options.length
|
||||
options[nextIndex].focus()
|
||||
}
|
||||
|
||||
function handleKeydown(sourceEvent) {
|
||||
if (sourceEvent.key === "Escape") {
|
||||
sourceEvent.preventDefault()
|
||||
hideMenu("escape", sourceEvent)
|
||||
} else if (sourceEvent.key === "ArrowDown") {
|
||||
sourceEvent.preventDefault()
|
||||
if (!isOpen()) {
|
||||
showMenu(sourceEvent)
|
||||
}
|
||||
moveFocus(sourceEvent, 1)
|
||||
} else if (sourceEvent.key === "ArrowUp") {
|
||||
sourceEvent.preventDefault()
|
||||
if (!isOpen()) {
|
||||
showMenu(sourceEvent)
|
||||
}
|
||||
moveFocus(sourceEvent, -1)
|
||||
} else if (sourceEvent.key === "Home" && isOpen()) {
|
||||
sourceEvent.preventDefault()
|
||||
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
|
||||
const first = root.querySelector("[data-dropdown-item]:not([disabled])")
|
||||
if (first) {
|
||||
first.focus()
|
||||
}
|
||||
} else if (sourceEvent.key === "End" && isOpen()) {
|
||||
sourceEvent.preventDefault()
|
||||
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
|
||||
const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
|
||||
if (options.length) {
|
||||
options[options.length - 1].focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<details class="wire-dropdown wire-dropdown--{placement} wire-next--color-{color} wire-next--size-{size} {class}" data-wrn-dropdown @toggle="toggleMenu($event)">
|
||||
<summary aria-label="{label}">
|
||||
<slot name="trigger" />
|
||||
<span class="wire-dropdown__chevron icon-[lucide--chevron-down]" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="wire-dropdown__panel" role="menu" aria-label="{label}">
|
||||
<slot name="header" />
|
||||
{#each items as item}
|
||||
{#if item.type === "divider"}
|
||||
<hr />
|
||||
{:else}
|
||||
{#if item.type === "header"}
|
||||
<span class="wire-dropdown__heading">{item.label}</span>
|
||||
{:else}
|
||||
<a class="wire-dropdown__item {item.danger ? 'wire-dropdown__item--danger' : ''}" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" role="menuitem" @click="selectItem(item)">
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span><strong>{item.label}</strong>{#if item.description}<small>{item.description}</small>{/if}</span>
|
||||
{#if item.external}<span class="icon-[lucide--arrow-up-right]" aria-hidden="true"></span>{/if}
|
||||
<div
|
||||
{...attrs}
|
||||
data-ui-component="Dropdown"
|
||||
data-open='{open || visible ? "true" : "false"}'
|
||||
data-placement='{placement}'
|
||||
data-width='{width}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
class='wire-dropdown {class}'
|
||||
@keydown='handleKeydown(event)'
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="wire-dropdown__trigger"
|
||||
disabled='{disabled}'
|
||||
aria-haspopup="menu"
|
||||
aria-expanded='{open || visible ? "true" : "false"}'
|
||||
@click='toggleMenu(event)'
|
||||
>
|
||||
<slot name="trigger"></slot>
|
||||
{#if icon}
|
||||
<span class='wire-dropdown__trigger-icon {icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
{#if label}
|
||||
<span class="wire-dropdown__trigger-label">{label}</span>
|
||||
{/if}
|
||||
{#if showChevron}
|
||||
<span class="icon-[lucide--chevron-down] wire-dropdown__chevron" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if closeOnOutside}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-dropdown__dismiss-layer"
|
||||
data-show='{open || visible}'
|
||||
aria-label="Close menu"
|
||||
@click='hideMenu("outside", event)'
|
||||
></button>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-dropdown__panel"
|
||||
data-show='{open || visible}'
|
||||
role="menu"
|
||||
aria-label='{menuLabel}'
|
||||
>
|
||||
<slot name="header"></slot>
|
||||
|
||||
<div class="wire-dropdown__items">
|
||||
{#each items as item, itemIndex}
|
||||
{#if item.type === "divider"}
|
||||
<div class="wire-dropdown__divider" role="separator"></div>
|
||||
{:else if item.type === "header"}
|
||||
<div class="wire-dropdown__section-label">{item.label || item.title}</div>
|
||||
{:else if item.href}
|
||||
<a
|
||||
href='{item.href}'
|
||||
target='{item.target || ""}'
|
||||
rel='{item.external || item.target === "_blank" ? "noopener noreferrer" : (item.rel || "")}'
|
||||
role="menuitem"
|
||||
data-dropdown-item
|
||||
data-danger='{item.danger ? "true" : "false"}'
|
||||
data-selected='{item.selected || item.checked ? "true" : "false"}'
|
||||
aria-disabled='{item.disabled ? "true" : "false"}'
|
||||
class="wire-dropdown__item"
|
||||
@click='chooseItem(item, itemIndex, event)'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='wire-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="wire-dropdown__copy">
|
||||
<strong>{item.label || item.title}</strong>
|
||||
{#if item.description}
|
||||
<small>{item.description}</small>
|
||||
{/if}
|
||||
</span>
|
||||
{#if item.badge}
|
||||
<span class="wire-dropdown__badge">{item.badge}</span>
|
||||
{:else if item.shortcut}
|
||||
<kbd>{item.shortcut}</kbd>
|
||||
{:else if item.checked || item.selected}
|
||||
<span class="icon-[lucide--check] wire-dropdown__status" aria-hidden="true"></span>
|
||||
{:else if item.external}
|
||||
<span class="icon-[lucide--arrow-up-right] wire-dropdown__status" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-dropdown-item
|
||||
data-danger='{item.danger ? "true" : "false"}'
|
||||
data-selected='{item.selected || item.checked ? "true" : "false"}'
|
||||
disabled='{item.disabled}'
|
||||
class="wire-dropdown__item"
|
||||
@click='chooseItem(item, itemIndex, event)'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='wire-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="wire-dropdown__copy">
|
||||
<strong>{item.label || item.title}</strong>
|
||||
{#if item.description}
|
||||
<small>{item.description}</small>
|
||||
{/if}
|
||||
</span>
|
||||
{#if item.badge}
|
||||
<span class="wire-dropdown__badge">{item.badge}</span>
|
||||
{:else if item.shortcut}
|
||||
<kbd>{item.shortcut}</kbd>
|
||||
{:else if item.checked || item.selected}
|
||||
<span class="icon-[lucide--check] wire-dropdown__status" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
<slot name="footer" />
|
||||
{:empty}
|
||||
<div class="wire-dropdown__empty">{emptyLabel}</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-dropdown {
|
||||
--dropdown-accent: var(--wire-color-primary);
|
||||
--dropdown-soft: var(--wire-color-primary-soft);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-color="secondary"] {
|
||||
--dropdown-accent: var(--wire-color-secondary);
|
||||
--dropdown-soft: var(--wire-color-secondary-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-color="info"] {
|
||||
--dropdown-accent: var(--wire-color-info);
|
||||
--dropdown-soft: var(--wire-color-info-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-color="success"] {
|
||||
--dropdown-accent: var(--wire-color-success);
|
||||
--dropdown-soft: var(--wire-color-success-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-color="warning"] {
|
||||
--dropdown-accent: var(--wire-color-warning);
|
||||
--dropdown-soft: var(--wire-color-warning-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-color="danger"] {
|
||||
--dropdown-accent: var(--wire-color-danger);
|
||||
--dropdown-soft: var(--wire-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown__trigger {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.55rem;
|
||||
min-height: 2.55rem;
|
||||
padding: 0.65rem 0.9rem;
|
||||
color: var(--wire-color-text);
|
||||
background: var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.78rem;
|
||||
font: inherit;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: border-color 150ms ease, background-color 150ms ease, color 150ms ease;
|
||||
}
|
||||
|
||||
.wire-dropdown__trigger:hover,
|
||||
.wire-dropdown__trigger:focus-visible,
|
||||
.wire-dropdown[data-open="true"] .wire-dropdown__trigger {
|
||||
color: var(--dropdown-accent);
|
||||
background: var(--dropdown-soft);
|
||||
border-color: color-mix(in srgb, var(--dropdown-accent) 38%, var(--wire-color-border));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-dropdown__trigger:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.wire-dropdown__trigger-icon,
|
||||
.wire-dropdown__chevron {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.wire-dropdown__chevron {
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-open="true"] .wire-dropdown__chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-size="sm"] .wire-dropdown__trigger {
|
||||
min-height: 2.2rem;
|
||||
padding: 0.5rem 0.72rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-size="lg"] .wire-dropdown__trigger {
|
||||
min-height: 2.9rem;
|
||||
padding: 0.78rem 1.05rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.wire-dropdown__dismiss-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1100;
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.wire-dropdown__panel {
|
||||
position: absolute;
|
||||
z-index: 1101;
|
||||
top: calc(100% + 0.55rem);
|
||||
left: 0;
|
||||
width: max-content;
|
||||
min-width: 12rem;
|
||||
max-width: min(22rem, calc(100vw - 1.5rem));
|
||||
padding: 0.45rem;
|
||||
color: var(--wire-color-text);
|
||||
background:
|
||||
linear-gradient(145deg, color-mix(in srgb, var(--dropdown-accent) 5%, transparent), transparent 60%),
|
||||
color-mix(in srgb, var(--wire-color-surface-raised) 97%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--dropdown-accent) 18%, var(--wire-color-border));
|
||||
border-radius: 1rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 24px 64px color-mix(in srgb, black 22%, transparent);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-width="trigger"] .wire-dropdown__panel {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-width="sm"] .wire-dropdown__panel {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-width="md"] .wire-dropdown__panel {
|
||||
width: 17rem;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-width="lg"] .wire-dropdown__panel {
|
||||
width: 22rem;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-placement="bottom-end"] .wire-dropdown__panel {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-placement="top-start"] .wire-dropdown__panel {
|
||||
top: auto;
|
||||
bottom: calc(100% + 0.55rem);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-placement="top-end"] .wire-dropdown__panel {
|
||||
top: auto;
|
||||
right: 0;
|
||||
bottom: calc(100% + 0.55rem);
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-variant="soft"] .wire-dropdown__panel {
|
||||
background:
|
||||
linear-gradient(145deg, var(--dropdown-soft), transparent 70%),
|
||||
var(--wire-color-surface-raised);
|
||||
box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-variant="outline"] .wire-dropdown__panel {
|
||||
background: var(--wire-color-surface-raised);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-dropdown__items {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.wire-dropdown__section-label {
|
||||
padding: 0.55rem 0.72rem 0.3rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wire-dropdown__divider {
|
||||
height: 1px;
|
||||
margin: 0.3rem 0.4rem;
|
||||
background: var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-dropdown__item {
|
||||
appearance: none;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0.68rem 0.72rem;
|
||||
color: var(--wire-color-text);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0.72rem;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms ease, color 150ms ease, transform 150ms ease;
|
||||
}
|
||||
|
||||
.wire-dropdown__item:hover,
|
||||
.wire-dropdown__item:focus-visible,
|
||||
.wire-dropdown__item[data-selected="true"] {
|
||||
color: var(--dropdown-accent);
|
||||
background: var(--dropdown-soft);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-dropdown__item:active {
|
||||
transform: scale(0.985);
|
||||
}
|
||||
|
||||
.wire-dropdown__item[data-danger="true"] {
|
||||
color: var(--wire-color-danger);
|
||||
}
|
||||
|
||||
.wire-dropdown__item[data-danger="true"]:hover,
|
||||
.wire-dropdown__item[data-danger="true"]:focus-visible {
|
||||
background: var(--wire-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown__item[disabled],
|
||||
.wire-dropdown__item[aria-disabled="true"] {
|
||||
opacity: 0.48;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wire-dropdown__item-icon,
|
||||
.wire-dropdown__status {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.wire-dropdown__copy {
|
||||
display: grid;
|
||||
gap: 0.12rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-dropdown__copy strong {
|
||||
overflow: hidden;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wire-dropdown__copy small {
|
||||
overflow: hidden;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wire-dropdown__badge {
|
||||
padding: 0.18rem 0.45rem;
|
||||
color: var(--dropdown-accent);
|
||||
background: var(--dropdown-soft);
|
||||
border-radius: 999px;
|
||||
font-size: 0.64rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wire-dropdown kbd {
|
||||
padding: 0.16rem 0.38rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
background: var(--wire-color-surface-soft);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.36rem;
|
||||
font-size: 0.64rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.wire-dropdown__empty {
|
||||
padding: 1rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-dropdown__panel {
|
||||
position: fixed;
|
||||
right: 0.75rem;
|
||||
bottom: 0.75rem;
|
||||
left: 0.75rem;
|
||||
top: auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-dropdown__trigger,
|
||||
.wire-dropdown__chevron,
|
||||
.wire-dropdown__item {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
component FeatureCard {
|
||||
props {
|
||||
icon = ""
|
||||
iconStyle = "soft"
|
||||
iconSize = "default"
|
||||
eyebrow = ""
|
||||
title = "Feature"
|
||||
description = ""
|
||||
|
||||
image = ""
|
||||
imageAlt = ""
|
||||
imagePosition = "top"
|
||||
imageAspect = "wide"
|
||||
imageLoading = "lazy"
|
||||
|
||||
href = ""
|
||||
target = ""
|
||||
rel = ""
|
||||
external = false
|
||||
actionLabel = "Learn more"
|
||||
actionIcon = ""
|
||||
showArrow = true
|
||||
stretchedLink = true
|
||||
|
||||
badge = ""
|
||||
badgeColor = "primary"
|
||||
size = "default"
|
||||
@@ -19,95 +35,695 @@ component FeatureCard {
|
||||
|
||||
view {
|
||||
<article
|
||||
{...attrs}
|
||||
data-ui-component="FeatureCard"
|
||||
aria-disabled='{disabled}'
|
||||
class='group relative flex h-full min-w-0 flex-col overflow-hidden rounded-2xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] shadow-sm transition duration-200 {class}'
|
||||
class:p-4='size === "sm"'
|
||||
class:p-6='size === "default" || size === "md"'
|
||||
class:p-8='size === "lg"'
|
||||
class:bg-[var(--wire-color-primary-soft)]='variant === "soft" && color === "primary"'
|
||||
class:bg-[var(--wire-color-success-soft)]='variant === "soft" && color === "success"'
|
||||
class:bg-[var(--wire-color-warning-soft)]='variant === "soft" && color === "warning"'
|
||||
class:bg-[var(--wire-color-danger-soft)]='variant === "soft" && color === "danger"'
|
||||
class:bg-transparent='variant === "ghost"'
|
||||
class:border-transparent='variant === "ghost"'
|
||||
class:shadow-none='variant === "ghost" || variant === "outline"'
|
||||
class:hover:-translate-y-1='hover === "lift" && !disabled'
|
||||
class:hover:shadow-xl='(hover === "lift" || hover === "shadow") && !disabled'
|
||||
class:hover:border-[var(--wire-color-primary)]='(hover === "lift" || hover === "border") && !disabled'
|
||||
class:text-center='align === "center"'
|
||||
class:items-center='align === "center"'
|
||||
class:text-right='align === "right"'
|
||||
class:items-end='align === "right"'
|
||||
class:opacity-60='disabled'
|
||||
class:pointer-events-none='disabled'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
data-hover='{hover}'
|
||||
data-align='{align}'
|
||||
data-icon-style='{iconStyle}'
|
||||
data-icon-size='{iconSize}'
|
||||
data-image-position='{imagePosition}'
|
||||
data-image-aspect='{imageAspect}'
|
||||
data-disabled='{disabled ? "true" : "false"}'
|
||||
data-stretched-link='{stretchedLink ? "true" : "false"}'
|
||||
class='wire-feature-card {class}'
|
||||
role="listitem"
|
||||
aria-disabled='{disabled ? "true" : "false"}'
|
||||
>
|
||||
<div class="flex w-full items-start justify-between gap-4">
|
||||
<div
|
||||
class="flex size-12 shrink-0 items-center justify-center rounded-2xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]"
|
||||
class:bg-[var(--wire-color-success-soft)]='color === "success"'
|
||||
class:text-[var(--wire-color-success)]='color === "success"'
|
||||
class:bg-[var(--wire-color-warning-soft)]='color === "warning"'
|
||||
class:text-[var(--wire-color-warning-text)]='color === "warning"'
|
||||
class:bg-[var(--wire-color-danger-soft)]='color === "danger"'
|
||||
class:text-[var(--wire-color-danger)]='color === "danger"'
|
||||
class:bg-[var(--wire-color-info-soft)]='color === "info"'
|
||||
class:text-[var(--wire-color-info)]='color === "info"'
|
||||
class:size-10='size === "sm"'
|
||||
class:size-14='size === "lg"'
|
||||
>
|
||||
<slot name="icon"></slot>
|
||||
{#if icon}
|
||||
<span class='{icon + " size-6"}' aria-hidden="true"></span>
|
||||
{#if image}
|
||||
<div class="wire-feature-card__media">
|
||||
<img
|
||||
src='{image}'
|
||||
alt='{imageAlt}'
|
||||
loading='{imageLoading}'
|
||||
class="wire-feature-card__image"
|
||||
/>
|
||||
<div class="wire-feature-card__media-overlay" aria-hidden="true"></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<slot name="media"></slot>
|
||||
|
||||
<div class="wire-feature-card__body">
|
||||
<div class="wire-feature-card__top">
|
||||
<div class="wire-feature-card__leading">
|
||||
<slot name="icon"></slot>
|
||||
|
||||
{#if icon}
|
||||
<div class="wire-feature-card__icon" aria-hidden="true">
|
||||
<span class='{icon}'></span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if badge}
|
||||
<span class="wire-feature-card__badge" data-color='{badgeColor}'>
|
||||
{badge}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if badge}
|
||||
<Badge
|
||||
label='{badge}'
|
||||
size="sm"
|
||||
color='{badgeColor}'
|
||||
variant="soft"
|
||||
/>
|
||||
{/if}
|
||||
<div class="wire-feature-card__copy">
|
||||
{#if eyebrow}
|
||||
<p class="wire-feature-card__eyebrow">{eyebrow}</p>
|
||||
{/if}
|
||||
|
||||
<h3 class="wire-feature-card__title">{title}</h3>
|
||||
|
||||
{#if description}
|
||||
<p class="wire-feature-card__description">{description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="wire-feature-card__content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<div class="wire-feature-card__footer">
|
||||
<div class="wire-feature-card__footer-content">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
|
||||
{#if href && actionLabel && !disabled}
|
||||
<a
|
||||
href='{href}'
|
||||
target='{target}'
|
||||
rel='{external || target === "_blank" ? "noopener noreferrer" : rel}'
|
||||
class="wire-feature-card__action"
|
||||
aria-label='{actionLabel + ": " + title}'
|
||||
>
|
||||
<span>{actionLabel}</span>
|
||||
|
||||
{#if actionIcon}
|
||||
<span class='wire-feature-card__action-icon {actionIcon}' aria-hidden="true"></span>
|
||||
{:else if external}
|
||||
<span class="wire-feature-card__action-icon icon-[lucide--external-link]" aria-hidden="true"></span>
|
||||
{:else if showArrow}
|
||||
<span class="wire-feature-card__action-icon icon-[lucide--arrow-right]" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</a>
|
||||
{:else if actionLabel}
|
||||
<span class="wire-feature-card__action wire-feature-card__action--disabled">
|
||||
<span>{actionLabel}</span>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-5 text-lg font-bold leading-tight text-[var(--wire-color-text)]" class:text-xl='size === "lg"'>
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
{#if description}
|
||||
<p class="mt-3 flex-1 text-sm leading-6 text-[var(--wire-color-text-muted)]" class:text-base='size === "lg"'>
|
||||
{description}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<slot></slot>
|
||||
|
||||
<div class="mt-6 flex w-full items-center justify-between gap-3">
|
||||
<slot name="footer"></slot>
|
||||
|
||||
{#if href && actionLabel}
|
||||
<TextLink
|
||||
label='{actionLabel}'
|
||||
href='{href}'
|
||||
icon='{actionIcon}'
|
||||
iconPosition="start"
|
||||
showArrow="true"
|
||||
size="sm"
|
||||
color='{color}'
|
||||
class="ml-auto"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
href='{href}'
|
||||
aria-label='{title}'
|
||||
class="absolute inset-0 z-10 rounded-2xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
|
||||
></a>
|
||||
{/if}
|
||||
</article>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-feature-card {
|
||||
--feature-accent: var(--wire-color-primary);
|
||||
--feature-accent-hover: var(--wire-color-primary-hover);
|
||||
--feature-accent-soft: var(--wire-color-primary-soft);
|
||||
--feature-accent-muted: var(--wire-color-primary-muted);
|
||||
--feature-contrast: var(--wire-color-primary-contrast);
|
||||
--feature-border: color-mix(in srgb, var(--feature-accent) 16%, var(--wire-color-border));
|
||||
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
color: var(--wire-color-text);
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 100% 0%,
|
||||
color-mix(in srgb, var(--feature-accent) 7%, transparent),
|
||||
transparent 34%
|
||||
),
|
||||
var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--feature-border);
|
||||
border-radius: 1.25rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 4%, transparent) inset,
|
||||
0 10px 30px color-mix(in srgb, black 9%, transparent);
|
||||
transition:
|
||||
transform 180ms ease,
|
||||
border-color 180ms ease,
|
||||
box-shadow 180ms ease,
|
||||
background-color 180ms ease;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-color="secondary"] {
|
||||
--feature-accent: var(--wire-color-secondary);
|
||||
--feature-accent-hover: var(--wire-color-secondary-hover);
|
||||
--feature-accent-soft: var(--wire-color-secondary-soft);
|
||||
--feature-accent-muted: var(--wire-color-secondary-muted);
|
||||
--feature-contrast: var(--wire-color-secondary-contrast);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-color="info"] {
|
||||
--feature-accent: var(--wire-color-info);
|
||||
--feature-accent-soft: var(--wire-color-info-soft);
|
||||
--feature-accent-muted: var(--wire-color-info-muted);
|
||||
--feature-contrast: var(--wire-color-info-contrast);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-color="success"] {
|
||||
--feature-accent: var(--wire-color-success);
|
||||
--feature-accent-soft: var(--wire-color-success-soft);
|
||||
--feature-accent-muted: var(--wire-color-success-muted);
|
||||
--feature-contrast: var(--wire-color-success-contrast);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-color="warning"] {
|
||||
--feature-accent: var(--wire-color-warning);
|
||||
--feature-accent-soft: var(--wire-color-warning-soft);
|
||||
--feature-accent-muted: var(--wire-color-warning-muted);
|
||||
--feature-contrast: var(--wire-color-warning-contrast);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-color="danger"] {
|
||||
--feature-accent: var(--wire-color-danger);
|
||||
--feature-accent-soft: var(--wire-color-danger-soft);
|
||||
--feature-accent-muted: var(--wire-color-danger-muted);
|
||||
--feature-contrast: var(--wire-color-danger-contrast);
|
||||
}
|
||||
|
||||
.wire-feature-card::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
height: 3px;
|
||||
content: "";
|
||||
background: linear-gradient(90deg, var(--feature-accent), transparent 72%);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.wire-feature-card__media {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--wire-color-surface-soft);
|
||||
border-bottom: 1px solid var(--feature-border);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-image-aspect="square"] .wire-feature-card__media {
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-image-aspect="portrait"] .wire-feature-card__media {
|
||||
aspect-ratio: 4 / 5;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-image-aspect="standard"] .wire-feature-card__media {
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-image-aspect="wide"] .wire-feature-card__media {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-image-aspect="cinema"] .wire-feature-card__media {
|
||||
aspect-ratio: 21 / 9;
|
||||
}
|
||||
|
||||
.wire-feature-card__image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 300ms ease;
|
||||
}
|
||||
|
||||
.wire-feature-card__media-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(180deg, transparent 58%, color-mix(in srgb, black 18%, transparent));
|
||||
}
|
||||
|
||||
.wire-feature-card__body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-size="sm"] .wire-feature-card__body {
|
||||
padding: 1.1rem;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-size="lg"] .wire-feature-card__body {
|
||||
padding: 1.85rem;
|
||||
}
|
||||
|
||||
.wire-feature-card__top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-feature-card__leading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-feature-card__icon {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
color: var(--feature-accent);
|
||||
background: var(--feature-accent-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--feature-accent) 18%, transparent);
|
||||
border-radius: 0.95rem;
|
||||
box-shadow: 0 1px 0 color-mix(in srgb, white 7%, transparent) inset;
|
||||
}
|
||||
|
||||
.wire-feature-card__icon > span {
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-icon-size="sm"] .wire-feature-card__icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-icon-size="sm"] .wire-feature-card__icon > span {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-icon-size="lg"] .wire-feature-card__icon {
|
||||
width: 3.6rem;
|
||||
height: 3.6rem;
|
||||
border-radius: 1.05rem;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-icon-size="lg"] .wire-feature-card__icon > span {
|
||||
width: 1.65rem;
|
||||
height: 1.65rem;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-icon-style="solid"] .wire-feature-card__icon {
|
||||
color: var(--feature-contrast);
|
||||
background: var(--feature-accent);
|
||||
border-color: color-mix(in srgb, white 18%, transparent);
|
||||
box-shadow: 0 10px 24px color-mix(in srgb, var(--feature-accent) 24%, transparent);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-icon-style="outline"] .wire-feature-card__icon {
|
||||
color: var(--feature-accent);
|
||||
background: transparent;
|
||||
border-color: color-mix(in srgb, var(--feature-accent) 38%, var(--wire-color-border));
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-icon-style="ghost"] .wire-feature-card__icon {
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: 0.25rem;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-feature-card__badge {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
min-height: 1.65rem;
|
||||
padding: 0.25rem 0.65rem;
|
||||
color: var(--feature-accent);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
background: var(--feature-accent-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--feature-accent) 18%, transparent);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.wire-feature-card__badge[data-color="secondary"] {
|
||||
color: var(--wire-color-secondary);
|
||||
background: var(--wire-color-secondary-soft);
|
||||
border-color: color-mix(in srgb, var(--wire-color-secondary) 20%, transparent);
|
||||
}
|
||||
|
||||
.wire-feature-card__badge[data-color="info"] {
|
||||
color: var(--wire-color-info);
|
||||
background: var(--wire-color-info-soft);
|
||||
border-color: color-mix(in srgb, var(--wire-color-info) 20%, transparent);
|
||||
}
|
||||
|
||||
.wire-feature-card__badge[data-color="success"] {
|
||||
color: var(--wire-color-success);
|
||||
background: var(--wire-color-success-soft);
|
||||
border-color: color-mix(in srgb, var(--wire-color-success) 20%, transparent);
|
||||
}
|
||||
|
||||
.wire-feature-card__badge[data-color="warning"] {
|
||||
color: var(--wire-color-warning-text);
|
||||
background: var(--wire-color-warning-soft);
|
||||
border-color: color-mix(in srgb, var(--wire-color-warning) 22%, transparent);
|
||||
}
|
||||
|
||||
.wire-feature-card__badge[data-color="danger"] {
|
||||
color: var(--wire-color-danger);
|
||||
background: var(--wire-color-danger-soft);
|
||||
border-color: color-mix(in srgb, var(--wire-color-danger) 20%, transparent);
|
||||
}
|
||||
|
||||
.wire-feature-card__copy {
|
||||
min-width: 0;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.wire-feature-card__eyebrow {
|
||||
margin: 0 0 0.5rem;
|
||||
color: var(--feature-accent);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wire-feature-card__title {
|
||||
margin: 0;
|
||||
color: var(--wire-color-text);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
letter-spacing: -0.015em;
|
||||
text-wrap: balance;
|
||||
transition: color 180ms ease;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-size="sm"] .wire-feature-card__title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-size="lg"] .wire-feature-card__title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.wire-feature-card__description {
|
||||
margin: 0.75rem 0 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.65;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-size="sm"] .wire-feature-card__description {
|
||||
margin-top: 0.6rem;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-size="lg"] .wire-feature-card__description {
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.wire-feature-card__content {
|
||||
min-width: 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.wire-feature-card__content:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-feature-card__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
min-width: 0;
|
||||
margin-top: auto;
|
||||
padding-top: 1.35rem;
|
||||
}
|
||||
|
||||
.wire-feature-card__footer-content {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-feature-card__footer-content:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-feature-card__action {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.45rem;
|
||||
min-height: 2.25rem;
|
||||
margin-left: auto;
|
||||
color: var(--feature-accent);
|
||||
font-size: 0.84rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
color 180ms ease,
|
||||
gap 180ms ease;
|
||||
}
|
||||
|
||||
.wire-feature-card__action::before {
|
||||
position: absolute;
|
||||
inset: -0.4rem -0.55rem;
|
||||
content: "";
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-stretched-link="true"] .wire-feature-card__action::after {
|
||||
position: absolute;
|
||||
inset: auto;
|
||||
z-index: 1;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.wire-feature-card[data-stretched-link="true"] .wire-feature-card__action {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-stretched-link="true"] .wire-feature-card__action::after {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.wire-feature-card__action:hover {
|
||||
color: var(--feature-accent-hover, var(--feature-accent));
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.wire-feature-card__action:focus-visible {
|
||||
outline: 2px solid var(--wire-color-focus);
|
||||
outline-offset: 4px;
|
||||
border-radius: 0.55rem;
|
||||
}
|
||||
|
||||
.wire-feature-card__action-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
|
||||
.wire-feature-card__action:hover .wire-feature-card__action-icon {
|
||||
transform: translateX(0.15rem);
|
||||
}
|
||||
|
||||
.wire-feature-card__action--disabled {
|
||||
color: var(--wire-color-text-muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-align="center"] .wire-feature-card__body {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-align="center"] .wire-feature-card__top,
|
||||
.wire-feature-card[data-align="center"] .wire-feature-card__footer {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-align="center"] .wire-feature-card__action {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-align="right"] .wire-feature-card__body {
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-align="right"] .wire-feature-card__top,
|
||||
.wire-feature-card[data-align="right"] .wire-feature-card__footer {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-variant="raised"] {
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 20px 50px color-mix(in srgb, black 17%, transparent);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-variant="soft"] {
|
||||
background:
|
||||
linear-gradient(135deg, var(--feature-accent-soft), transparent 68%),
|
||||
var(--wire-color-surface-raised);
|
||||
border-color: color-mix(in srgb, var(--feature-accent) 22%, var(--wire-color-border));
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-variant="outline"] {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-variant="ghost"] {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-variant="minimal"] {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-variant="solid"] {
|
||||
color: var(--feature-contrast);
|
||||
background:
|
||||
linear-gradient(135deg, color-mix(in srgb, white 9%, transparent), transparent 60%),
|
||||
var(--feature-accent);
|
||||
border-color: color-mix(in srgb, white 20%, transparent);
|
||||
box-shadow: 0 22px 54px color-mix(in srgb, var(--feature-accent) 28%, transparent);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-variant="solid"] .wire-feature-card__title,
|
||||
.wire-feature-card[data-variant="solid"] .wire-feature-card__description,
|
||||
.wire-feature-card[data-variant="solid"] .wire-feature-card__eyebrow,
|
||||
.wire-feature-card[data-variant="solid"] .wire-feature-card__action {
|
||||
color: var(--feature-contrast);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-variant="solid"] .wire-feature-card__description {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-variant="solid"] .wire-feature-card__icon,
|
||||
.wire-feature-card[data-variant="solid"] .wire-feature-card__badge {
|
||||
color: var(--feature-contrast);
|
||||
background: color-mix(in srgb, white 12%, transparent);
|
||||
border-color: color-mix(in srgb, white 20%, transparent);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-variant="gradient"] {
|
||||
background:
|
||||
radial-gradient(circle at 100% 0%, color-mix(in srgb, var(--feature-accent) 24%, transparent), transparent 44%),
|
||||
linear-gradient(145deg, var(--wire-color-surface-raised), color-mix(in srgb, var(--feature-accent) 8%, var(--wire-color-surface-raised)));
|
||||
border-color: color-mix(in srgb, var(--feature-accent) 26%, var(--wire-color-border));
|
||||
}
|
||||
|
||||
.wire-feature-card[data-hover="lift"]:not([data-disabled="true"]):hover {
|
||||
transform: translateY(-0.3rem);
|
||||
border-color: color-mix(in srgb, var(--feature-accent) 48%, var(--wire-color-border));
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 24px 58px color-mix(in srgb, black 20%, transparent);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-hover="shadow"]:not([data-disabled="true"]):hover {
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 24px 58px color-mix(in srgb, black 20%, transparent);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-hover="border"]:not([data-disabled="true"]):hover {
|
||||
border-color: color-mix(in srgb, var(--feature-accent) 58%, var(--wire-color-border));
|
||||
}
|
||||
|
||||
.wire-feature-card[data-hover="glow"]:not([data-disabled="true"]):hover {
|
||||
border-color: color-mix(in srgb, var(--feature-accent) 54%, var(--wire-color-border));
|
||||
box-shadow: 0 20px 60px color-mix(in srgb, var(--feature-accent) 22%, transparent);
|
||||
}
|
||||
|
||||
.wire-feature-card:not([data-disabled="true"]):hover .wire-feature-card__image {
|
||||
transform: scale(1.035);
|
||||
}
|
||||
|
||||
.wire-feature-card:not([data-disabled="true"]):hover .wire-feature-card__title {
|
||||
color: var(--feature-accent);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-variant="solid"]:not([data-disabled="true"]):hover .wire-feature-card__title {
|
||||
color: var(--feature-contrast);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-disabled="true"] {
|
||||
pointer-events: none;
|
||||
opacity: 0.55;
|
||||
filter: saturate(0.65);
|
||||
}
|
||||
|
||||
@media (min-width: 48rem) {
|
||||
.wire-feature-card[data-image-position="left"],
|
||||
.wire-feature-card[data-image-position="right"] {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.88fr) minmax(0, 1.12fr);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-image-position="right"] {
|
||||
grid-template-columns: minmax(0, 1.12fr) minmax(0, 0.88fr);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-image-position="right"] .wire-feature-card__media {
|
||||
order: 2;
|
||||
border-right: 0;
|
||||
border-bottom: 0;
|
||||
border-left: 1px solid var(--feature-border);
|
||||
}
|
||||
|
||||
.wire-feature-card[data-image-position="left"] .wire-feature-card__media {
|
||||
border-right: 1px solid var(--feature-border);
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.wire-feature-card[data-image-position="left"] .wire-feature-card__media,
|
||||
.wire-feature-card[data-image-position="right"] .wire-feature-card__media {
|
||||
height: 100%;
|
||||
min-height: 15rem;
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-feature-card,
|
||||
.wire-feature-card__image,
|
||||
.wire-feature-card__action,
|
||||
.wire-feature-card__action-icon {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
component FeatureGrid {
|
||||
props {
|
||||
color = "primary"
|
||||
size = "default"
|
||||
items = []
|
||||
columns = 3
|
||||
tabletColumns = 2
|
||||
mobileColumns = 1
|
||||
@@ -10,46 +9,220 @@ component FeatureGrid {
|
||||
equalHeight = true
|
||||
align = "stretch"
|
||||
maxWidth = "full"
|
||||
variant = "default"
|
||||
label = "Features"
|
||||
class = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<div
|
||||
<section
|
||||
{...attrs}
|
||||
data-ui-component="FeatureGrid"
|
||||
data-columns='{columns}'
|
||||
data-tablet-columns='{tabletColumns}'
|
||||
data-mobile-columns='{mobileColumns}'
|
||||
class='grid w-full {class}'
|
||||
class:wire-feature-grid-autofit='minItemWidth !== ""'
|
||||
class:grid-cols-1='mobileColumns === 1'
|
||||
class:grid-cols-2='mobileColumns === 2'
|
||||
class:sm:grid-cols-1='tabletColumns === 1'
|
||||
class:sm:grid-cols-2='tabletColumns === 2'
|
||||
class:sm:grid-cols-3='tabletColumns === 3'
|
||||
class:lg:grid-cols-2='columns === 2'
|
||||
class:lg:grid-cols-3='columns === 3'
|
||||
class:lg:grid-cols-4='columns === 4'
|
||||
class:lg:grid-cols-5='columns === 5'
|
||||
class:lg:grid-cols-6='columns === 6'
|
||||
class:gap-3='gap === "sm"'
|
||||
class:gap-5='gap === "md"'
|
||||
class:gap-8='gap === "lg"'
|
||||
class:gap-10='gap === "xl"'
|
||||
class:items-stretch='equalHeight || align === "stretch"'
|
||||
class:items-start='align === "start"'
|
||||
class:items-center='align === "center"'
|
||||
class:max-w-5xl='maxWidth === "lg"'
|
||||
class:max-w-7xl='maxWidth === "xl"'
|
||||
class:max-w-none='maxWidth === "full"'
|
||||
style='--wire-feature-grid-min: {minItemWidth || "16rem"};'
|
||||
data-gap='{gap}'
|
||||
data-autofit='{minItemWidth !== "" ? "true" : "false"}'
|
||||
data-equal-height='{equalHeight ? "true" : "false"}'
|
||||
data-align='{align}'
|
||||
data-max-width='{maxWidth}'
|
||||
data-variant='{variant}'
|
||||
class='wire-feature-grid {class}'
|
||||
aria-label='{label}'
|
||||
style='--wire-feature-grid-columns: {columns}; --wire-feature-grid-tablet-columns: {tabletColumns}; --wire-feature-grid-mobile-columns: {mobileColumns}; --wire-feature-grid-min-item-width: {minItemWidth || "17rem"};'
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="wire-feature-grid__inner">
|
||||
<div class="wire-feature-grid__grid" role="list">
|
||||
{#if items.length > 0}
|
||||
{#each items as item, itemIndex}
|
||||
<FeatureCard
|
||||
icon='{item.icon || ""}'
|
||||
iconStyle='{item.iconStyle || "soft"}'
|
||||
iconSize='{item.iconSize || "default"}'
|
||||
eyebrow='{item.eyebrow || ""}'
|
||||
title='{item.title || item.label || "Feature"}'
|
||||
description='{item.description || ""}'
|
||||
image='{item.image || item.imageSrc || ""}'
|
||||
imageAlt='{item.imageAlt || item.title || item.label || ""}'
|
||||
imagePosition='{item.imagePosition || "top"}'
|
||||
imageAspect='{item.imageAspect || "wide"}'
|
||||
href='{item.href || ""}'
|
||||
target='{item.target || ""}'
|
||||
rel='{item.rel || ""}'
|
||||
external='{item.external || false}'
|
||||
actionLabel='{item.actionLabel || "Learn more"}'
|
||||
actionIcon='{item.actionIcon || ""}'
|
||||
badge='{item.badge || ""}'
|
||||
badgeColor='{item.badgeColor || item.color || "primary"}'
|
||||
size='{item.size || "default"}'
|
||||
color='{item.color || "primary"}'
|
||||
variant='{item.variant || "default"}'
|
||||
hover='{item.hover || "lift"}'
|
||||
align='{item.align || "left"}'
|
||||
disabled='{item.disabled || false}'
|
||||
stretchedLink='{item.stretchedLink !== false}'
|
||||
showArrow='{item.showArrow !== false}'
|
||||
class='{item.class || ""}'
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-feature-grid-autofit {
|
||||
grid-template-columns: repeat(auto-fit, minmax(var(--wire-feature-grid-min), 1fr));
|
||||
.wire-feature-grid {
|
||||
--feature-grid-gap: 1.25rem;
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
color: var(--wire-color-text);
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-gap="none"] {
|
||||
--feature-grid-gap: 0;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-gap="xs"] {
|
||||
--feature-grid-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-gap="sm"] {
|
||||
--feature-grid-gap: 0.75rem;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-gap="md"] {
|
||||
--feature-grid-gap: 1.25rem;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-gap="lg"] {
|
||||
--feature-grid-gap: 1.75rem;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-gap="xl"] {
|
||||
--feature-grid-gap: 2.25rem;
|
||||
}
|
||||
|
||||
.wire-feature-grid__inner {
|
||||
width: 100%;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-max-width="compact"] .wire-feature-grid__inner {
|
||||
max-width: 64rem;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-max-width="lg"] .wire-feature-grid__inner {
|
||||
max-width: 72rem;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-max-width="xl"] .wire-feature-grid__inner {
|
||||
max-width: 80rem;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-max-width="wide"] .wire-feature-grid__inner {
|
||||
max-width: 90rem;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-max-width="2xl"] .wire-feature-grid__inner {
|
||||
max-width: 96rem;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-max-width="full"] .wire-feature-grid__inner {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.wire-feature-grid__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
var(--wire-feature-grid-mobile-columns),
|
||||
minmax(0, 1fr)
|
||||
);
|
||||
align-items: stretch;
|
||||
gap: var(--feature-grid-gap);
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-align="start"] .wire-feature-grid__grid {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-align="center"] .wire-feature-grid__grid {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-align="end"] .wire-feature-grid__grid {
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.wire-feature-grid__grid > * {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-equal-height="true"] .wire-feature-grid__grid > * {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-variant="panel"] .wire-feature-grid__inner {
|
||||
padding: 1rem;
|
||||
background: var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-variant="soft"] .wire-feature-grid__inner {
|
||||
padding: 1rem;
|
||||
background: color-mix(in srgb, var(--wire-color-primary) 5%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--wire-color-primary) 14%, var(--wire-color-border));
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 40rem) {
|
||||
.wire-feature-grid__grid {
|
||||
grid-template-columns: repeat(
|
||||
var(--wire-feature-grid-tablet-columns),
|
||||
minmax(0, 1fr)
|
||||
);
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-autofit="true"] .wire-feature-grid__grid {
|
||||
grid-template-columns: repeat(
|
||||
auto-fit,
|
||||
minmax(min(100%, var(--wire-feature-grid-min-item-width)), 1fr)
|
||||
);
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-variant="panel"] .wire-feature-grid__inner,
|
||||
.wire-feature-grid[data-variant="soft"] .wire-feature-grid__inner {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 64rem) {
|
||||
.wire-feature-grid__grid {
|
||||
grid-template-columns: repeat(
|
||||
var(--wire-feature-grid-columns),
|
||||
minmax(0, 1fr)
|
||||
);
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-autofit="true"] .wire-feature-grid__grid {
|
||||
grid-template-columns: repeat(
|
||||
auto-fit,
|
||||
minmax(min(100%, var(--wire-feature-grid-min-item-width)), 1fr)
|
||||
);
|
||||
}
|
||||
|
||||
.wire-feature-grid[data-variant="panel"] .wire-feature-grid__inner,
|
||||
.wire-feature-grid[data-variant="soft"] .wire-feature-grid__inner {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+989
-110
File diff suppressed because it is too large
Load Diff
@@ -1,107 +1,669 @@
|
||||
component MetricCard {
|
||||
props {
|
||||
@event select = function
|
||||
@event action = function
|
||||
|
||||
label = "Metric"
|
||||
value = "0"
|
||||
description = ""
|
||||
icon = ""
|
||||
iconStyle = "soft"
|
||||
prefix = ""
|
||||
suffix = ""
|
||||
badge = ""
|
||||
trend = ""
|
||||
trendLabel = ""
|
||||
trendDirection = "neutral"
|
||||
progress = -1
|
||||
progressLabel = ""
|
||||
href = ""
|
||||
target = ""
|
||||
rel = ""
|
||||
external = false
|
||||
actionLabel = ""
|
||||
actionIcon = ""
|
||||
showArrow = true
|
||||
selectable = false
|
||||
disabled = false
|
||||
size = "default"
|
||||
color = "primary"
|
||||
variant = "default"
|
||||
hover = "lift"
|
||||
align = "left"
|
||||
class = ""
|
||||
}
|
||||
|
||||
functions {
|
||||
function selectCard(sourceEvent) {
|
||||
if (disabled || !selectable) {
|
||||
return
|
||||
}
|
||||
if (sourceEvent.target.closest(".wire-metric-card__action")) {
|
||||
return
|
||||
}
|
||||
$emit("select", {
|
||||
label: label,
|
||||
value: value,
|
||||
href: href,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
|
||||
function activateAction(sourceEvent) {
|
||||
if (disabled) {
|
||||
sourceEvent.preventDefault()
|
||||
return
|
||||
}
|
||||
$emit("action", {
|
||||
label: label,
|
||||
value: value,
|
||||
href: href,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<article
|
||||
{...attrs}
|
||||
data-ui-component="MetricCard"
|
||||
class='group relative flex h-full flex-col rounded-2xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] p-5 shadow-sm transition {class}'
|
||||
class:p-4='size === "sm"'
|
||||
class:p-6='size === "lg"'
|
||||
class:bg-[var(--wire-color-primary-soft)]='variant === "soft" && color === "primary"'
|
||||
class:bg-[var(--wire-color-success-soft)]='variant === "soft" && color === "success"'
|
||||
class:bg-[var(--wire-color-warning-soft)]='variant === "soft" && color === "warning"'
|
||||
class:bg-[var(--wire-color-danger-soft)]='variant === "soft" && color === "danger"'
|
||||
class:bg-transparent='variant === "minimal"'
|
||||
class:border-transparent='variant === "minimal"'
|
||||
class:shadow-none='variant === "minimal"'
|
||||
class:text-center='align === "center"'
|
||||
class:items-center='align === "center"'
|
||||
class:text-right='align === "right"'
|
||||
class:items-end='align === "right"'
|
||||
class:hover:border-[var(--wire-color-primary)]='href !== ""'
|
||||
class:hover:shadow-lg='href !== ""'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
data-hover='{hover}'
|
||||
data-align='{align}'
|
||||
data-icon-style='{iconStyle}'
|
||||
data-disabled='{disabled ? "true" : "false"}'
|
||||
data-selectable='{selectable ? "true" : "false"}'
|
||||
class='wire-metric-card {class}'
|
||||
role='{selectable ? "button" : ""}'
|
||||
tabindex='{selectable && !disabled ? "0" : "-1"}'
|
||||
@click='selectCard(event)'
|
||||
@keydown='if (selectable && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); selectCard(event) }'
|
||||
style='--wire-metric-progress: {progress}%;'
|
||||
>
|
||||
<div class="flex w-full items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-[var(--wire-color-text-muted)]">{label}</p>
|
||||
<p class="mt-2 text-3xl font-bold tracking-tight text-[var(--wire-color-text)]" class:text-4xl='size === "lg"'>
|
||||
<span>{prefix}</span><span>{value}</span><span>{suffix}</span>
|
||||
</p>
|
||||
<div class="wire-metric-card__top">
|
||||
<div class="wire-metric-card__heading">
|
||||
{#if label}
|
||||
<p class="wire-metric-card__label">{label}</p>
|
||||
{/if}
|
||||
|
||||
{#if badge}
|
||||
<span class="wire-metric-card__badge">{badge}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if icon}
|
||||
<span
|
||||
class="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]"
|
||||
class:bg-[var(--wire-color-success-soft)]='color === "success"'
|
||||
class:text-[var(--wire-color-success)]='color === "success"'
|
||||
class:bg-[var(--wire-color-warning-soft)]='color === "warning"'
|
||||
class:text-[var(--wire-color-warning-text)]='color === "warning"'
|
||||
class:bg-[var(--wire-color-danger-soft)]='color === "danger"'
|
||||
class:text-[var(--wire-color-danger)]='color === "danger"'
|
||||
class:bg-[var(--wire-color-info-soft)]='color === "info"'
|
||||
class:text-[var(--wire-color-info)]='color === "info"'
|
||||
>
|
||||
<span class='{icon + " size-5"}' aria-hidden="true"></span>
|
||||
</span>
|
||||
<div class="wire-metric-card__icon" aria-hidden="true">
|
||||
<span class='{icon}'></span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="wire-metric-card__value-row">
|
||||
{#if prefix}
|
||||
<span class="wire-metric-card__prefix">{prefix}</span>
|
||||
{/if}
|
||||
|
||||
<strong class="wire-metric-card__value">{value}</strong>
|
||||
|
||||
{#if suffix}
|
||||
<span class="wire-metric-card__suffix">{suffix}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if description}
|
||||
<p class="mt-3 text-sm leading-6 text-[var(--wire-color-text-muted)]">{description}</p>
|
||||
<p class="wire-metric-card__description">{description}</p>
|
||||
{/if}
|
||||
|
||||
<slot></slot>
|
||||
|
||||
{#if progress >= 0}
|
||||
<div class="wire-metric-card__progress-block">
|
||||
<div class="wire-metric-card__progress-meta">
|
||||
<span>{progressLabel || "Progress"}</span>
|
||||
<strong>{progress}%</strong>
|
||||
</div>
|
||||
<div
|
||||
class="wire-metric-card__progress-track"
|
||||
role="progressbar"
|
||||
aria-label='{progressLabel || label}'
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow='{progress}'
|
||||
>
|
||||
<span class="wire-metric-card__progress-value"></span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if trend || trendLabel}
|
||||
<div class="mt-4 inline-flex items-center gap-2 text-sm font-semibold">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-full px-2.5 py-1"
|
||||
class:bg-[var(--wire-color-success-soft)]='trendDirection === "up" || trendDirection === "positive"'
|
||||
class:text-[var(--wire-color-success)]='trendDirection === "up" || trendDirection === "positive"'
|
||||
class:bg-[var(--wire-color-danger-soft)]='trendDirection === "down" || trendDirection === "negative"'
|
||||
class:text-[var(--wire-color-danger)]='trendDirection === "down" || trendDirection === "negative"'
|
||||
class:bg-[var(--wire-color-surface-soft)]='trendDirection === "neutral"'
|
||||
class:text-[var(--wire-color-text-muted)]='trendDirection === "neutral"'
|
||||
>
|
||||
{#if trendDirection === "up" || trendDirection === "positive"}
|
||||
<span class="icon-[lucide--trending-up] size-4" aria-hidden="true"></span>
|
||||
{:else if trendDirection === "down" || trendDirection === "negative"}
|
||||
<span class="icon-[lucide--trending-down] size-4" aria-hidden="true"></span>
|
||||
{:else}
|
||||
<span class="icon-[lucide--minus] size-4" aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{trend}</span>
|
||||
</span>
|
||||
<div
|
||||
class="wire-metric-card__trend"
|
||||
data-direction='{trendDirection}'
|
||||
>
|
||||
{#if trendDirection === "up" || trendDirection === "positive"}
|
||||
<span class="icon-[lucide--trending-up] wire-metric-card__trend-icon" aria-hidden="true"></span>
|
||||
{:else if trendDirection === "down" || trendDirection === "negative"}
|
||||
<span class="icon-[lucide--trending-down] wire-metric-card__trend-icon" aria-hidden="true"></span>
|
||||
{:else}
|
||||
<span class="icon-[lucide--minus] wire-metric-card__trend-icon" aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
{#if trend}
|
||||
<strong class="wire-metric-card__trend-value">{trend}</strong>
|
||||
{/if}
|
||||
|
||||
{#if trendLabel}
|
||||
<span class="text-[var(--wire-color-text-muted)]">{trendLabel}</span>
|
||||
<span class="wire-metric-card__trend-label">{trendLabel}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if href}
|
||||
<TextLink
|
||||
label='{actionLabel || "View details"}'
|
||||
href='{href}'
|
||||
color='{color}'
|
||||
size="sm"
|
||||
class="mt-5"
|
||||
/>
|
||||
{/if}
|
||||
<div class="wire-metric-card__footer">
|
||||
<slot name="footer"></slot>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
href='{href}'
|
||||
target='{target}'
|
||||
rel='{external || target === "_blank" ? "noopener noreferrer" : rel}'
|
||||
class="wire-metric-card__action"
|
||||
aria-disabled='{disabled ? "true" : "false"}'
|
||||
@click='activateAction(event)'
|
||||
>
|
||||
<span>{actionLabel || "View details"}</span>
|
||||
|
||||
{#if actionIcon}
|
||||
<span class='wire-metric-card__action-icon {actionIcon}' aria-hidden="true"></span>
|
||||
{:else if external}
|
||||
<span class="wire-metric-card__action-icon icon-[lucide--external-link]" aria-hidden="true"></span>
|
||||
{:else if showArrow}
|
||||
<span class="wire-metric-card__action-icon icon-[lucide--arrow-right]" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</a>
|
||||
{:else if actionLabel}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-metric-card__action"
|
||||
disabled='{disabled}'
|
||||
@click='activateAction(event)'
|
||||
>
|
||||
<span>{actionLabel}</span>
|
||||
{#if actionIcon}
|
||||
<span class='wire-metric-card__action-icon {actionIcon}' aria-hidden="true"></span>
|
||||
{:else if showArrow}
|
||||
<span class="wire-metric-card__action-icon icon-[lucide--arrow-right]" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-metric-card {
|
||||
--metric-accent: var(--wire-color-primary);
|
||||
--metric-accent-hover: var(--wire-color-primary-hover);
|
||||
--metric-accent-soft: var(--wire-color-primary-soft);
|
||||
--metric-accent-muted: var(--wire-color-primary-muted);
|
||||
--metric-contrast: var(--wire-color-primary-contrast);
|
||||
--metric-border: color-mix(in srgb, var(--metric-accent) 16%, var(--wire-color-border));
|
||||
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding: 1.5rem;
|
||||
color: var(--wire-color-text);
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 100% 0%,
|
||||
color-mix(in srgb, var(--metric-accent) 8%, transparent),
|
||||
transparent 36%
|
||||
),
|
||||
var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--metric-border);
|
||||
border-radius: 1.25rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 4%, transparent) inset,
|
||||
0 14px 36px color-mix(in srgb, black 10%, transparent);
|
||||
overflow: hidden;
|
||||
transition:
|
||||
transform 180ms ease,
|
||||
border-color 180ms ease,
|
||||
box-shadow 180ms ease,
|
||||
background-color 180ms ease;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-color="secondary"] {
|
||||
--metric-accent: var(--wire-color-secondary);
|
||||
--metric-accent-hover: var(--wire-color-secondary-hover);
|
||||
--metric-accent-soft: var(--wire-color-secondary-soft);
|
||||
--metric-accent-muted: var(--wire-color-secondary-muted);
|
||||
--metric-contrast: var(--wire-color-secondary-contrast);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-color="info"] {
|
||||
--metric-accent: var(--wire-color-info);
|
||||
--metric-accent-hover: var(--wire-color-info-hover);
|
||||
--metric-accent-soft: var(--wire-color-info-soft);
|
||||
--metric-accent-muted: var(--wire-color-info-muted);
|
||||
--metric-contrast: var(--wire-color-info-contrast);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-color="success"] {
|
||||
--metric-accent: var(--wire-color-success);
|
||||
--metric-accent-hover: var(--wire-color-success-hover);
|
||||
--metric-accent-soft: var(--wire-color-success-soft);
|
||||
--metric-accent-muted: var(--wire-color-success-muted);
|
||||
--metric-contrast: var(--wire-color-success-contrast);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-color="warning"] {
|
||||
--metric-accent: var(--wire-color-warning);
|
||||
--metric-accent-hover: var(--wire-color-warning-hover);
|
||||
--metric-accent-soft: var(--wire-color-warning-soft);
|
||||
--metric-accent-muted: var(--wire-color-warning-muted);
|
||||
--metric-contrast: var(--wire-color-warning-contrast);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-color="danger"] {
|
||||
--metric-accent: var(--wire-color-danger);
|
||||
--metric-accent-hover: var(--wire-color-danger-hover);
|
||||
--metric-accent-soft: var(--wire-color-danger-soft);
|
||||
--metric-accent-muted: var(--wire-color-danger-muted);
|
||||
--metric-contrast: var(--wire-color-danger-contrast);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-size="sm"] {
|
||||
padding: 1.1rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-size="lg"] {
|
||||
padding: 1.9rem;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-variant="soft"] {
|
||||
background:
|
||||
linear-gradient(135deg, var(--metric-accent-soft), transparent 68%),
|
||||
var(--wire-color-surface-raised);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-variant="raised"] {
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 24px 64px color-mix(in srgb, black 16%, transparent);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-variant="outline"] {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-variant="minimal"] {
|
||||
padding-inline: 0;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-variant="solid"] {
|
||||
color: var(--metric-contrast);
|
||||
background:
|
||||
linear-gradient(135deg, color-mix(in srgb, white 10%, transparent), transparent 58%),
|
||||
var(--metric-accent);
|
||||
border-color: color-mix(in srgb, white 22%, transparent);
|
||||
box-shadow: 0 20px 52px color-mix(in srgb, var(--metric-accent) 28%, transparent);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-variant="gradient"] {
|
||||
background:
|
||||
linear-gradient(
|
||||
145deg,
|
||||
color-mix(in srgb, var(--metric-accent) 20%, var(--wire-color-surface-raised)),
|
||||
var(--wire-color-surface-raised) 58%
|
||||
);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.58;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-selectable="true"]:not([data-disabled="true"]) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-hover="lift"]:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: color-mix(in srgb, var(--metric-accent) 42%, var(--wire-color-border));
|
||||
box-shadow: 0 24px 56px color-mix(in srgb, black 16%, transparent);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-hover="border"]:hover {
|
||||
border-color: var(--metric-accent);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-hover="glow"]:hover {
|
||||
border-color: color-mix(in srgb, var(--metric-accent) 52%, var(--wire-color-border));
|
||||
box-shadow: 0 22px 58px color-mix(in srgb, var(--metric-accent) 18%, transparent);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-align="center"] {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-align="right"] {
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.wire-metric-card__top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wire-metric-card__heading {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-metric-card__label {
|
||||
margin: 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.84rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.wire-metric-card__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 1.45rem;
|
||||
padding: 0.2rem 0.55rem;
|
||||
color: var(--metric-accent);
|
||||
background: var(--metric-accent-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--metric-accent) 24%, transparent);
|
||||
border-radius: 999px;
|
||||
font-size: 0.69rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-variant="solid"] .wire-metric-card__label,
|
||||
.wire-metric-card[data-variant="solid"] .wire-metric-card__description,
|
||||
.wire-metric-card[data-variant="solid"] .wire-metric-card__trend-label,
|
||||
.wire-metric-card[data-variant="solid"] .wire-metric-card__progress-meta {
|
||||
color: color-mix(in srgb, currentColor 78%, transparent);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-variant="solid"] .wire-metric-card__badge {
|
||||
color: currentColor;
|
||||
background: color-mix(in srgb, white 14%, transparent);
|
||||
border-color: color-mix(in srgb, white 22%, transparent);
|
||||
}
|
||||
|
||||
.wire-metric-card__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
color: var(--metric-accent);
|
||||
background: var(--metric-accent-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--metric-accent) 18%, transparent);
|
||||
border-radius: 0.9rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-icon-style="ghost"] .wire-metric-card__icon {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-icon-style="outline"] .wire-metric-card__icon {
|
||||
background: transparent;
|
||||
border-color: color-mix(in srgb, var(--metric-accent) 34%, var(--wire-color-border));
|
||||
}
|
||||
|
||||
.wire-metric-card[data-icon-style="solid"] .wire-metric-card__icon,
|
||||
.wire-metric-card[data-variant="solid"] .wire-metric-card__icon {
|
||||
color: var(--metric-contrast);
|
||||
background: var(--metric-accent);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-variant="solid"] .wire-metric-card__icon {
|
||||
color: currentColor;
|
||||
background: color-mix(in srgb, white 14%, transparent);
|
||||
border-color: color-mix(in srgb, white 20%, transparent);
|
||||
}
|
||||
|
||||
.wire-metric-card__value-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.2rem;
|
||||
width: 100%;
|
||||
margin-top: 1.15rem;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-align="center"] .wire-metric-card__value-row {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-align="right"] .wire-metric-card__value-row {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.wire-metric-card__value {
|
||||
color: var(--wire-color-text);
|
||||
font-size: clamp(2rem, 5vw, 2.8rem);
|
||||
font-weight: 700;
|
||||
line-height: 0.98;
|
||||
letter-spacing: -0.045em;
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-size="sm"] .wire-metric-card__value {
|
||||
font-size: clamp(1.65rem, 4vw, 2.15rem);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-size="lg"] .wire-metric-card__value {
|
||||
font-size: clamp(2.45rem, 6vw, 3.5rem);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-variant="solid"] .wire-metric-card__value {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-metric-card__prefix,
|
||||
.wire-metric-card__suffix {
|
||||
color: var(--metric-accent);
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-variant="solid"] .wire-metric-card__prefix,
|
||||
.wire-metric-card[data-variant="solid"] .wire-metric-card__suffix {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-metric-card__description {
|
||||
max-width: 38rem;
|
||||
margin: 0.8rem 0 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.wire-metric-card__progress-block {
|
||||
width: 100%;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.wire-metric-card__progress-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.45rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.wire-metric-card__progress-track {
|
||||
width: 100%;
|
||||
height: 0.42rem;
|
||||
overflow: hidden;
|
||||
background: color-mix(in srgb, var(--wire-color-border) 72%, transparent);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.wire-metric-card__progress-value {
|
||||
display: block;
|
||||
width: clamp(0%, var(--wire-metric-progress), 100%);
|
||||
height: 100%;
|
||||
background: var(--metric-accent);
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-variant="solid"] .wire-metric-card__progress-track {
|
||||
background: color-mix(in srgb, white 20%, transparent);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-variant="solid"] .wire-metric-card__progress-value {
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.wire-metric-card__trend {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
gap: 0.42rem;
|
||||
margin-top: 1.15rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-align="center"] .wire-metric-card__trend {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-align="right"] .wire-metric-card__trend {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.wire-metric-card__trend[data-direction="up"],
|
||||
.wire-metric-card__trend[data-direction="positive"] {
|
||||
color: var(--wire-color-success);
|
||||
}
|
||||
|
||||
.wire-metric-card__trend[data-direction="down"],
|
||||
.wire-metric-card__trend[data-direction="negative"] {
|
||||
color: var(--wire-color-danger);
|
||||
}
|
||||
|
||||
.wire-metric-card__trend-icon {
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
}
|
||||
|
||||
.wire-metric-card__trend-label {
|
||||
color: var(--wire-color-text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wire-metric-card__footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.8rem;
|
||||
width: 100%;
|
||||
margin-top: auto;
|
||||
padding-top: 1.35rem;
|
||||
}
|
||||
|
||||
.wire-metric-card__action {
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
color: var(--metric-accent);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
color 160ms ease,
|
||||
gap 160ms ease;
|
||||
}
|
||||
|
||||
.wire-metric-card__action:hover {
|
||||
gap: 0.62rem;
|
||||
color: var(--metric-accent-hover);
|
||||
}
|
||||
|
||||
.wire-metric-card[data-variant="solid"] .wire-metric-card__action {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-metric-card__action-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.wire-metric-card__action:focus-visible {
|
||||
outline: 2px solid var(--wire-color-focus);
|
||||
outline-offset: 4px;
|
||||
border-radius: 0.35rem;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-metric-card {
|
||||
padding: 1.2rem;
|
||||
}
|
||||
|
||||
.wire-metric-card__value {
|
||||
font-size: 2.05rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-metric-card,
|
||||
.wire-metric-card__action {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.wire-metric-card[data-hover="lift"]:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
component MetricGrid {
|
||||
props {
|
||||
@event select = function
|
||||
@event action = function
|
||||
|
||||
items = []
|
||||
columns = 4
|
||||
tabletColumns = 2
|
||||
@@ -10,51 +13,217 @@ component MetricGrid {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
variant = "default"
|
||||
maxWidth = "full"
|
||||
minItemWidth = ""
|
||||
class = ""
|
||||
}
|
||||
|
||||
functions {
|
||||
function forwardSelect(item, itemIndex, sourceEvent) {
|
||||
$emit("select", {
|
||||
item: item,
|
||||
itemIndex: itemIndex,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
|
||||
function forwardAction(item, itemIndex, sourceEvent) {
|
||||
$emit("action", {
|
||||
item: item,
|
||||
itemIndex: itemIndex,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div
|
||||
<section
|
||||
{...attrs}
|
||||
data-ui-component="MetricGrid"
|
||||
class='grid w-full grid-cols-1 {class}'
|
||||
class:grid-cols-2='mobileColumns === 2'
|
||||
class:sm:grid-cols-2='tabletColumns === 2'
|
||||
class:sm:grid-cols-3='tabletColumns === 3'
|
||||
class:lg:grid-cols-2='columns === 2'
|
||||
class:lg:grid-cols-3='columns === 3'
|
||||
class:lg:grid-cols-4='columns === 4'
|
||||
class:lg:grid-cols-5='columns === 5'
|
||||
class:lg:grid-cols-6='columns === 6'
|
||||
class:gap-3='gap === "sm"'
|
||||
class:gap-5='gap === "md"'
|
||||
class:gap-8='gap === "lg"'
|
||||
class:divide-y='dividers'
|
||||
class:sm:divide-y-0='dividers'
|
||||
class:sm:divide-x='dividers'
|
||||
class:divide-[var(--wire-color-border)]='dividers'
|
||||
data-gap='{gap}'
|
||||
data-equal-height='{equalHeight ? "true" : "false"}'
|
||||
data-dividers='{dividers ? "true" : "false"}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
data-max-width='{maxWidth}'
|
||||
data-auto-fit='{minItemWidth ? "true" : "false"}'
|
||||
class='wire-metric-grid {class}'
|
||||
style='display:block;width:100%;min-width:0;--wire-metric-columns:{columns};--wire-metric-tablet-columns:{tabletColumns};--wire-metric-mobile-columns:{mobileColumns};--wire-metric-min-item-width:{minItemWidth || "18rem"};'
|
||||
>
|
||||
{#each items as item}
|
||||
<MetricCard
|
||||
label='{item.label || item.title}'
|
||||
value='{item.value}'
|
||||
description='{item.description || ""}'
|
||||
icon='{item.icon || ""}'
|
||||
prefix='{item.prefix || ""}'
|
||||
suffix='{item.suffix || ""}'
|
||||
trend='{item.trend || ""}'
|
||||
trendLabel='{item.trendLabel || ""}'
|
||||
trendDirection='{item.trendDirection || "neutral"}'
|
||||
href='{item.href || ""}'
|
||||
actionLabel='{item.actionLabel || ""}'
|
||||
size='{item.size || size}'
|
||||
color='{item.color || color}'
|
||||
variant='{dividers ? "minimal" : (item.variant || variant)}'
|
||||
align='{item.align || "left"}'
|
||||
class="h-full"
|
||||
/>
|
||||
{:empty}
|
||||
<slot></slot>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="wire-metric-grid__frame">
|
||||
<div class="wire-metric-grid__surface">
|
||||
{#each items as item, itemIndex}
|
||||
<MetricCard
|
||||
label='{item.label || item.title || "Metric"}'
|
||||
value='{item.value}'
|
||||
description='{item.description || ""}'
|
||||
icon='{item.icon || ""}'
|
||||
iconStyle='{item.iconStyle || "soft"}'
|
||||
prefix='{item.prefix || ""}'
|
||||
suffix='{item.suffix || ""}'
|
||||
badge='{item.badge || ""}'
|
||||
trend='{item.trend || ""}'
|
||||
trendLabel='{item.trendLabel || ""}'
|
||||
trendDirection='{item.trendDirection || "neutral"}'
|
||||
progress='{item.progress === undefined ? -1 : item.progress}'
|
||||
progressLabel='{item.progressLabel || ""}'
|
||||
href='{item.href || ""}'
|
||||
target='{item.target || ""}'
|
||||
rel='{item.rel || ""}'
|
||||
external='{item.external || false}'
|
||||
actionLabel='{item.actionLabel || ""}'
|
||||
actionIcon='{item.actionIcon || ""}'
|
||||
showArrow='{item.showArrow === undefined ? true : item.showArrow}'
|
||||
selectable='{item.selectable || false}'
|
||||
disabled='{item.disabled || false}'
|
||||
size='{item.size || size}'
|
||||
color='{item.color || color}'
|
||||
variant='{dividers ? "minimal" : (item.variant || variant)}'
|
||||
hover='{item.hover || (dividers ? "none" : "lift")}'
|
||||
align='{item.align || "left"}'
|
||||
class="wire-metric-grid__card"
|
||||
@select='forwardSelect(item, itemIndex, event)'
|
||||
@action='forwardAction(item, itemIndex, event)'
|
||||
/>
|
||||
{:empty}
|
||||
<slot></slot>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-metric-grid__frame {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-max-width="compact"] .wire-metric-grid__frame {
|
||||
max-width: 64rem;
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-max-width="lg"] .wire-metric-grid__frame {
|
||||
max-width: 72rem;
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-max-width="xl"] .wire-metric-grid__frame {
|
||||
max-width: 80rem;
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-max-width="wide"] .wire-metric-grid__frame,
|
||||
.wire-metric-grid[data-max-width="2xl"] .wire-metric-grid__frame {
|
||||
max-width: 90rem;
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-max-width="full"] .wire-metric-grid__frame {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.wire-metric-grid__surface {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--wire-metric-mobile-columns), minmax(0, 1fr));
|
||||
gap: 1.25rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-auto-fit="true"] .wire-metric-grid__surface {
|
||||
grid-template-columns: repeat(
|
||||
auto-fit,
|
||||
minmax(min(100%, var(--wire-metric-min-item-width)), 1fr)
|
||||
);
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-gap="none"] .wire-metric-grid__surface {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-gap="sm"] .wire-metric-grid__surface {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-gap="lg"] .wire-metric-grid__surface {
|
||||
gap: 1.75rem;
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-gap="xl"] .wire-metric-grid__surface {
|
||||
gap: 2.25rem;
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-equal-height="true"] .wire-metric-grid__surface > * {
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-variant="panel"] .wire-metric-grid__surface,
|
||||
.wire-metric-grid[data-variant="soft"] .wire-metric-grid__surface,
|
||||
.wire-metric-grid[data-dividers="true"] .wire-metric-grid__surface {
|
||||
overflow: hidden;
|
||||
padding: 0.75rem;
|
||||
background: var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 1.4rem;
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-variant="soft"] .wire-metric-grid__surface {
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--wire-color-primary) 8%, transparent),
|
||||
transparent 62%
|
||||
),
|
||||
var(--wire-color-surface-raised);
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-dividers="true"] .wire-metric-grid__surface {
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-dividers="true"] .wire-metric-grid__card {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.wire-metric-grid[data-auto-fit="false"] .wire-metric-grid__surface {
|
||||
grid-template-columns: repeat(var(--wire-metric-tablet-columns), minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.wire-metric-grid[data-auto-fit="false"] .wire-metric-grid__surface {
|
||||
grid-template-columns: repeat(var(--wire-metric-columns), minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-metric-grid[data-dividers="true"] .wire-metric-grid__surface > * + * {
|
||||
border-top: 1px solid var(--wire-color-border);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) and (max-width: 1023px) {
|
||||
.wire-metric-grid[data-dividers="true"] .wire-metric-grid__surface > * {
|
||||
border-top: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-dividers="true"] .wire-metric-grid__surface > *:nth-child(-n + 2) {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.wire-metric-grid[data-dividers="true"] .wire-metric-grid__surface > *:nth-child(2n) {
|
||||
border-left: 1px solid var(--wire-color-border);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.wire-metric-grid[data-dividers="true"] .wire-metric-grid__surface > * + * {
|
||||
border-left: 1px solid var(--wire-color-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,602 @@ component Modal {
|
||||
@event close = function
|
||||
@event cancel = function
|
||||
@event confirm = function
|
||||
size = "default"
|
||||
color = "primary"
|
||||
|
||||
isOpen = false
|
||||
defaultOpen = false
|
||||
title = "Modal"
|
||||
description = ""
|
||||
open = false
|
||||
placement = "bottom"
|
||||
closeLabel = "Close"
|
||||
icon = ""
|
||||
label = "Modal dialog"
|
||||
size = "md"
|
||||
placement = "center"
|
||||
color = "primary"
|
||||
variant = "default"
|
||||
showClose = true
|
||||
closeLabel = "Close modal"
|
||||
closeOnBackdrop = true
|
||||
closeOnEscape = true
|
||||
closeOnCancel = true
|
||||
closeOnConfirm = false
|
||||
showFooter = true
|
||||
cancelLabel = "Cancel"
|
||||
cancelIcon = ""
|
||||
confirmLabel = "Confirm"
|
||||
confirmIcon = ""
|
||||
confirmDisabled = false
|
||||
confirmLoading = false
|
||||
destructive = false
|
||||
triggerLabel = ""
|
||||
triggerIcon = ""
|
||||
scrollable = true
|
||||
class = ""
|
||||
}
|
||||
|
||||
state visible = defaultOpen
|
||||
|
||||
functions {
|
||||
function isOpen() {
|
||||
return open || visible
|
||||
}
|
||||
|
||||
function showModal(sourceEvent) {
|
||||
visible = true
|
||||
$emit("open", { sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
function hideModal(reason, sourceEvent) {
|
||||
visible = false
|
||||
$emit("close", {
|
||||
reason: reason,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
|
||||
function cancelModal(sourceEvent) {
|
||||
$emit("cancel", { sourceEvent: sourceEvent })
|
||||
if (closeOnCancel) {
|
||||
hideModal("cancel", sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
function confirmModal(sourceEvent) {
|
||||
if (confirmDisabled || confirmLoading) {
|
||||
return
|
||||
}
|
||||
$emit("confirm", { sourceEvent: sourceEvent })
|
||||
if (closeOnConfirm) {
|
||||
hideModal("confirm", sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(sourceEvent) {
|
||||
if (closeOnEscape && sourceEvent.key === "Escape") {
|
||||
sourceEvent.preventDefault()
|
||||
cancelModal(sourceEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div data-show="{open}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--modal wire-next--placement-{placement} {class}" role="dialog" aria-modal="true" aria-label="{title}">
|
||||
<header><strong>{title}</strong><button type="button" aria-label="{closeLabel}">×</button></header>
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
<slot />
|
||||
<div
|
||||
{...attrs}
|
||||
data-ui-component="Modal"
|
||||
data-open='{open || visible ? "true" : "false"}'
|
||||
data-size='{size}'
|
||||
data-placement='{placement}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
data-scrollable='{scrollable ? "true" : "false"}'
|
||||
data-destructive='{destructive ? "true" : "false"}'
|
||||
class='wire-modal {class}'
|
||||
>
|
||||
{#if triggerLabel}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-modal__trigger"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded='{open || visible ? "true" : "false"}'
|
||||
@click='showModal(event)'
|
||||
>
|
||||
{#if triggerIcon}
|
||||
<span
|
||||
class='{triggerIcon}'
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
<span>{triggerLabel}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<span
|
||||
class="wire-modal__trigger-slot"
|
||||
@click='showModal(event)'
|
||||
>
|
||||
<slot
|
||||
name="trigger"
|
||||
>
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="wire-modal__layer"
|
||||
data-show='{open || visible}'
|
||||
role="presentation"
|
||||
@keydown='handleKeydown(event)'
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="wire-modal__backdrop"
|
||||
aria-label='{closeLabel}'
|
||||
@click='if (closeOnBackdrop) { cancelModal(event) }'
|
||||
>
|
||||
</button>
|
||||
|
||||
<section
|
||||
class="wire-modal__panel"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label='{label || title}'
|
||||
tabindex="-1"
|
||||
>
|
||||
<header
|
||||
class="wire-modal__header"
|
||||
>
|
||||
<div
|
||||
class="wire-modal__heading"
|
||||
>
|
||||
{#if icon}
|
||||
<span
|
||||
class='wire-modal__icon {icon}'
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-modal__heading-copy"
|
||||
>
|
||||
<slot
|
||||
name="header"
|
||||
>
|
||||
</slot>
|
||||
{#if title}
|
||||
<h2>{title}</h2>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p>{description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showClose}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-modal__close"
|
||||
aria-label='{closeLabel}'
|
||||
@click='hideModal("close-button", event)'
|
||||
>
|
||||
<span
|
||||
class="icon-[lucide--x]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="wire-modal__body"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
{#if showFooter}
|
||||
<footer
|
||||
class="wire-modal__footer"
|
||||
>
|
||||
<div
|
||||
class="wire-modal__custom-footer"
|
||||
>
|
||||
<slot
|
||||
name="footer"
|
||||
>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="wire-modal__actions"
|
||||
>
|
||||
{#if cancelLabel}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-modal__button wire-modal__button--secondary"
|
||||
@click='cancelModal(event)'
|
||||
>
|
||||
{#if cancelIcon}
|
||||
<span
|
||||
class='{cancelIcon}'
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
<span>{cancelLabel}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if confirmLabel}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-modal__button wire-modal__button--primary"
|
||||
disabled='{confirmDisabled || confirmLoading}'
|
||||
@click='confirmModal(event)'
|
||||
>
|
||||
{#if confirmLoading}
|
||||
<span
|
||||
class="wire-modal__spinner"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{:else if confirmIcon}
|
||||
<span
|
||||
class='{confirmIcon}'
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
<span>{confirmLabel}</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</footer>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-modal {
|
||||
--modal-accent: var(--wire-color-primary);
|
||||
--modal-soft: var(--wire-color-primary-soft);
|
||||
--modal-contrast: var(--wire-color-primary-contrast);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.wire-modal[data-color="secondary"] {
|
||||
--modal-accent: var(--wire-color-secondary);
|
||||
--modal-soft: var(--wire-color-secondary-soft);
|
||||
--modal-contrast: var(--wire-color-secondary-contrast);
|
||||
}
|
||||
|
||||
.wire-modal[data-color="info"] {
|
||||
--modal-accent: var(--wire-color-info);
|
||||
--modal-soft: var(--wire-color-info-soft);
|
||||
--modal-contrast: var(--wire-color-info-contrast);
|
||||
}
|
||||
|
||||
.wire-modal[data-color="success"] {
|
||||
--modal-accent: var(--wire-color-success);
|
||||
--modal-soft: var(--wire-color-success-soft);
|
||||
--modal-contrast: var(--wire-color-success-contrast);
|
||||
}
|
||||
|
||||
.wire-modal[data-color="warning"] {
|
||||
--modal-accent: var(--wire-color-warning);
|
||||
--modal-soft: var(--wire-color-warning-soft);
|
||||
--modal-contrast: var(--wire-color-warning-text);
|
||||
}
|
||||
|
||||
.wire-modal[data-color="danger"],
|
||||
.wire-modal[data-destructive="true"] {
|
||||
--modal-accent: var(--wire-color-danger);
|
||||
--modal-soft: var(--wire-color-danger-soft);
|
||||
--modal-contrast: var(--wire-color-on-danger);
|
||||
}
|
||||
|
||||
.wire-modal__trigger,
|
||||
.wire-modal__trigger-slot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.wire-modal__trigger {
|
||||
appearance: none;
|
||||
min-height: 2.55rem;
|
||||
padding: 0.65rem 1rem;
|
||||
color: var(--modal-contrast);
|
||||
background: var(--modal-accent);
|
||||
border: 0;
|
||||
border-radius: 0.8rem;
|
||||
font: inherit;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wire-modal__layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1250;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.wire-modal[data-placement="top"] .wire-modal__layer {
|
||||
align-items: flex-start;
|
||||
padding-top: clamp(1rem, 8vh, 5rem);
|
||||
}
|
||||
|
||||
.wire-modal__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
background: color-mix(in srgb, black 62%, transparent);
|
||||
border: 0;
|
||||
backdrop-filter: blur(9px);
|
||||
}
|
||||
|
||||
.wire-modal__panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(32rem, calc(100vw - 2rem));
|
||||
max-height: min(88vh, 52rem);
|
||||
color: var(--wire-color-text);
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 100% 0%,
|
||||
color-mix(in srgb, var(--modal-accent) 9%, transparent),
|
||||
transparent 34%
|
||||
),
|
||||
var(--wire-color-surface-raised);
|
||||
border: 1px solid color-mix(in srgb, var(--modal-accent) 18%, var(--wire-color-border));
|
||||
border-radius: 1.35rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 40px 110px color-mix(in srgb, black 38%, transparent);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wire-modal[data-size="sm"] .wire-modal__panel {
|
||||
width: min(25rem, calc(100vw - 2rem));
|
||||
}
|
||||
|
||||
.wire-modal[data-size="lg"] .wire-modal__panel {
|
||||
width: min(44rem, calc(100vw - 2rem));
|
||||
}
|
||||
|
||||
.wire-modal[data-size="xl"] .wire-modal__panel {
|
||||
width: min(64rem, calc(100vw - 2rem));
|
||||
}
|
||||
|
||||
.wire-modal[data-size="full"] .wire-modal__panel {
|
||||
width: calc(100vw - 2rem);
|
||||
height: calc(100vh - 2rem);
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="soft"] .wire-modal__panel {
|
||||
background:
|
||||
linear-gradient(145deg, var(--modal-soft), transparent 68%),
|
||||
var(--wire-color-surface-raised);
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="outline"] .wire-modal__panel {
|
||||
background: var(--wire-color-surface-raised);
|
||||
box-shadow: 0 24px 72px color-mix(in srgb, black 24%, transparent);
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="solid"] .wire-modal__panel {
|
||||
color: var(--modal-contrast);
|
||||
background: var(--modal-accent);
|
||||
border-color: color-mix(in srgb, white 20%, transparent);
|
||||
}
|
||||
|
||||
.wire-modal__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1.4rem 1.4rem 1.15rem;
|
||||
border-bottom: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-modal__heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.9rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-modal__icon {
|
||||
flex: 0 0 auto;
|
||||
width: 1.3rem;
|
||||
height: 1.3rem;
|
||||
margin-top: 0.15rem;
|
||||
color: var(--modal-accent);
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="solid"] .wire-modal__icon {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-modal__heading-copy {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-modal__heading-copy h2,
|
||||
.wire-modal__heading-copy p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wire-modal__heading-copy h2 {
|
||||
font-size: 1.08rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.wire-modal__heading-copy p {
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="solid"] .wire-modal__heading-copy p {
|
||||
color: color-mix(in srgb, currentColor 76%, transparent);
|
||||
}
|
||||
|
||||
.wire-modal__close {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
width: 2.35rem;
|
||||
height: 2.35rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
background: var(--wire-color-surface-soft);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wire-modal__close:hover,
|
||||
.wire-modal__close:focus-visible {
|
||||
color: var(--modal-accent);
|
||||
border-color: color-mix(in srgb, var(--modal-accent) 34%, var(--wire-color-border));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-modal__body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: 1.4rem;
|
||||
}
|
||||
|
||||
.wire-modal[data-scrollable="true"] .wire-modal__body {
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.wire-modal__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.4rem 1.4rem;
|
||||
border-top: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-modal__custom-footer:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-modal__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.65rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.wire-modal__button {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
min-height: 2.55rem;
|
||||
padding: 0.65rem 1rem;
|
||||
border-radius: 0.78rem;
|
||||
font: inherit;
|
||||
font-size: 0.83rem;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
transition: transform 150ms ease, opacity 150ms ease, border-color 150ms ease;
|
||||
}
|
||||
|
||||
.wire-modal__button:active {
|
||||
transform: scale(0.985);
|
||||
}
|
||||
|
||||
.wire-modal__button--secondary {
|
||||
color: var(--wire-color-text);
|
||||
background: transparent;
|
||||
border: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-modal__button--primary {
|
||||
color: var(--modal-contrast);
|
||||
background: var(--modal-accent);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.wire-modal__button:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.wire-modal__spinner {
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
border: 2px solid color-mix(in srgb, currentColor 35%, transparent);
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: wire-modal-spin 750ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes wire-modal-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-modal__layer {
|
||||
align-items: flex-end;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.wire-modal__panel,
|
||||
.wire-modal[data-size="sm"] .wire-modal__panel,
|
||||
.wire-modal[data-size="lg"] .wire-modal__panel,
|
||||
.wire-modal[data-size="xl"] .wire-modal__panel {
|
||||
width: 100%;
|
||||
max-height: 92vh;
|
||||
border-radius: 1.25rem 1.25rem 0 0;
|
||||
}
|
||||
|
||||
.wire-modal__footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wire-modal__actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wire-modal__button {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-modal__button,
|
||||
.wire-modal__spinner {
|
||||
animation: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,17 @@ component PageHeader {
|
||||
title = ""
|
||||
description = ""
|
||||
icon = ""
|
||||
id = "page-title"
|
||||
align = "left"
|
||||
size = "default"
|
||||
centered = false
|
||||
compact = false
|
||||
size = "default"
|
||||
maxWidth = "xl"
|
||||
showBreadcrumbs = false
|
||||
breadcrumbs = []
|
||||
breadcrumbParent = ""
|
||||
breadcrumbParentHref = ""
|
||||
breadcrumbCurrent = ""
|
||||
primaryLabel = ""
|
||||
primaryHref = ""
|
||||
primaryIcon = ""
|
||||
@@ -17,89 +23,665 @@ component PageHeader {
|
||||
secondaryIcon = ""
|
||||
color = "primary"
|
||||
variant = "default"
|
||||
borderBottom = true
|
||||
class = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<div
|
||||
<section
|
||||
{...attrs}
|
||||
data-ui-component="PageHeader"
|
||||
data-size='{size}'
|
||||
data-align='{centered ? "center" : align}'
|
||||
data-color='{color}'
|
||||
class='{class}'
|
||||
data-variant='{variant}'
|
||||
data-compact='{compact ? "true" : "false"}'
|
||||
data-max-width='{maxWidth}'
|
||||
data-border-bottom='{borderBottom ? "true" : "false"}'
|
||||
aria-labelledby='{id}'
|
||||
class='wire-page-header {class}'
|
||||
>
|
||||
<Section
|
||||
spacing='{compact || size === "compact" ? "md" : "lg"}'
|
||||
variant='{variant}'
|
||||
color='{color}'
|
||||
borderBottom="true"
|
||||
<div
|
||||
class="wire-page-header__decoration"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span
|
||||
class="wire-page-header__glow wire-page-header__glow--one"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
class="wire-page-header__glow wire-page-header__glow--two"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
class="wire-page-header__grid-pattern"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="wire-page-header__inner"
|
||||
>
|
||||
{#if showBreadcrumbs && breadcrumbs.length > 0}
|
||||
<Breadcrumb
|
||||
label="Breadcrumb"
|
||||
items='{breadcrumbs}'
|
||||
active='{breadcrumbs[breadcrumbs.length - 1]?.value || breadcrumbs[breadcrumbs.length - 1]?.label || ""}'
|
||||
color='{color}'
|
||||
class="mb-6"
|
||||
/>
|
||||
<div
|
||||
class="wire-page-header__breadcrumb"
|
||||
>
|
||||
<Breadcrumb
|
||||
label="Breadcrumb"
|
||||
items='{breadcrumbs}'
|
||||
active='{breadcrumbs[breadcrumbs.length - 1]?.value || breadcrumbs[breadcrumbs.length - 1]?.label || breadcrumbs[breadcrumbs.length - 1]?.title || ""}'
|
||||
color='{color}'
|
||||
size='{compact ? "sm" : "default"}'
|
||||
variant='{variant === "solid" ? "contrast" : "minimal"}'
|
||||
/>
|
||||
</div>
|
||||
{:else if showBreadcrumbs && breadcrumbParent}
|
||||
<div
|
||||
class="wire-page-header__breadcrumb"
|
||||
>
|
||||
<Breadcrumb
|
||||
label="Breadcrumb"
|
||||
items='{[
|
||||
{
|
||||
label: breadcrumbParent,
|
||||
href: breadcrumbParentHref,
|
||||
value: breadcrumbParent
|
||||
},
|
||||
{
|
||||
label: breadcrumbCurrent || title,
|
||||
value: breadcrumbCurrent || title,
|
||||
current: true
|
||||
}
|
||||
]}'
|
||||
active='{breadcrumbCurrent || title}'
|
||||
color='{color}'
|
||||
size='{compact ? "sm" : "default"}'
|
||||
variant='{variant === "solid" ? "contrast" : "minimal"}'
|
||||
/>
|
||||
</div>
|
||||
{:else if showBreadcrumbs && (breadcrumbCurrent || title)}
|
||||
<div
|
||||
class="wire-page-header__breadcrumb"
|
||||
>
|
||||
<Breadcrumb
|
||||
label="Breadcrumb"
|
||||
items='{[
|
||||
{
|
||||
label: breadcrumbCurrent || title,
|
||||
value: breadcrumbCurrent || title,
|
||||
current: true
|
||||
}
|
||||
]}'
|
||||
active='{breadcrumbCurrent || title}'
|
||||
color='{color}'
|
||||
size='{compact ? "sm" : "default"}'
|
||||
variant='{variant === "solid" ? "contrast" : "minimal"}'
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-6"
|
||||
class:items-center='align === "center"'
|
||||
class:text-center='align === "center"'
|
||||
class:lg:flex-row='align !== "center"'
|
||||
class:lg:items-end='align !== "center"'
|
||||
class:lg:justify-between='align !== "center"'
|
||||
class="wire-page-header__layout"
|
||||
>
|
||||
<div class="max-w-4xl">
|
||||
{#if icon}
|
||||
<span class="mb-5 inline-flex size-12 items-center justify-center rounded-2xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]">
|
||||
<span class='{icon + " size-6"}' aria-hidden="true"></span>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<SectionHeader
|
||||
eyebrow='{eyebrow}'
|
||||
title='{title}'
|
||||
description='{description}'
|
||||
align='{align === "center" ? "center" : "left"}'
|
||||
size='{compact || size === "compact" ? "sm" : "lg"}'
|
||||
color='{color}'
|
||||
headingLevel="1"
|
||||
maxWidth="4xl"
|
||||
<div
|
||||
class="wire-page-header__content"
|
||||
>
|
||||
<div
|
||||
class="wire-page-header__intro"
|
||||
>
|
||||
<slot name="meta"></slot>
|
||||
</SectionHeader>
|
||||
{#if icon}
|
||||
<span
|
||||
class="wire-page-header__icon-wrap"
|
||||
>
|
||||
{#if icon === "sparkles"}
|
||||
<span
|
||||
class="icon-[lucide--sparkles] wire-page-header__icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{:else if icon === "component"}
|
||||
<span
|
||||
class="icon-[lucide--component] wire-page-header__icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{:else}
|
||||
<span
|
||||
class='{icon} wire-page-header__icon'
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-page-header__copy"
|
||||
>
|
||||
{#if eyebrow}
|
||||
<div
|
||||
class="wire-page-header__eyebrow-row"
|
||||
>
|
||||
<span
|
||||
class="wire-page-header__eyebrow-mark"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
<p
|
||||
class="wire-page-header__eyebrow"
|
||||
>
|
||||
{eyebrow}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if title}
|
||||
<h1
|
||||
id='{id}'
|
||||
class="wire-page-header__title"
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
{/if}
|
||||
|
||||
{#if description}
|
||||
<p
|
||||
class="wire-page-header__description"
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-page-header__meta"
|
||||
>
|
||||
<slot
|
||||
name="meta"
|
||||
>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3" class:justify-center='align === "center"'>
|
||||
{#if secondaryLabel}
|
||||
<Button
|
||||
label='{secondaryLabel}'
|
||||
href='{secondaryHref}'
|
||||
icon='{secondaryIcon}'
|
||||
variant="outline"
|
||||
color='{color}'
|
||||
/>
|
||||
{/if}
|
||||
{#if primaryLabel || secondaryLabel}
|
||||
<div
|
||||
class="wire-page-header__actions"
|
||||
>
|
||||
{#if secondaryLabel}
|
||||
<Button
|
||||
label='{secondaryLabel}'
|
||||
href='{secondaryHref || "#"}'
|
||||
icon='{secondaryIcon}'
|
||||
size='{compact ? "sm" : "default"}'
|
||||
variant="outline"
|
||||
color='{color}'
|
||||
class="wire-page-header__action wire-page-header__action--secondary"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if primaryLabel}
|
||||
<Button
|
||||
label='{primaryLabel}'
|
||||
href='{primaryHref}'
|
||||
icon='{primaryIcon}'
|
||||
variant="default"
|
||||
color='{color}'
|
||||
/>
|
||||
{/if}
|
||||
{#if primaryLabel}
|
||||
<Button
|
||||
label='{primaryLabel}'
|
||||
href='{primaryHref || "#"}'
|
||||
icon='{primaryIcon}'
|
||||
size='{compact ? "sm" : "default"}'
|
||||
variant="default"
|
||||
color='{color}'
|
||||
class="wire-page-header__action wire-page-header__action--primary"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
<slot
|
||||
name="actions"
|
||||
>
|
||||
</slot>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="wire-page-header__actions wire-page-header__actions--slot-only"
|
||||
>
|
||||
<slot
|
||||
name="actions"
|
||||
>
|
||||
</slot>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
</Section>
|
||||
</div>
|
||||
<div
|
||||
class="wire-page-header__body"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-page-header {
|
||||
--page-header-accent: var(--wire-color-primary);
|
||||
--page-header-soft: var(--wire-color-primary-soft);
|
||||
--page-header-muted: var(--wire-color-primary-muted);
|
||||
--page-header-accent-text: var(--wire-color-primary-text);
|
||||
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
color: var(--wire-color-text);
|
||||
background: var(--wire-color-background);
|
||||
}
|
||||
|
||||
.wire-page-header[data-color="secondary"] {
|
||||
--page-header-accent: var(--wire-color-secondary);
|
||||
--page-header-soft: var(--wire-color-secondary-soft);
|
||||
--page-header-muted: var(--wire-color-secondary-muted);
|
||||
--page-header-accent-text: var(--wire-color-secondary-text);
|
||||
}
|
||||
|
||||
.wire-page-header[data-color="info"] {
|
||||
--page-header-accent: var(--wire-color-info);
|
||||
--page-header-soft: var(--wire-color-info-soft);
|
||||
--page-header-muted: var(--wire-color-info-muted);
|
||||
--page-header-accent-text: var(--wire-color-info-text);
|
||||
}
|
||||
|
||||
.wire-page-header[data-color="success"] {
|
||||
--page-header-accent: var(--wire-color-success);
|
||||
--page-header-soft: var(--wire-color-success-soft);
|
||||
--page-header-muted: var(--wire-color-success-muted);
|
||||
--page-header-accent-text: var(--wire-color-success-text);
|
||||
}
|
||||
|
||||
.wire-page-header[data-color="warning"] {
|
||||
--page-header-accent: var(--wire-color-warning);
|
||||
--page-header-soft: var(--wire-color-warning-soft);
|
||||
--page-header-muted: var(--wire-color-warning-muted);
|
||||
--page-header-accent-text: var(--wire-color-warning-text);
|
||||
}
|
||||
|
||||
.wire-page-header[data-color="danger"] {
|
||||
--page-header-accent: var(--wire-color-danger);
|
||||
--page-header-soft: var(--wire-color-danger-soft);
|
||||
--page-header-muted: var(--wire-color-danger-muted);
|
||||
--page-header-accent-text: var(--wire-color-danger-text);
|
||||
}
|
||||
|
||||
.wire-page-header[data-variant="soft"] {
|
||||
background: var(--wire-color-surface);
|
||||
}
|
||||
|
||||
.wire-page-header[data-variant="raised"] {
|
||||
background: var(--wire-color-surface-raised);
|
||||
box-shadow: var(--wire-shadow-sm);
|
||||
}
|
||||
|
||||
.wire-page-header[data-variant="tinted"] {
|
||||
background:
|
||||
linear-gradient(135deg, var(--page-header-soft), transparent 72%),
|
||||
var(--wire-color-background);
|
||||
}
|
||||
|
||||
.wire-page-header[data-variant="solid"] {
|
||||
color: #ffffff;
|
||||
background: var(--page-header-accent);
|
||||
}
|
||||
|
||||
.wire-page-header[data-variant="minimal"] {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.wire-page-header[data-border-bottom="true"] {
|
||||
border-bottom: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-page-header[data-variant="solid"][data-border-bottom="true"] {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.wire-page-header__decoration {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wire-page-header__glow {
|
||||
position: absolute;
|
||||
display: block;
|
||||
border-radius: 999px;
|
||||
filter: blur(60px);
|
||||
opacity: 0.17;
|
||||
}
|
||||
|
||||
.wire-page-header__glow--one {
|
||||
top: -9rem;
|
||||
right: -5rem;
|
||||
width: 24rem;
|
||||
height: 24rem;
|
||||
background: var(--page-header-accent);
|
||||
}
|
||||
|
||||
.wire-page-header__glow--two {
|
||||
bottom: -10rem;
|
||||
left: 18%;
|
||||
width: 18rem;
|
||||
height: 18rem;
|
||||
background: var(--page-header-muted);
|
||||
opacity: 0.13;
|
||||
}
|
||||
|
||||
.wire-page-header__grid-pattern {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.2;
|
||||
background-image:
|
||||
linear-gradient(to right, var(--wire-color-border) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, var(--wire-color-border) 1px, transparent 1px);
|
||||
background-size: 32px 32px;
|
||||
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.35), transparent 78%);
|
||||
}
|
||||
|
||||
.wire-page-header[data-variant="minimal"] .wire-page-header__decoration,
|
||||
.wire-page-header[data-compact="true"] .wire-page-header__grid-pattern {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-page-header[data-variant="solid"] .wire-page-header__grid-pattern {
|
||||
opacity: 0.1;
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(255, 255, 255, 0.28) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.28) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.wire-page-header__inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 80rem;
|
||||
margin-inline: auto;
|
||||
padding: 1.25rem 1rem 2.75rem;
|
||||
}
|
||||
|
||||
.wire-page-header[data-max-width="compact"] .wire-page-header__inner,
|
||||
.wire-page-header[data-max-width="lg"] .wire-page-header__inner {
|
||||
max-width: 64rem;
|
||||
}
|
||||
|
||||
.wire-page-header[data-max-width="wide"] .wire-page-header__inner,
|
||||
.wire-page-header[data-max-width="2xl"] .wire-page-header__inner {
|
||||
max-width: 90rem;
|
||||
}
|
||||
|
||||
.wire-page-header[data-max-width="full"] .wire-page-header__inner {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.wire-page-header[data-compact="true"] .wire-page-header__inner {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.wire-page-header[data-size="lg"] .wire-page-header__inner {
|
||||
padding-bottom: 3.5rem;
|
||||
}
|
||||
|
||||
.wire-page-header__breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
margin-bottom: 1.75rem;
|
||||
padding-bottom: 0.9rem;
|
||||
border-bottom: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-page-header[data-variant="solid"] .wire-page-header__breadcrumb {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.wire-page-header[data-compact="true"] .wire-page-header__breadcrumb {
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.wire-page-header__layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 1.75rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.wire-page-header__content,
|
||||
.wire-page-header__copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-page-header__intro {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-page-header__icon-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
width: 3.25rem;
|
||||
height: 3.25rem;
|
||||
margin-top: 0.15rem;
|
||||
color: var(--page-header-accent);
|
||||
background: var(--page-header-soft);
|
||||
border: 1px solid var(--page-header-muted);
|
||||
border-radius: 1rem;
|
||||
box-shadow: var(--wire-shadow-sm);
|
||||
}
|
||||
|
||||
.wire-page-header[data-compact="true"] .wire-page-header__icon-wrap {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
.wire-page-header[data-variant="solid"] .wire-page-header__icon-wrap {
|
||||
color: #ffffff;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
.wire-page-header__icon {
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
}
|
||||
|
||||
.wire-page-header__eyebrow-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
margin-bottom: 0.65rem;
|
||||
}
|
||||
|
||||
.wire-page-header__eyebrow-mark {
|
||||
width: 1.75rem;
|
||||
height: 2px;
|
||||
flex: 0 0 auto;
|
||||
background: var(--page-header-accent);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.wire-page-header__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--page-header-accent-text);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wire-page-header[data-variant="solid"] .wire-page-header__eyebrow,
|
||||
.wire-page-header[data-variant="solid"] .wire-page-header__title,
|
||||
.wire-page-header[data-variant="solid"] .wire-page-header__description {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.wire-page-header[data-variant="solid"] .wire-page-header__eyebrow-mark {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.wire-page-header__title {
|
||||
max-width: 54rem;
|
||||
margin: 0;
|
||||
color: var(--wire-color-text);
|
||||
font-size: clamp(2rem, 4vw, 3.75rem);
|
||||
font-weight: 800;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.035em;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.wire-page-header[data-compact="true"] .wire-page-header__title {
|
||||
font-size: clamp(1.75rem, 3vw, 2.65rem);
|
||||
}
|
||||
|
||||
.wire-page-header[data-size="lg"] .wire-page-header__title {
|
||||
font-size: clamp(2.35rem, 5vw, 4.5rem);
|
||||
}
|
||||
|
||||
.wire-page-header__description {
|
||||
max-width: 48rem;
|
||||
margin: 0.9rem 0 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 1rem;
|
||||
line-height: 1.75;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.wire-page-header[data-size="lg"] .wire-page-header__description {
|
||||
font-size: 1.075rem;
|
||||
}
|
||||
|
||||
.wire-page-header[data-variant="solid"] .wire-page-header__description {
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.wire-page-header__meta:empty,
|
||||
.wire-page-header__body:empty,
|
||||
.wire-page-header__actions--slot-only:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-page-header__meta {
|
||||
margin-top: 1.1rem;
|
||||
}
|
||||
|
||||
.wire-page-header__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-page-header__body {
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
.wire-page-header[data-align="center"] .wire-page-header__layout {
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wire-page-header[data-align="center"] .wire-page-header__intro {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wire-page-header[data-align="center"] .wire-page-header__eyebrow-row,
|
||||
.wire-page-header[data-align="center"] .wire-page-header__actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wire-page-header[data-align="center"] .wire-page-header__title,
|
||||
.wire-page-header[data-align="center"] .wire-page-header__description {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.wire-page-header[data-align="right"] .wire-page-header__layout {
|
||||
justify-items: end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.wire-page-header[data-align="right"] .wire-page-header__intro {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.wire-page-header[data-align="right"] .wire-page-header__eyebrow-row,
|
||||
.wire-page-header[data-align="right"] .wire-page-header__actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.wire-page-header[data-align="right"] .wire-page-header__title,
|
||||
.wire-page-header[data-align="right"] .wire-page-header__description {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.wire-page-header__inner {
|
||||
padding-inline: 1.5rem;
|
||||
}
|
||||
|
||||
.wire-page-header__intro {
|
||||
gap: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.wire-page-header__inner {
|
||||
padding-inline: 2rem;
|
||||
}
|
||||
|
||||
.wire-page-header__layout {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
.wire-page-header[data-align="center"] .wire-page-header__layout,
|
||||
.wire-page-header[data-align="right"] .wire-page-header__layout {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.wire-page-header__actions {
|
||||
align-self: start;
|
||||
justify-self: end;
|
||||
justify-content: flex-end;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-page-header__intro {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wire-page-header__actions {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.wire-page-header__action {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-page-header__glow {
|
||||
filter: blur(52px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,533 @@
|
||||
component Popover {
|
||||
props {
|
||||
@event toggle = function
|
||||
@event open = function
|
||||
@event close = function
|
||||
@event action = function
|
||||
|
||||
open = false
|
||||
defaultOpen = false
|
||||
triggerLabel = "Open popover"
|
||||
triggerIcon = ""
|
||||
title = ""
|
||||
description = ""
|
||||
icon = ""
|
||||
placement = "bottom-start"
|
||||
width = "md"
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Popover"
|
||||
description = ""
|
||||
open = false
|
||||
placement = "bottom"
|
||||
closeLabel = "Close"
|
||||
variant = "raised"
|
||||
showArrow = true
|
||||
showClose = false
|
||||
closeLabel = "Close popover"
|
||||
closeOnOutside = true
|
||||
closeOnEscape = true
|
||||
actionLabel = ""
|
||||
actionHref = ""
|
||||
actionIcon = ""
|
||||
closeOnAction = true
|
||||
disabled = false
|
||||
class = ""
|
||||
}
|
||||
|
||||
state visible = defaultOpen
|
||||
|
||||
functions {
|
||||
function isOpen() {
|
||||
return open || visible
|
||||
}
|
||||
|
||||
function showPopover(sourceEvent) {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
visible = true
|
||||
$emit("open", { sourceEvent: sourceEvent })
|
||||
$emit("toggle", { open: true, sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
function hidePopover(reason, sourceEvent) {
|
||||
visible = false
|
||||
$emit("close", { reason: reason, sourceEvent: sourceEvent })
|
||||
$emit("toggle", { open: false, reason: reason, sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
function togglePopover(sourceEvent) {
|
||||
if (isOpen()) {
|
||||
hidePopover("toggle", sourceEvent)
|
||||
} else {
|
||||
showPopover(sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
function activateAction(sourceEvent) {
|
||||
$emit("action", {
|
||||
href: actionHref,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
if (closeOnAction) {
|
||||
hidePopover("action", sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(sourceEvent) {
|
||||
if (closeOnEscape && sourceEvent.key === "Escape") {
|
||||
sourceEvent.preventDefault()
|
||||
hidePopover("escape", sourceEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div data-show="{open}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--popover wire-next--placement-{placement} {class}" role="region" aria-modal="false" aria-label="{title}">
|
||||
<header><strong>{title}</strong><button type="button" aria-label="{closeLabel}">×</button></header>
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
<slot />
|
||||
<div
|
||||
{...attrs}
|
||||
data-ui-component="Popover"
|
||||
data-open='{open || visible ? "true" : "false"}'
|
||||
data-placement='{placement}'
|
||||
data-width='{width}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
class='wire-popover {class}'
|
||||
@keydown='handleKeydown(event)'
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="wire-popover__trigger"
|
||||
disabled='{disabled}'
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded='{open || visible ? "true" : "false"}'
|
||||
@click='togglePopover(event)'
|
||||
>
|
||||
<slot name="trigger"></slot>
|
||||
{#if triggerIcon}
|
||||
<span class='{triggerIcon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
{#if triggerLabel}
|
||||
<span>{triggerLabel}</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if closeOnOutside}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-popover__dismiss-layer"
|
||||
data-show='{open || visible}'
|
||||
aria-label='{closeLabel}'
|
||||
@click='hidePopover("outside", event)'
|
||||
></button>
|
||||
{/if}
|
||||
|
||||
<section
|
||||
class="wire-popover__panel"
|
||||
data-show='{open || visible}'
|
||||
role="dialog"
|
||||
aria-label='{title || triggerLabel}'
|
||||
>
|
||||
{#if showArrow}
|
||||
<span class="wire-popover__arrow" aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
{#if title || description || icon || showClose}
|
||||
<header class="wire-popover__header">
|
||||
<div class="wire-popover__heading">
|
||||
{#if icon}
|
||||
<span class='wire-popover__icon {icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<div class="wire-popover__heading-copy">
|
||||
<slot name="header"></slot>
|
||||
{#if title}
|
||||
<h3>{title}</h3>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p>{description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showClose}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-popover__close"
|
||||
aria-label='{closeLabel}'
|
||||
@click='hidePopover("close-button", event)'
|
||||
>
|
||||
<span class="icon-[lucide--x]" aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
<div class="wire-popover__body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
{#if actionLabel}
|
||||
<footer class="wire-popover__footer">
|
||||
{#if actionHref}
|
||||
<a
|
||||
href='{actionHref}'
|
||||
class="wire-popover__action"
|
||||
@click='activateAction(event)'
|
||||
>
|
||||
<span>{actionLabel}</span>
|
||||
{#if actionIcon}
|
||||
<span class='{actionIcon}' aria-hidden="true"></span>
|
||||
{:else}
|
||||
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-popover__action"
|
||||
@click='activateAction(event)'
|
||||
>
|
||||
<span>{actionLabel}</span>
|
||||
{#if actionIcon}
|
||||
<span class='{actionIcon}' aria-hidden="true"></span>
|
||||
{:else}
|
||||
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</footer>
|
||||
{/if}
|
||||
|
||||
<slot name="footer"></slot>
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-popover {
|
||||
--popover-accent: var(--wire-color-primary);
|
||||
--popover-soft: var(--wire-color-primary-soft);
|
||||
--popover-contrast: var(--wire-color-primary-contrast);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.wire-popover[data-color="secondary"] {
|
||||
--popover-accent: var(--wire-color-secondary);
|
||||
--popover-soft: var(--wire-color-secondary-soft);
|
||||
--popover-contrast: var(--wire-color-secondary-contrast);
|
||||
}
|
||||
|
||||
.wire-popover[data-color="info"] {
|
||||
--popover-accent: var(--wire-color-info);
|
||||
--popover-soft: var(--wire-color-info-soft);
|
||||
--popover-contrast: var(--wire-color-info-contrast);
|
||||
}
|
||||
|
||||
.wire-popover[data-color="success"] {
|
||||
--popover-accent: var(--wire-color-success);
|
||||
--popover-soft: var(--wire-color-success-soft);
|
||||
--popover-contrast: var(--wire-color-success-contrast);
|
||||
}
|
||||
|
||||
.wire-popover[data-color="warning"] {
|
||||
--popover-accent: var(--wire-color-warning);
|
||||
--popover-soft: var(--wire-color-warning-soft);
|
||||
--popover-contrast: var(--wire-color-warning-text);
|
||||
}
|
||||
|
||||
.wire-popover[data-color="danger"] {
|
||||
--popover-accent: var(--wire-color-danger);
|
||||
--popover-soft: var(--wire-color-danger-soft);
|
||||
--popover-contrast: var(--wire-color-on-danger);
|
||||
}
|
||||
|
||||
.wire-popover__trigger {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.55rem;
|
||||
min-height: 2.55rem;
|
||||
padding: 0.65rem 0.9rem;
|
||||
color: var(--wire-color-text);
|
||||
background: var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.78rem;
|
||||
font: inherit;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: color 150ms ease, border-color 150ms ease, background-color 150ms ease;
|
||||
}
|
||||
|
||||
.wire-popover__trigger:hover,
|
||||
.wire-popover__trigger:focus-visible,
|
||||
.wire-popover[data-open="true"] .wire-popover__trigger {
|
||||
color: var(--popover-accent);
|
||||
background: var(--popover-soft);
|
||||
border-color: color-mix(in srgb, var(--popover-accent) 38%, var(--wire-color-border));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-popover__trigger:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.wire-popover[data-size="sm"] .wire-popover__trigger {
|
||||
min-height: 2.2rem;
|
||||
padding: 0.5rem 0.72rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.wire-popover[data-size="lg"] .wire-popover__trigger {
|
||||
min-height: 2.9rem;
|
||||
padding: 0.78rem 1.05rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.wire-popover__dismiss-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1120;
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.wire-popover__panel {
|
||||
position: absolute;
|
||||
z-index: 1121;
|
||||
top: calc(100% + 0.72rem);
|
||||
left: 0;
|
||||
width: 18rem;
|
||||
max-width: calc(100vw - 1.5rem);
|
||||
color: var(--wire-color-text);
|
||||
background:
|
||||
linear-gradient(145deg, color-mix(in srgb, var(--popover-accent) 5%, transparent), transparent 62%),
|
||||
color-mix(in srgb, var(--wire-color-surface-raised) 97%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--popover-accent) 18%, var(--wire-color-border));
|
||||
border-radius: 1rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 24px 64px color-mix(in srgb, black 22%, transparent);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.wire-popover[data-width="sm"] .wire-popover__panel {
|
||||
width: 14rem;
|
||||
}
|
||||
|
||||
.wire-popover[data-width="lg"] .wire-popover__panel {
|
||||
width: 24rem;
|
||||
}
|
||||
|
||||
.wire-popover[data-width="xl"] .wire-popover__panel {
|
||||
width: 32rem;
|
||||
}
|
||||
|
||||
.wire-popover[data-width="trigger"] .wire-popover__panel {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="bottom-end"] .wire-popover__panel {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="top-start"] .wire-popover__panel {
|
||||
top: auto;
|
||||
bottom: calc(100% + 0.72rem);
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="top-end"] .wire-popover__panel {
|
||||
top: auto;
|
||||
right: 0;
|
||||
bottom: calc(100% + 0.72rem);
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="left"] .wire-popover__panel {
|
||||
top: 50%;
|
||||
right: calc(100% + 0.72rem);
|
||||
left: auto;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="right"] .wire-popover__panel {
|
||||
top: 50%;
|
||||
left: calc(100% + 0.72rem);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.wire-popover[data-variant="soft"] .wire-popover__panel {
|
||||
background:
|
||||
linear-gradient(145deg, var(--popover-soft), transparent 70%),
|
||||
var(--wire-color-surface-raised);
|
||||
box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
|
||||
}
|
||||
|
||||
.wire-popover[data-variant="outline"] .wire-popover__panel {
|
||||
background: var(--wire-color-surface-raised);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-popover[data-variant="solid"] .wire-popover__panel {
|
||||
color: var(--popover-contrast);
|
||||
background: var(--popover-accent);
|
||||
border-color: color-mix(in srgb, white 20%, transparent);
|
||||
}
|
||||
|
||||
.wire-popover__arrow {
|
||||
position: absolute;
|
||||
top: -0.35rem;
|
||||
left: 1.25rem;
|
||||
width: 0.7rem;
|
||||
height: 0.7rem;
|
||||
background: inherit;
|
||||
border-top: 1px solid color-mix(in srgb, var(--popover-accent) 18%, var(--wire-color-border));
|
||||
border-left: 1px solid color-mix(in srgb, var(--popover-accent) 18%, var(--wire-color-border));
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="bottom-end"] .wire-popover__arrow {
|
||||
right: 1.25rem;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="top-start"] .wire-popover__arrow,
|
||||
.wire-popover[data-placement="top-end"] .wire-popover__arrow {
|
||||
top: auto;
|
||||
bottom: -0.35rem;
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="left"] .wire-popover__arrow {
|
||||
top: 50%;
|
||||
right: -0.35rem;
|
||||
left: auto;
|
||||
transform: translateY(-50%) rotate(135deg);
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="right"] .wire-popover__arrow {
|
||||
top: 50%;
|
||||
left: -0.35rem;
|
||||
transform: translateY(-50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
.wire-popover__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.8rem;
|
||||
padding: 1rem 1rem 0.8rem;
|
||||
border-bottom: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-popover__heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.7rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-popover__icon {
|
||||
flex: 0 0 auto;
|
||||
width: 1.05rem;
|
||||
height: 1.05rem;
|
||||
margin-top: 0.15rem;
|
||||
color: var(--popover-accent);
|
||||
}
|
||||
|
||||
.wire-popover[data-variant="solid"] .wire-popover__icon {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-popover__heading-copy {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-popover__heading-copy h3,
|
||||
.wire-popover__heading-copy p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wire-popover__heading-copy h3 {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.wire-popover__heading-copy p {
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.74rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.wire-popover[data-variant="solid"] .wire-popover__heading-copy p {
|
||||
color: color-mix(in srgb, currentColor 76%, transparent);
|
||||
}
|
||||
|
||||
.wire-popover__close {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
background: var(--wire-color-surface-soft);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.65rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wire-popover__body {
|
||||
padding: 1rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.wire-popover__footer {
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
|
||||
.wire-popover__action {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.45rem;
|
||||
min-height: 2.35rem;
|
||||
padding: 0.58rem 0.8rem;
|
||||
color: var(--popover-contrast);
|
||||
background: var(--popover-accent);
|
||||
border: 0;
|
||||
border-radius: 0.7rem;
|
||||
font: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 650;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-popover__panel {
|
||||
position: fixed;
|
||||
right: 0.75rem;
|
||||
bottom: 0.75rem;
|
||||
left: 0.75rem;
|
||||
top: auto;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.wire-popover__arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
component SplitHero {
|
||||
props {
|
||||
eyebrow = ""
|
||||
eyebrowIcon = "icon-[lucide--sparkles]"
|
||||
title = "A better digital experience"
|
||||
highlight = ""
|
||||
description = ""
|
||||
@@ -13,86 +14,804 @@ component SplitHero {
|
||||
visualPosition = "right"
|
||||
reverse = false
|
||||
ratio = "balanced"
|
||||
align = "left"
|
||||
size = "default"
|
||||
color = "primary"
|
||||
variant = "default"
|
||||
maxWidth = "xl"
|
||||
fullBleed = true
|
||||
backgroundImage = ""
|
||||
visualImage = ""
|
||||
visualAlt = ""
|
||||
visualIcon = ""
|
||||
visualEyebrow = ""
|
||||
visualTitle = ""
|
||||
visualDescription = ""
|
||||
visualItems = []
|
||||
visualStyle = "panel"
|
||||
trustItems = []
|
||||
class = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<Section
|
||||
spacing='{size === "compact" ? "md" : "xl"}'
|
||||
variant='{variant === "soft" ? "soft" : "default"}'
|
||||
color='{color}'
|
||||
class='overflow-hidden {class}'
|
||||
<section
|
||||
{...attrs}
|
||||
data-ui-component="SplitHero"
|
||||
data-position='{reverse || visualPosition === "left" ? "left" : "right"}'
|
||||
data-ratio='{ratio}'
|
||||
data-align='{align}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
data-max-width='{maxWidth}'
|
||||
data-full-bleed='{fullBleed ? "true" : "false"}'
|
||||
data-visual-style='{visualStyle}'
|
||||
class='wire-split-hero {class}'
|
||||
>
|
||||
<div
|
||||
data-ui-component="SplitHero"
|
||||
class="grid items-center gap-10 lg:grid-cols-2 lg:gap-16"
|
||||
class:lg:grid-cols-[1.15fr_0.85fr]='ratio === "content"'
|
||||
class:lg:grid-cols-[0.85fr_1.15fr]='ratio === "visual"'
|
||||
>
|
||||
<div
|
||||
class="max-w-3xl"
|
||||
class:lg:order-2='reverse || visualPosition === "left"'
|
||||
>
|
||||
{#if eyebrow}
|
||||
<div class="mb-6 inline-flex items-center gap-2 rounded-full border border-[var(--wire-color-primary-muted)] bg-[var(--wire-color-primary-soft)] px-4 py-2 text-sm font-semibold text-[var(--wire-color-primary)]">
|
||||
<span class="icon-[lucide--sparkles] size-4" aria-hidden="true"></span>
|
||||
<span>{eyebrow}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if backgroundImage}
|
||||
<img
|
||||
src='{backgroundImage}'
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="wire-split-hero__background-image"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<h1 class="text-4xl font-bold leading-[1.08] tracking-[-0.035em] text-[var(--wire-color-text)] sm:text-5xl lg:text-6xl">
|
||||
<span>{title}</span>
|
||||
{#if highlight}
|
||||
<span class="block text-[var(--wire-color-primary)]">{highlight}</span>
|
||||
<div class="wire-split-hero__glow wire-split-hero__glow--one" aria-hidden="true"></div>
|
||||
<div class="wire-split-hero__glow wire-split-hero__glow--two" aria-hidden="true"></div>
|
||||
|
||||
<div class="wire-split-hero__inner">
|
||||
<div class="wire-split-hero__layout">
|
||||
<div class="wire-split-hero__content">
|
||||
{#if eyebrow}
|
||||
<div class="wire-split-hero__eyebrow">
|
||||
{#if eyebrowIcon}
|
||||
<span class='wire-split-hero__eyebrow-icon {eyebrowIcon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{eyebrow}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</h1>
|
||||
|
||||
{#if description}
|
||||
<p class="mt-6 text-base leading-8 text-[var(--wire-color-text-muted)] sm:text-lg">{description}</p>
|
||||
{/if}
|
||||
<h1 class="wire-split-hero__title">
|
||||
<span>{title}</span>
|
||||
{#if highlight}
|
||||
<span class="wire-split-hero__highlight">{highlight}</span>
|
||||
{/if}
|
||||
</h1>
|
||||
|
||||
<HeroActions
|
||||
actions='{[
|
||||
{ label: primaryLabel, href: primaryHref, icon: primaryIcon, variant: "default", color: color },
|
||||
{ label: secondaryLabel, href: secondaryHref, icon: secondaryIcon, variant: "outline", color: color }
|
||||
]}'
|
||||
align="left"
|
||||
stackOnMobile="true"
|
||||
fullWidthMobile="true"
|
||||
size="lg"
|
||||
color='{color}'
|
||||
class="mt-8"
|
||||
>
|
||||
<slot name="actions"></slot>
|
||||
</HeroActions>
|
||||
{#if description}
|
||||
<p class="wire-split-hero__description">{description}</p>
|
||||
{/if}
|
||||
|
||||
{#if trustItems.length > 0}
|
||||
<div class="mt-8 flex flex-wrap gap-x-6 gap-y-3 text-sm text-[var(--wire-color-text-muted)]">
|
||||
{#each trustItems as item}
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span class='{(item.icon || "icon-[lucide--check-circle-2]") + " size-4 text-[var(--wire-color-primary)]"}' aria-hidden="true"></span>
|
||||
<span>{item.label || item.title || item}</span>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<slot></slot>
|
||||
|
||||
<slot name="trust"></slot>
|
||||
<slot></slot>
|
||||
</div>
|
||||
{#if primaryLabel || secondaryLabel}
|
||||
<div class="wire-split-hero__actions">
|
||||
{#if primaryLabel}
|
||||
<a href='{primaryHref || "#"}' class="wire-split-hero__action wire-split-hero__action--primary">
|
||||
{#if primaryIcon}
|
||||
<span class='wire-split-hero__action-icon {primaryIcon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{primaryLabel}</span>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="relative min-w-0"
|
||||
class:lg:order-1='reverse || visualPosition === "left"'
|
||||
>
|
||||
<div class="absolute -inset-8 -z-10 rounded-[2.5rem] bg-[var(--wire-color-primary-soft)] blur-2xl" aria-hidden="true"></div>
|
||||
<slot name="visual"></slot>
|
||||
{#if secondaryLabel}
|
||||
<a href='{secondaryHref || "#"}' class="wire-split-hero__action wire-split-hero__action--secondary">
|
||||
{#if secondaryIcon}
|
||||
<span class='wire-split-hero__action-icon {secondaryIcon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{secondaryLabel}</span>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="wire-split-hero__actions wire-split-hero__actions--slot">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if trustItems.length > 0}
|
||||
<div class="wire-split-hero__trust">
|
||||
{#each trustItems as item}
|
||||
<span class="wire-split-hero__trust-item">
|
||||
<span class='{item.icon || "icon-[lucide--check-circle-2]"}' aria-hidden="true"></span>
|
||||
<span>{item.label || item.title || item}</span>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<slot name="trust"></slot>
|
||||
</div>
|
||||
|
||||
<div class="wire-split-hero__visual-column">
|
||||
<slot name="visual"></slot>
|
||||
|
||||
{#if visualImage || visualIcon || visualEyebrow || visualTitle || visualDescription || visualItems.length > 0}
|
||||
<div class="wire-split-hero__visual">
|
||||
{#if visualImage}
|
||||
<div class="wire-split-hero__visual-media">
|
||||
<img src='{visualImage}' alt='{visualAlt}' class="wire-split-hero__visual-image" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if visualIcon || visualEyebrow || visualTitle || visualDescription}
|
||||
<div class="wire-split-hero__visual-header">
|
||||
{#if visualIcon}
|
||||
<div class="wire-split-hero__visual-icon" aria-hidden="true">
|
||||
<span class='{visualIcon}'></span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="wire-split-hero__visual-copy">
|
||||
{#if visualEyebrow}
|
||||
<p class="wire-split-hero__visual-eyebrow">{visualEyebrow}</p>
|
||||
{/if}
|
||||
|
||||
{#if visualTitle}
|
||||
<h2 class="wire-split-hero__visual-title">{visualTitle}</h2>
|
||||
{/if}
|
||||
|
||||
{#if visualDescription}
|
||||
<p class="wire-split-hero__visual-description">{visualDescription}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if visualItems.length > 0}
|
||||
<div class="wire-split-hero__visual-list">
|
||||
{#each visualItems as item}
|
||||
<div class="wire-split-hero__visual-item">
|
||||
{#if item.icon}
|
||||
<div class="wire-split-hero__visual-item-icon" aria-hidden="true">
|
||||
<span class='{item.icon}'></span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="wire-split-hero__visual-item-copy">
|
||||
<div class="wire-split-hero__visual-item-heading">
|
||||
<strong>{item.label || item.title}</strong>
|
||||
{#if item.value}
|
||||
<span>{item.value}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if item.description}
|
||||
<p>{item.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-split-hero {
|
||||
--split-accent: var(--wire-color-primary);
|
||||
--split-accent-hover: var(--wire-color-primary-hover);
|
||||
--split-accent-soft: var(--wire-color-primary-soft);
|
||||
--split-accent-muted: var(--wire-color-primary-muted);
|
||||
--split-contrast: var(--wire-color-primary-contrast);
|
||||
--split-border: color-mix(in srgb, var(--split-accent) 14%, var(--wire-color-border));
|
||||
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
color: var(--wire-color-text);
|
||||
background:
|
||||
radial-gradient(circle at 88% 18%, color-mix(in srgb, var(--split-accent) 11%, transparent), transparent 28%),
|
||||
var(--wire-color-background);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-color="secondary"] {
|
||||
--split-accent: var(--wire-color-secondary);
|
||||
--split-accent-hover: var(--wire-color-secondary-hover);
|
||||
--split-accent-soft: var(--wire-color-secondary-soft);
|
||||
--split-accent-muted: var(--wire-color-secondary-muted);
|
||||
--split-contrast: var(--wire-color-secondary-contrast);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-color="info"] {
|
||||
--split-accent: var(--wire-color-info);
|
||||
--split-accent-hover: var(--wire-color-info-hover);
|
||||
--split-accent-soft: var(--wire-color-info-soft);
|
||||
--split-accent-muted: var(--wire-color-info-muted);
|
||||
--split-contrast: var(--wire-color-info-contrast);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-color="success"] {
|
||||
--split-accent: var(--wire-color-success);
|
||||
--split-accent-hover: var(--wire-color-success-hover);
|
||||
--split-accent-soft: var(--wire-color-success-soft);
|
||||
--split-accent-muted: var(--wire-color-success-muted);
|
||||
--split-contrast: var(--wire-color-success-contrast);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-color="warning"] {
|
||||
--split-accent: var(--wire-color-warning);
|
||||
--split-accent-hover: var(--wire-color-warning-hover);
|
||||
--split-accent-soft: var(--wire-color-warning-soft);
|
||||
--split-accent-muted: var(--wire-color-warning-muted);
|
||||
--split-contrast: var(--wire-color-warning-contrast);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-color="danger"] {
|
||||
--split-accent: var(--wire-color-danger);
|
||||
--split-accent-hover: var(--wire-color-danger-hover);
|
||||
--split-accent-soft: var(--wire-color-danger-soft);
|
||||
--split-accent-muted: var(--wire-color-danger-muted);
|
||||
--split-contrast: var(--wire-color-danger-contrast);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-variant="soft"] {
|
||||
background:
|
||||
radial-gradient(circle at 84% 18%, color-mix(in srgb, var(--split-accent) 16%, transparent), transparent 30%),
|
||||
linear-gradient(135deg, var(--split-accent-soft), transparent 64%),
|
||||
var(--wire-color-background);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-variant="raised"] {
|
||||
background: var(--wire-color-surface-raised);
|
||||
border-block: 1px solid var(--split-border);
|
||||
box-shadow: 0 22px 70px color-mix(in srgb, black 14%, transparent);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-variant="outline"] {
|
||||
background: transparent;
|
||||
border-block: 1px solid var(--split-border);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-variant="gradient"] {
|
||||
background:
|
||||
radial-gradient(circle at 82% 16%, color-mix(in srgb, var(--split-accent) 28%, transparent), transparent 32%),
|
||||
linear-gradient(135deg, color-mix(in srgb, var(--split-accent) 13%, var(--wire-color-background)), var(--wire-color-background) 58%);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-variant="solid"] {
|
||||
color: var(--split-contrast);
|
||||
background:
|
||||
radial-gradient(circle at 82% 16%, color-mix(in srgb, white 14%, transparent), transparent 32%),
|
||||
var(--split-accent);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-full-bleed="false"] {
|
||||
width: min(calc(100% - 2rem), 90rem);
|
||||
margin-inline: auto;
|
||||
border: 1px solid var(--split-border);
|
||||
border-radius: 1.75rem;
|
||||
}
|
||||
|
||||
.wire-split-hero__background-image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 0.12;
|
||||
}
|
||||
|
||||
.wire-split-hero__glow {
|
||||
position: absolute;
|
||||
z-index: -2;
|
||||
width: 24rem;
|
||||
height: 24rem;
|
||||
pointer-events: none;
|
||||
background: var(--split-accent);
|
||||
border-radius: 999px;
|
||||
filter: blur(110px);
|
||||
opacity: 0.08;
|
||||
}
|
||||
|
||||
.wire-split-hero__glow--one {
|
||||
top: -11rem;
|
||||
right: -6rem;
|
||||
}
|
||||
|
||||
.wire-split-hero__glow--two {
|
||||
bottom: -14rem;
|
||||
left: -7rem;
|
||||
opacity: 0.05;
|
||||
}
|
||||
|
||||
.wire-split-hero__inner {
|
||||
width: min(calc(100% - 2rem), 80rem);
|
||||
margin-inline: auto;
|
||||
padding-block: 5rem;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-max-width="compact"] .wire-split-hero__inner {
|
||||
width: min(calc(100% - 2rem), 64rem);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-max-width="lg"] .wire-split-hero__inner {
|
||||
width: min(calc(100% - 2rem), 72rem);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-max-width="wide"] .wire-split-hero__inner,
|
||||
.wire-split-hero[data-max-width="2xl"] .wire-split-hero__inner {
|
||||
width: min(calc(100% - 2rem), 90rem);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-max-width="full"] .wire-split-hero__inner {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
padding-inline: 2rem;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-size="compact"] .wire-split-hero__inner,
|
||||
.wire-split-hero[data-size="sm"] .wire-split-hero__inner {
|
||||
padding-block: 3rem;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-size="lg"] .wire-split-hero__inner {
|
||||
padding-block: 6rem;
|
||||
}
|
||||
|
||||
.wire-split-hero__layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.wire-split-hero__content,
|
||||
.wire-split-hero__visual-column {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-split-hero__content {
|
||||
max-width: 45rem;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-align="center"] .wire-split-hero__content {
|
||||
margin-inline: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-align="right"] .wire-split-hero__content {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.wire-split-hero__eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-height: 2.25rem;
|
||||
padding: 0.45rem 0.8rem;
|
||||
color: var(--split-accent);
|
||||
background: var(--split-accent-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--split-accent) 22%, transparent);
|
||||
border-radius: 999px;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-variant="solid"] .wire-split-hero__eyebrow {
|
||||
color: currentColor;
|
||||
background: color-mix(in srgb, white 14%, transparent);
|
||||
border-color: color-mix(in srgb, white 22%, transparent);
|
||||
}
|
||||
|
||||
.wire-split-hero__eyebrow-icon {
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
}
|
||||
|
||||
.wire-split-hero__title {
|
||||
max-width: 15ch;
|
||||
margin: 1.45rem 0 0;
|
||||
color: var(--wire-color-text);
|
||||
font-size: clamp(2.35rem, 6vw, 4.8rem);
|
||||
font-weight: 650;
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-size="compact"] .wire-split-hero__title,
|
||||
.wire-split-hero[data-size="sm"] .wire-split-hero__title {
|
||||
font-size: clamp(2rem, 5vw, 3.4rem);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-size="lg"] .wire-split-hero__title {
|
||||
font-size: clamp(2.75rem, 7vw, 5.5rem);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-align="center"] .wire-split-hero__title,
|
||||
.wire-split-hero[data-align="right"] .wire-split-hero__title {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-align="right"] .wire-split-hero__title {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-variant="solid"] .wire-split-hero__title {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-split-hero__highlight {
|
||||
display: block;
|
||||
color: var(--split-accent);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-variant="solid"] .wire-split-hero__highlight {
|
||||
color: currentColor;
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.wire-split-hero__description {
|
||||
max-width: 42rem;
|
||||
margin: 1.4rem 0 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: clamp(0.96rem, 1.8vw, 1.1rem);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-align="center"] .wire-split-hero__description,
|
||||
.wire-split-hero[data-align="right"] .wire-split-hero__description {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-align="right"] .wire-split-hero__description {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-variant="solid"] .wire-split-hero__description {
|
||||
color: color-mix(in srgb, currentColor 82%, transparent);
|
||||
}
|
||||
|
||||
.wire-split-hero__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.wire-split-hero__actions--slot:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-align="center"] .wire-split-hero__actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-align="right"] .wire-split-hero__actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.wire-split-hero__action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.55rem;
|
||||
min-height: 2.85rem;
|
||||
padding: 0.78rem 1.1rem;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
transform 160ms ease,
|
||||
background-color 160ms ease,
|
||||
border-color 160ms ease,
|
||||
color 160ms ease,
|
||||
box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.wire-split-hero__action:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.wire-split-hero__action--primary {
|
||||
color: var(--split-contrast);
|
||||
background: var(--split-accent);
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 10px 26px color-mix(in srgb, var(--split-accent) 22%, transparent);
|
||||
}
|
||||
|
||||
.wire-split-hero__action--primary:hover {
|
||||
background: var(--split-accent-hover);
|
||||
}
|
||||
|
||||
.wire-split-hero__action--secondary {
|
||||
color: var(--split-accent);
|
||||
background: transparent;
|
||||
border: 1px solid color-mix(in srgb, var(--split-accent) 42%, var(--wire-color-border));
|
||||
}
|
||||
|
||||
.wire-split-hero__action--secondary:hover {
|
||||
background: var(--split-accent-soft);
|
||||
border-color: var(--split-accent);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-variant="solid"] .wire-split-hero__action--primary {
|
||||
color: var(--split-accent);
|
||||
background: var(--split-contrast);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-variant="solid"] .wire-split-hero__action--secondary {
|
||||
color: currentColor;
|
||||
border-color: color-mix(in srgb, white 46%, transparent);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-variant="solid"] .wire-split-hero__action--secondary:hover {
|
||||
background: color-mix(in srgb, white 12%, transparent);
|
||||
border-color: color-mix(in srgb, white 68%, transparent);
|
||||
}
|
||||
|
||||
.wire-split-hero__action-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.wire-split-hero__action:focus-visible {
|
||||
outline: 2px solid var(--wire-color-focus);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.wire-split-hero__trust {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem 1.2rem;
|
||||
margin-top: 1.7rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-align="center"] .wire-split-hero__trust {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-align="right"] .wire-split-hero__trust {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-variant="solid"] .wire-split-hero__trust {
|
||||
color: color-mix(in srgb, currentColor 80%, transparent);
|
||||
}
|
||||
|
||||
.wire-split-hero__trust-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.42rem;
|
||||
}
|
||||
|
||||
.wire-split-hero__trust-item > span:first-child {
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
color: var(--split-accent);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-variant="solid"] .wire-split-hero__trust-item > span:first-child {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-column {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-column::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 8% -5% -4% 8%;
|
||||
z-index: -1;
|
||||
background: var(--split-accent-soft);
|
||||
border-radius: 2rem;
|
||||
filter: blur(2px);
|
||||
transform: rotate(2deg);
|
||||
}
|
||||
|
||||
.wire-split-hero__visual {
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(145deg, color-mix(in srgb, var(--split-accent) 7%, transparent), transparent 58%),
|
||||
var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--split-border);
|
||||
border-radius: 1.5rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 30px 76px color-mix(in srgb, black 18%, transparent);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-visual-style="plain"] .wire-split-hero__visual {
|
||||
overflow: visible;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-visual-style="soft"] .wire-split-hero__visual {
|
||||
background:
|
||||
linear-gradient(145deg, var(--split-accent-soft), transparent 72%),
|
||||
var(--wire-color-surface-raised);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-variant="solid"] .wire-split-hero__visual {
|
||||
color: var(--wire-color-text);
|
||||
background: var(--wire-color-surface-raised);
|
||||
border-color: color-mix(in srgb, white 24%, transparent);
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-media {
|
||||
position: relative;
|
||||
aspect-ratio: 16 / 10;
|
||||
overflow: hidden;
|
||||
background: var(--wire-color-surface-soft);
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-icon,
|
||||
.wire-split-hero__visual-item-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
width: 2.8rem;
|
||||
height: 2.8rem;
|
||||
color: var(--split-accent);
|
||||
background: var(--split-accent-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--split-accent) 20%, transparent);
|
||||
border-radius: 0.9rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-copy,
|
||||
.wire-split-hero__visual-item-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-eyebrow {
|
||||
margin: 0 0 0.35rem;
|
||||
color: var(--split-accent);
|
||||
font-size: 0.69rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.11em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-title {
|
||||
margin: 0;
|
||||
color: var(--wire-color-text);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-description {
|
||||
margin: 0.5rem 0 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-list {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
border-top: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 0.85rem;
|
||||
padding: 1.1rem 1.5rem;
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-item + .wire-split-hero__visual-item {
|
||||
border-top: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-item-icon {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 0.72rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-item-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
color: var(--wire-color-text);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-item-heading > span {
|
||||
color: var(--split-accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-item p {
|
||||
margin: 0.35rem 0 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.wire-split-hero__layout {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 4.5rem;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-ratio="content"] .wire-split-hero__layout {
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-ratio="visual"] .wire-split-hero__layout {
|
||||
grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
|
||||
}
|
||||
|
||||
.wire-split-hero[data-position="left"] .wire-split-hero__content {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.wire-split-hero[data-position="left"] .wire-split-hero__visual-column {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-split-hero__inner {
|
||||
padding-block: 3.25rem;
|
||||
}
|
||||
|
||||
.wire-split-hero__actions {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.wire-split-hero__action {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wire-split-hero__visual-header,
|
||||
.wire-split-hero__visual-item {
|
||||
padding-inline: 1.15rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-split-hero__action {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.wire-split-hero__action:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,28 +13,472 @@ component StatsBar {
|
||||
}
|
||||
|
||||
view {
|
||||
<Section
|
||||
spacing='{compact ? "sm" : "md"}'
|
||||
variant='{variant}'
|
||||
color='{color}'
|
||||
maxWidth='{maxWidth}'
|
||||
borderTop="true"
|
||||
borderBottom="true"
|
||||
class='{class}'
|
||||
<section
|
||||
{...attrs}
|
||||
data-ui-component="StatsBar"
|
||||
data-columns='{columns}'
|
||||
data-compact='{compact ? "true" : "false"}'
|
||||
data-dividers='{dividers ? "true" : "false"}'
|
||||
data-icons='{icons ? "true" : "false"}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
data-max-width='{maxWidth}'
|
||||
class='wire-stats-bar {class}'
|
||||
aria-label="Key statistics"
|
||||
>
|
||||
<div data-ui-component="StatsBar">
|
||||
<MetricGrid
|
||||
items='{items.map((item) => ({ ...item, icon: icons ? item.icon : "", variant: "minimal", size: compact ? "sm" : size }))}'
|
||||
columns='{columns}'
|
||||
tabletColumns="2"
|
||||
mobileColumns="1"
|
||||
gap='{compact ? "sm" : "md"}'
|
||||
dividers='{dividers}'
|
||||
size='{compact ? "sm" : size}'
|
||||
color='{color}'
|
||||
variant="minimal"
|
||||
/>
|
||||
<div class="wire-stats-bar__inner">
|
||||
<div class="wire-stats-bar__surface">
|
||||
{#each items as item, itemIndex}
|
||||
<article
|
||||
class="wire-stats-bar__item"
|
||||
data-color='{item.color || color}'
|
||||
data-index='{itemIndex}'
|
||||
>
|
||||
<div class="wire-stats-bar__content">
|
||||
{#if item.label}
|
||||
<p class="wire-stats-bar__label">{item.label}</p>
|
||||
{/if}
|
||||
|
||||
<div class="wire-stats-bar__value-row">
|
||||
<strong class="wire-stats-bar__value">{item.value}</strong>
|
||||
|
||||
{#if item.suffix}
|
||||
<span class="wire-stats-bar__suffix">{item.suffix}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if item.description}
|
||||
<p class="wire-stats-bar__description">{item.description}</p>
|
||||
{/if}
|
||||
|
||||
{#if item.trend || item.trendLabel}
|
||||
<div
|
||||
class="wire-stats-bar__trend"
|
||||
data-direction='{item.trendDirection || "neutral"}'
|
||||
>
|
||||
{#if item.trendDirection === "up"}
|
||||
<span
|
||||
class="icon-[lucide--trending-up] wire-stats-bar__trend-icon"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{:else if item.trendDirection === "down"}
|
||||
<span
|
||||
class="icon-[lucide--trending-down] wire-stats-bar__trend-icon"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{:else}
|
||||
<span
|
||||
class="icon-[lucide--minus] wire-stats-bar__trend-icon"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{/if}
|
||||
|
||||
{#if item.trend}
|
||||
<span class="wire-stats-bar__trend-value">{item.trend}</span>
|
||||
{/if}
|
||||
|
||||
{#if item.trendLabel}
|
||||
<span class="wire-stats-bar__trend-label">{item.trendLabel}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if icons && item.icon}
|
||||
<div class="wire-stats-bar__icon" aria-hidden="true">
|
||||
<span class='{item.icon}'></span>
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-stats-bar {
|
||||
--stats-accent: var(--wire-color-primary);
|
||||
--stats-contrast: var(--wire-color-primary-contrast);
|
||||
--stats-soft: color-mix(in srgb, var(--stats-accent) 12%, transparent);
|
||||
--stats-border: color-mix(in srgb, var(--stats-accent) 22%, var(--wire-color-border));
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
color: var(--wire-color-text);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-color="secondary"] {
|
||||
--stats-accent: var(--wire-color-secondary);
|
||||
--stats-contrast: var(--wire-color-secondary-contrast);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-color="info"] {
|
||||
--stats-accent: var(--wire-color-info);
|
||||
--stats-contrast: var(--wire-color-info-contrast);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-color="success"] {
|
||||
--stats-accent: var(--wire-color-success);
|
||||
--stats-contrast: var(--wire-color-success-contrast);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-color="warning"] {
|
||||
--stats-accent: var(--wire-color-warning);
|
||||
--stats-contrast: var(--wire-color-warning-contrast);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-color="danger"] {
|
||||
--stats-accent: var(--wire-color-danger);
|
||||
--stats-contrast: var(--wire-color-danger-contrast);
|
||||
}
|
||||
|
||||
.wire-stats-bar__inner {
|
||||
width: min(calc(100% - 2rem), 80rem);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-max-width="compact"] .wire-stats-bar__inner {
|
||||
width: min(calc(100% - 2rem), 64rem);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-max-width="lg"] .wire-stats-bar__inner {
|
||||
width: min(calc(100% - 2rem), 72rem);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-max-width="wide"] .wire-stats-bar__inner,
|
||||
.wire-stats-bar[data-max-width="2xl"] .wire-stats-bar__inner {
|
||||
width: min(calc(100% - 2rem), 90rem);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-max-width="full"] .wire-stats-bar__inner {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.wire-stats-bar__surface {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--stats-accent) 7%, transparent),
|
||||
transparent 42%
|
||||
),
|
||||
var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--stats-border);
|
||||
border-radius: 1.25rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 18px 48px color-mix(in srgb, black 16%, transparent);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-variant="default"] .wire-stats-bar__surface {
|
||||
background: var(--wire-color-surface);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-variant="soft"] .wire-stats-bar__surface {
|
||||
background:
|
||||
linear-gradient(135deg, var(--stats-soft), transparent 58%),
|
||||
var(--wire-color-surface-raised);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-variant="outline"] .wire-stats-bar__surface {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-variant="minimal"] .wire-stats-bar__surface {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-variant="solid"] .wire-stats-bar__surface {
|
||||
color: var(--stats-contrast);
|
||||
background:
|
||||
linear-gradient(135deg, color-mix(in srgb, white 8%, transparent), transparent 52%),
|
||||
var(--stats-accent);
|
||||
border-color: color-mix(in srgb, white 24%, transparent);
|
||||
box-shadow: 0 20px 56px color-mix(in srgb, var(--stats-accent) 28%, transparent);
|
||||
}
|
||||
|
||||
.wire-stats-bar__item {
|
||||
--item-accent: var(--stats-accent);
|
||||
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
gap: 1rem;
|
||||
min-width: 0;
|
||||
padding: 1.5rem;
|
||||
transition:
|
||||
background-color 180ms ease,
|
||||
transform 180ms ease;
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-compact="true"] .wire-stats-bar__item {
|
||||
padding: 1.15rem 1.25rem;
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-size="sm"] .wire-stats-bar__item {
|
||||
padding: 1rem 1.1rem;
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-size="lg"] .wire-stats-bar__item {
|
||||
padding: 1.8rem;
|
||||
}
|
||||
|
||||
.wire-stats-bar__item[data-color="secondary"] {
|
||||
--item-accent: var(--wire-color-secondary);
|
||||
}
|
||||
|
||||
.wire-stats-bar__item[data-color="info"] {
|
||||
--item-accent: var(--wire-color-info);
|
||||
}
|
||||
|
||||
.wire-stats-bar__item[data-color="success"] {
|
||||
--item-accent: var(--wire-color-success);
|
||||
}
|
||||
|
||||
.wire-stats-bar__item[data-color="warning"] {
|
||||
--item-accent: var(--wire-color-warning);
|
||||
}
|
||||
|
||||
.wire-stats-bar__item[data-color="danger"] {
|
||||
--item-accent: var(--wire-color-danger);
|
||||
}
|
||||
|
||||
.wire-stats-bar__item:hover {
|
||||
background: color-mix(in srgb, var(--item-accent) 6%, transparent);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-variant="solid"] .wire-stats-bar__item:hover {
|
||||
background: color-mix(in srgb, white 8%, transparent);
|
||||
}
|
||||
|
||||
.wire-stats-bar__content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-stats-bar__label {
|
||||
margin: 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.wire-stats-bar__value-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.45rem;
|
||||
}
|
||||
|
||||
.wire-stats-bar__value {
|
||||
margin: 0;
|
||||
color: var(--wire-color-text);
|
||||
font-size: clamp(1.75rem, 2.4vw, 2.35rem);
|
||||
font-weight: 650;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.035em;
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-compact="true"] .wire-stats-bar__value {
|
||||
font-size: clamp(1.45rem, 2vw, 1.9rem);
|
||||
}
|
||||
|
||||
.wire-stats-bar__suffix {
|
||||
color: var(--item-accent);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wire-stats-bar__description {
|
||||
max-width: 30rem;
|
||||
margin: 0.7rem 0 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-compact="true"] .wire-stats-bar__description {
|
||||
margin-top: 0.55rem;
|
||||
font-size: 0.73rem;
|
||||
}
|
||||
|
||||
.wire-stats-bar__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.65rem;
|
||||
height: 2.65rem;
|
||||
color: var(--item-accent);
|
||||
background: color-mix(in srgb, var(--item-accent) 11%, transparent);
|
||||
border-radius: 0.85rem;
|
||||
}
|
||||
|
||||
.wire-stats-bar__icon > span {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-compact="true"] .wire-stats-bar__icon {
|
||||
width: 2.35rem;
|
||||
height: 2.35rem;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.wire-stats-bar__trend {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
width: fit-content;
|
||||
margin-top: 0.7rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.7rem;
|
||||
line-height: 1;
|
||||
background: color-mix(in srgb, var(--wire-color-text) 5%, transparent);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.wire-stats-bar__trend[data-direction="up"] {
|
||||
color: var(--wire-color-success);
|
||||
}
|
||||
|
||||
.wire-stats-bar__trend[data-direction="down"] {
|
||||
color: var(--wire-color-danger);
|
||||
}
|
||||
|
||||
.wire-stats-bar__trend-icon {
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
}
|
||||
|
||||
.wire-stats-bar__trend-value {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wire-stats-bar__trend-label {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-variant="solid"] .wire-stats-bar__label,
|
||||
.wire-stats-bar[data-variant="solid"] .wire-stats-bar__description,
|
||||
.wire-stats-bar[data-variant="solid"] .wire-stats-bar__value,
|
||||
.wire-stats-bar[data-variant="solid"] .wire-stats-bar__suffix {
|
||||
color: var(--stats-contrast);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-variant="solid"] .wire-stats-bar__label,
|
||||
.wire-stats-bar[data-variant="solid"] .wire-stats-bar__description {
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-variant="solid"] .wire-stats-bar__icon {
|
||||
color: var(--stats-contrast);
|
||||
background: color-mix(in srgb, white 13%, transparent);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-dividers="true"] .wire-stats-bar__item + .wire-stats-bar__item {
|
||||
border-top: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-variant="solid"][data-dividers="true"]
|
||||
.wire-stats-bar__item + .wire-stats-bar__item {
|
||||
border-color: color-mix(in srgb, white 18%, transparent);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.wire-stats-bar__surface {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-columns="1"] .wire-stats-bar__surface {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-dividers="true"] .wire-stats-bar__item {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-dividers="true"] .wire-stats-bar__item:nth-child(even) {
|
||||
border-left: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-dividers="true"] .wire-stats-bar__item:nth-child(n + 3) {
|
||||
border-top: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-variant="solid"][data-dividers="true"]
|
||||
.wire-stats-bar__item:nth-child(even),
|
||||
.wire-stats-bar[data-variant="solid"][data-dividers="true"]
|
||||
.wire-stats-bar__item:nth-child(n + 3) {
|
||||
border-color: color-mix(in srgb, white 18%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.wire-stats-bar[data-columns="1"] .wire-stats-bar__surface {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-columns="2"] .wire-stats-bar__surface {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-columns="3"] .wire-stats-bar__surface {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-columns="4"] .wire-stats-bar__surface {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-columns="5"] .wire-stats-bar__surface {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-columns="6"] .wire-stats-bar__surface {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-dividers="true"] .wire-stats-bar__item,
|
||||
.wire-stats-bar[data-dividers="true"] .wire-stats-bar__item:nth-child(even),
|
||||
.wire-stats-bar[data-dividers="true"] .wire-stats-bar__item:nth-child(n + 3) {
|
||||
border-top: 0;
|
||||
border-left: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-dividers="true"] .wire-stats-bar__item:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.wire-stats-bar[data-variant="solid"][data-dividers="true"] .wire-stats-bar__item {
|
||||
border-color: color-mix(in srgb, white 18%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-stats-bar[data-max-width="full"] .wire-stats-bar__surface {
|
||||
border-inline: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-stats-bar__item {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,333 @@
|
||||
component Tooltip {
|
||||
props {
|
||||
@event toggle = function
|
||||
@event open = function
|
||||
@event close = function
|
||||
|
||||
id = ""
|
||||
open = false
|
||||
defaultOpen = false
|
||||
title = ""
|
||||
description = ""
|
||||
content = "Tooltip"
|
||||
trigger = "hover"
|
||||
placement = "top"
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Tooltip"
|
||||
description = ""
|
||||
open = false
|
||||
placement = "bottom"
|
||||
closeLabel = "Close"
|
||||
variant = "dark"
|
||||
maxWidth = "18rem"
|
||||
offset = 10
|
||||
showArrow = true
|
||||
interactive = false
|
||||
disabled = false
|
||||
class = ""
|
||||
}
|
||||
|
||||
state visible = defaultOpen
|
||||
|
||||
functions {
|
||||
function isOpen() {
|
||||
return open || visible
|
||||
}
|
||||
|
||||
function showTooltip(reason, sourceEvent) {
|
||||
if (disabled || trigger === "manual") {
|
||||
return
|
||||
}
|
||||
visible = true
|
||||
$emit("open", { reason: reason, sourceEvent: sourceEvent })
|
||||
$emit("toggle", { open: true, reason: reason, sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
function hideTooltip(reason, sourceEvent) {
|
||||
if (trigger === "manual") {
|
||||
return
|
||||
}
|
||||
visible = false
|
||||
$emit("close", { reason: reason, sourceEvent: sourceEvent })
|
||||
$emit("toggle", { open: false, reason: reason, sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
function toggleTooltip(sourceEvent) {
|
||||
if (isOpen()) {
|
||||
hideTooltip("click", sourceEvent)
|
||||
} else {
|
||||
showTooltip("click", sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(sourceEvent) {
|
||||
if (sourceEvent.key === "Escape") {
|
||||
sourceEvent.preventDefault()
|
||||
hideTooltip("escape", sourceEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div data-show="{open}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--tooltip wire-next--placement-{placement} {class}" role="region" aria-modal="false" aria-label="{title}">
|
||||
<header><strong>{title}</strong><button type="button" aria-label="{closeLabel}">×</button></header>
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
<slot />
|
||||
</div>
|
||||
<span
|
||||
{...attrs}
|
||||
data-ui-component="Tooltip"
|
||||
data-open='{open || visible ? "true" : "false"}'
|
||||
data-trigger='{trigger}'
|
||||
data-placement='{placement}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
data-interactive='{interactive ? "true" : "false"}'
|
||||
class='wire-tooltip {class}'
|
||||
style='--wire-tooltip-max-width:{maxWidth};--wire-tooltip-offset:{offset}px;'
|
||||
@mouseenter='if (trigger === "hover" || trigger === "both") { showTooltip("hover", event) }'
|
||||
@mouseleave='if (trigger === "hover" || trigger === "both") { hideTooltip("hover", event) }'
|
||||
@focusin='if (trigger === "focus" || trigger === "hover" || trigger === "both") { showTooltip("focus", event) }'
|
||||
@focusout='if (trigger === "focus" || trigger === "hover" || trigger === "both") { hideTooltip("focus", event) }'
|
||||
@keydown='handleKeydown(event)'
|
||||
>
|
||||
<span
|
||||
class="wire-tooltip__trigger"
|
||||
tabindex='{disabled ? "-1" : "0"}'
|
||||
aria-describedby='{(open || visible) && id ? id : ""}'
|
||||
@click='if (trigger === "click" || trigger === "both") { toggleTooltip(event) }'
|
||||
>
|
||||
<slot name="trigger"></slot>
|
||||
</span>
|
||||
|
||||
<span
|
||||
id='{id}'
|
||||
class="wire-tooltip__content"
|
||||
data-show='{open || visible}'
|
||||
role="tooltip"
|
||||
>
|
||||
{#if showArrow}
|
||||
<span class="wire-tooltip__arrow" aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
{#if title}
|
||||
<strong>{title}</strong>
|
||||
{/if}
|
||||
|
||||
{#if description}
|
||||
<span class="wire-tooltip__description">{description}</span>
|
||||
{:else if content}
|
||||
<span class="wire-tooltip__description">{content}</span>
|
||||
{/if}
|
||||
|
||||
<slot></slot>
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-tooltip {
|
||||
--tooltip-accent: var(--wire-color-primary);
|
||||
--tooltip-soft: var(--wire-color-primary-soft);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.wire-tooltip[data-color="secondary"] {
|
||||
--tooltip-accent: var(--wire-color-secondary);
|
||||
--tooltip-soft: var(--wire-color-secondary-soft);
|
||||
}
|
||||
|
||||
.wire-tooltip[data-color="info"] {
|
||||
--tooltip-accent: var(--wire-color-info);
|
||||
--tooltip-soft: var(--wire-color-info-soft);
|
||||
}
|
||||
|
||||
.wire-tooltip[data-color="success"] {
|
||||
--tooltip-accent: var(--wire-color-success);
|
||||
--tooltip-soft: var(--wire-color-success-soft);
|
||||
}
|
||||
|
||||
.wire-tooltip[data-color="warning"] {
|
||||
--tooltip-accent: var(--wire-color-warning);
|
||||
--tooltip-soft: var(--wire-color-warning-soft);
|
||||
}
|
||||
|
||||
.wire-tooltip[data-color="danger"] {
|
||||
--tooltip-accent: var(--wire-color-danger);
|
||||
--tooltip-soft: var(--wire-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-tooltip__trigger {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-tooltip__trigger:focus-visible {
|
||||
border-radius: 0.4rem;
|
||||
outline: 2px solid var(--wire-color-focus);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.wire-tooltip__content {
|
||||
position: absolute;
|
||||
z-index: 1300;
|
||||
left: 50%;
|
||||
bottom: calc(100% + var(--wire-tooltip-offset));
|
||||
display: grid;
|
||||
gap: 0.22rem;
|
||||
width: max-content;
|
||||
max-width: min(var(--wire-tooltip-max-width), calc(100vw - 1rem));
|
||||
padding: 0.58rem 0.72rem;
|
||||
color: white;
|
||||
background: color-mix(in srgb, black 88%, var(--tooltip-accent) 12%);
|
||||
border: 1px solid color-mix(in srgb, white 12%, transparent);
|
||||
border-radius: 0.65rem;
|
||||
box-shadow: 0 14px 38px color-mix(in srgb, black 28%, transparent);
|
||||
font-size: 0.73rem;
|
||||
line-height: 1.45;
|
||||
text-align: left;
|
||||
transform: translateX(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wire-tooltip[data-interactive="true"] .wire-tooltip__content {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.wire-tooltip[data-variant="soft"] .wire-tooltip__content {
|
||||
color: var(--wire-color-text);
|
||||
background: var(--tooltip-soft);
|
||||
border-color: color-mix(in srgb, var(--tooltip-accent) 24%, var(--wire-color-border));
|
||||
}
|
||||
|
||||
.wire-tooltip[data-variant="light"] .wire-tooltip__content {
|
||||
color: var(--wire-color-text);
|
||||
background: var(--wire-color-surface-raised);
|
||||
border-color: var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-tooltip[data-variant="solid"] .wire-tooltip__content {
|
||||
color: var(--wire-color-primary-contrast);
|
||||
background: var(--tooltip-accent);
|
||||
border-color: color-mix(in srgb, white 18%, transparent);
|
||||
}
|
||||
|
||||
.wire-tooltip__content strong {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.wire-tooltip__description {
|
||||
color: color-mix(in srgb, currentColor 82%, transparent);
|
||||
}
|
||||
|
||||
.wire-tooltip__arrow {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -0.32rem;
|
||||
width: 0.62rem;
|
||||
height: 0.62rem;
|
||||
background: inherit;
|
||||
border-right: 1px solid color-mix(in srgb, white 10%, transparent);
|
||||
border-bottom: 1px solid color-mix(in srgb, white 10%, transparent);
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.wire-tooltip[data-placement="bottom"] .wire-tooltip__content {
|
||||
top: calc(100% + var(--wire-tooltip-offset));
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.wire-tooltip[data-placement="bottom"] .wire-tooltip__arrow {
|
||||
top: -0.32rem;
|
||||
bottom: auto;
|
||||
transform: translateX(-50%) rotate(225deg);
|
||||
}
|
||||
|
||||
.wire-tooltip[data-placement="left"] .wire-tooltip__content {
|
||||
top: 50%;
|
||||
right: calc(100% + var(--wire-tooltip-offset));
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.wire-tooltip[data-placement="left"] .wire-tooltip__arrow {
|
||||
top: 50%;
|
||||
right: -0.32rem;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
transform: translateY(-50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
.wire-tooltip[data-placement="right"] .wire-tooltip__content {
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
left: calc(100% + var(--wire-tooltip-offset));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.wire-tooltip[data-placement="right"] .wire-tooltip__arrow {
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
left: -0.32rem;
|
||||
transform: translateY(-50%) rotate(135deg);
|
||||
}
|
||||
|
||||
.wire-tooltip[data-placement="top-start"] .wire-tooltip__content,
|
||||
.wire-tooltip[data-placement="bottom-start"] .wire-tooltip__content {
|
||||
left: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.wire-tooltip[data-placement="top-start"] .wire-tooltip__arrow,
|
||||
.wire-tooltip[data-placement="bottom-start"] .wire-tooltip__arrow {
|
||||
left: 1rem;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.wire-tooltip[data-placement="bottom-start"] .wire-tooltip__arrow {
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
|
||||
.wire-tooltip[data-placement="top-end"] .wire-tooltip__content,
|
||||
.wire-tooltip[data-placement="bottom-end"] .wire-tooltip__content {
|
||||
right: 0;
|
||||
left: auto;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.wire-tooltip[data-placement="top-end"] .wire-tooltip__arrow,
|
||||
.wire-tooltip[data-placement="bottom-end"] .wire-tooltip__arrow {
|
||||
right: 1rem;
|
||||
left: auto;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.wire-tooltip[data-placement="bottom-end"] .wire-tooltip__arrow {
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
|
||||
.wire-tooltip[data-size="sm"] .wire-tooltip__content {
|
||||
padding: 0.45rem 0.58rem;
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
|
||||
.wire-tooltip[data-size="lg"] .wire-tooltip__content {
|
||||
padding: 0.72rem 0.85rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-tooltip__content {
|
||||
position: fixed;
|
||||
right: 0.75rem;
|
||||
bottom: 0.75rem;
|
||||
left: 0.75rem;
|
||||
top: auto;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.wire-tooltip__arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,11 +80,25 @@ test("component reference contains only explicitly declared public events", () =
|
||||
) as { components: Array<{ name: string; events: string[] }> };
|
||||
|
||||
for (const component of reference.components) {
|
||||
const source = readFileSync(uiComponentPath(component.name), "utf8");
|
||||
const componentPath = uiComponentPath(component.name);
|
||||
const source = readFileSync(componentPath, "utf8");
|
||||
|
||||
const declaredEvents = [
|
||||
...source.matchAll(/@event\s+([A-Za-z][A-Za-z0-9_]*)\s*=\s*function/g),
|
||||
].map((match) => match[1]);
|
||||
expect(component.events).toEqual([...new Set(declaredEvents)]);
|
||||
|
||||
const expectedEvents = [...new Set(declaredEvents)];
|
||||
|
||||
if (JSON.stringify(component.events) !== JSON.stringify(expectedEvents)) {
|
||||
console.error("UI EVENT REFERENCE MISMATCH", {
|
||||
component: component.name,
|
||||
componentPath,
|
||||
referenceEvents: component.events,
|
||||
declaredEvents: expectedEvents,
|
||||
});
|
||||
}
|
||||
|
||||
expect(component.events).toEqual(expectedEvents);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user