release: WRNexusJS 0.5.13
This commit is contained in:
@@ -841,7 +841,7 @@ 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`, `copyright-left`, `copyright-right`, `post-footer`
|
||||
- Slots: `pre-footer`, `post-footer`, `copyright-left`, `copyright-right`
|
||||
- Events: `select`, `action`
|
||||
|
||||
### MegaMenu
|
||||
|
||||
@@ -4534,7 +4534,7 @@
|
||||
"default": "\"\""
|
||||
}
|
||||
],
|
||||
"slots": ["pre-footer", "copyright-left", "copyright-right", "post-footer"],
|
||||
"slots": ["pre-footer", "post-footer", "copyright-left", "copyright-right"],
|
||||
"events": ["select", "action"],
|
||||
"source": "components/Footer.wrn"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ component Footer {
|
||||
props {
|
||||
@event select = function
|
||||
@event action = function
|
||||
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Footer navigation"
|
||||
@@ -18,127 +19,265 @@ component Footer {
|
||||
data-ui-component="Footer"
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
class='wire-footer border-t border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-text)] {class}'
|
||||
class='wire-footer {maxWidth === "full" ? "wire-footer--width-full" : ""} {class}'
|
||||
>
|
||||
<slot name="pre-footer"></slot>
|
||||
|
||||
<div
|
||||
class="wire-footer__inner mx-auto w-full px-4 py-12 sm:px-6 lg:px-8"
|
||||
class:max-w-5xl='maxWidth === "compact"'
|
||||
class:max-w-7xl='maxWidth === "wide" || maxWidth === "xl"'
|
||||
class:max-w-screen-2xl='maxWidth === "full"'
|
||||
class:py-8='size === "compact"'
|
||||
class:py-16='size === "spacious"'
|
||||
class="wire-footer__grid"
|
||||
>
|
||||
<nav aria-label='{label}'>
|
||||
<div class='wire-footer__columns wire-footer--columns-{columns}'>
|
||||
<div
|
||||
class="wire-footer__pre"
|
||||
>
|
||||
<slot
|
||||
name="pre-footer"
|
||||
>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
{#if items.length > 0}
|
||||
<nav
|
||||
class='wire-footer__links wire-footer--columns-{columns}'
|
||||
aria-label='{label}'
|
||||
>
|
||||
{#each items as item, itemIndex}
|
||||
{#if item.type === "header"}
|
||||
<section class="wire-footer__column" aria-labelledby='footer-heading-{itemIndex}'>
|
||||
<h2 id='footer-heading-{itemIndex}' class="wire-footer__heading">
|
||||
<section
|
||||
class="wire-footer__column"
|
||||
aria-labelledby='footer-heading-{itemIndex}'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span
|
||||
class='{item.icon}'
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<h2
|
||||
id='footer-heading-{itemIndex}'
|
||||
class="wire-footer__heading"
|
||||
>
|
||||
{item.label || item.title}
|
||||
</h2>
|
||||
|
||||
{#if item.description}
|
||||
<p class="wire-footer__description">{item.description}</p>
|
||||
<p>
|
||||
{item.description}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if (item.items || item.links || []).length > 0}
|
||||
<ul class="wire-footer__column-links">
|
||||
<div
|
||||
class="wire-footer__column-links"
|
||||
>
|
||||
{#each item.items || item.links || [] as child, childIndex}
|
||||
<li>
|
||||
<a
|
||||
href='{child.href || "#"}'
|
||||
target='{child.target || ""}'
|
||||
rel='{child.external ? "noopener noreferrer" : (child.rel || "")}'
|
||||
class="wire-footer__link"
|
||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex } })); child.action && event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex } }))'
|
||||
>
|
||||
{#if child.icon}
|
||||
<span class='{child.icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{child.label || child.title}</span>
|
||||
{#if child.external}
|
||||
<span class="icon-[lucide--external-link]" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</section>
|
||||
{:else if item.type === "link" || item.href}
|
||||
<section class="wire-footer__column">
|
||||
<ul class="wire-footer__column-links">
|
||||
<li>
|
||||
<a
|
||||
href='{item.href || "#"}'
|
||||
target='{item.target || ""}'
|
||||
rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
|
||||
class="wire-footer__link"
|
||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, itemIndex: itemIndex } })); item.action && event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { item: item, itemIndex: itemIndex } }))'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='{item.icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{item.label || item.title}</span>
|
||||
{#if item.external}
|
||||
<span class="icon-[lucide--external-link]" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
{:else}
|
||||
<section class="wire-footer__column">
|
||||
{#if item.title || item.label}
|
||||
<h2 class="wire-footer__heading">{item.title || item.label}</h2>
|
||||
{/if}
|
||||
{#if item.description}
|
||||
<p class="wire-footer__description">{item.description}</p>
|
||||
{/if}
|
||||
<ul class="wire-footer__column-links">
|
||||
{#each item.items || item.links || [] as child, childIndex}
|
||||
<li>
|
||||
<a
|
||||
href='{child.href || "#"}'
|
||||
target='{child.target || ""}'
|
||||
rel='{child.external ? "noopener noreferrer" : (child.rel || "")}'
|
||||
aria-current='{child.active ? "page" : ""}'
|
||||
class="wire-footer__link"
|
||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex } })); child.action && event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex } }))'
|
||||
>
|
||||
{#if child.icon}
|
||||
<span class='{child.icon}' aria-hidden="true"></span>
|
||||
<span
|
||||
class='{child.icon}'
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
<span>{child.label || child.title}</span>
|
||||
|
||||
<span>
|
||||
{child.label || child.title}
|
||||
</span>
|
||||
|
||||
{#if child.badge}
|
||||
<span
|
||||
class="wire-badge wire-badge--default"
|
||||
>
|
||||
{child.badge}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if child.external}
|
||||
<span class="icon-[lucide--external-link]" aria-hidden="true"></span>
|
||||
<span
|
||||
class="icon-[lucide--arrow-up-right]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{:else if item.type === "link" || item.href}
|
||||
<section
|
||||
class="wire-footer__column"
|
||||
>
|
||||
<div
|
||||
class="wire-footer__column-links"
|
||||
>
|
||||
<a
|
||||
href='{item.href || "#"}'
|
||||
target='{item.target || ""}'
|
||||
rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
|
||||
aria-current='{item.active ? "page" : ""}'
|
||||
class="wire-footer__link"
|
||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, itemIndex: itemIndex } })); item.action && event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { item: item, itemIndex: itemIndex } }))'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span
|
||||
class='{item.icon}'
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<span>
|
||||
{item.label || item.title}
|
||||
</span>
|
||||
|
||||
{#if item.badge}
|
||||
<span
|
||||
class="wire-badge wire-badge--default"
|
||||
>
|
||||
{item.badge}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if item.external}
|
||||
<span
|
||||
class="icon-[lucide--arrow-up-right]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{:else}
|
||||
<section
|
||||
class="wire-footer__column"
|
||||
aria-labelledby='footer-heading-{itemIndex}'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span
|
||||
class='{item.icon}'
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if item.title || item.label}
|
||||
<h2
|
||||
id='footer-heading-{itemIndex}'
|
||||
class="wire-footer__heading"
|
||||
>
|
||||
{item.title || item.label}
|
||||
</h2>
|
||||
{/if}
|
||||
|
||||
{#if item.description}
|
||||
<p>
|
||||
{item.description}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if (item.items || item.links || []).length > 0}
|
||||
<div
|
||||
class="wire-footer__column-links"
|
||||
>
|
||||
{#each item.items || item.links || [] as child, childIndex}
|
||||
<a
|
||||
href='{child.href || "#"}'
|
||||
target='{child.target || ""}'
|
||||
rel='{child.external ? "noopener noreferrer" : (child.rel || "")}'
|
||||
aria-current='{child.active ? "page" : ""}'
|
||||
class="wire-footer__link"
|
||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex } })); child.action && event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex } }))'
|
||||
>
|
||||
{#if child.icon}
|
||||
<span
|
||||
class='{child.icon}'
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<span>
|
||||
{child.label || child.title}
|
||||
</span>
|
||||
|
||||
{#if child.badge}
|
||||
<span
|
||||
class="wire-badge wire-badge--default"
|
||||
>
|
||||
{child.badge}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if child.external}
|
||||
<span
|
||||
class="icon-[lucide--arrow-up-right]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<div class="wire-footer__copyright">
|
||||
<div class="wire-footer__copyright-left">
|
||||
<slot name="copyright-left"></slot>
|
||||
{#if copyright}
|
||||
<p>{copyright}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="wire-footer__copyright-right">
|
||||
<slot name="copyright-right"></slot>
|
||||
</div>
|
||||
<div
|
||||
class="wire-footer__post"
|
||||
>
|
||||
<slot
|
||||
name="post-footer"
|
||||
>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot name="post-footer"></slot>
|
||||
<div
|
||||
class="wire-footer__bottom wire-footer__bottom--slots"
|
||||
>
|
||||
<div
|
||||
class="wire-footer__copyright-left"
|
||||
>
|
||||
<slot
|
||||
name="copyright-left"
|
||||
>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="wire-footer__copyright-right"
|
||||
>
|
||||
<slot
|
||||
name="copyright-right"
|
||||
>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if copyright}
|
||||
<div
|
||||
class="wire-footer__bottom"
|
||||
>
|
||||
<div></div>
|
||||
<p class="wire-footer__copyright">
|
||||
{copyright}
|
||||
</p>
|
||||
<div></div>
|
||||
</div>
|
||||
{/if}
|
||||
</footer>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.5.12",
|
||||
"version": "0.5.13",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -194,10 +194,10 @@ test("auth form uses package schemas, disables native validation, and renders a
|
||||
|
||||
test("footer supports typed entries, responsive columns, pre/post slots, and events", () => {
|
||||
const source = readFileSync(uiComponentPath("Footer"), "utf8");
|
||||
expect(source).toContain('slot name="pre-footer"');
|
||||
expect(source).toContain('slot name="post-footer"');
|
||||
expect(source).toContain('slot name="copyright-left"');
|
||||
expect(source).toContain('slot name="copyright-right"');
|
||||
expect(source).toMatch(/<slot\s+name="pre-footer"\s*>/);
|
||||
expect(source).toMatch(/<slot\s+name="post-footer"\s*>/);
|
||||
expect(source).toMatch(/<slot\s+name="copyright-left"\s*>/);
|
||||
expect(source).toMatch(/<slot\s+name="copyright-right"\s*>/);
|
||||
expect(source).toContain('item.type === "header"');
|
||||
expect(source).toContain("wire-footer--columns-{columns}");
|
||||
expect(source).toContain("@event select = function");
|
||||
|
||||
Reference in New Issue
Block a user