Files
WRNexusJS/packages/ui/components/Footer.wrn
Clintchiz f88dd47408
Quality / quality (ubuntu-latest) (push) Failing after 23s
Quality / quality (windows-latest) (push) Canceled after 0s
fix(runtime): stabilize navigation and custom errors
2026-08-15 10:44:06 +05:30

557 lines
16 KiB
Plaintext

component Footer {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
action(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Footer navigation"
items: unknown[] = []
columns: number = 3
maxWidth: string = "compact"
copyright: string = ""
copyrightKey: string = ""
translationPrefix: string = ""
class: string = ""
}
functions {
shared function translationKey(item, field) {
if (!item) return ""
const explicit = item[field + "Key"]
if (explicit) return explicit
if (!translationPrefix) return ""
const source = item.value || item.href || item.label || item.title || ""
const slug = String(source).replace(/^\/+|\/+$/g, "").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase()
return slug ? translationPrefix + "." + slug + (field === "description" ? ".description" : "") : ""
}
}
view {
<footer
{...attrs}
data-ui-component="Footer"
data-size='{size}'
data-color='{color}'
class='wrn-footer {maxWidth === "full" ? "wrn-footer--width-full" : ""} {class}'
>
<div
class="wrn-footer__grid"
>
<div
class="wrn-footer__pre"
>
<slot
name="pre-footer"
>
</slot>
</div>
{#if items.length > 0}
<nav
class='wrn-footer__links wrn-footer--columns-{columns}'
aria-label='{label}'
>
{#each items as item, itemIndex}
{#if item.type === "header"}
<section
class="wrn-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="wrn-footer__heading"
data-t='{translationKey(item, "label")}'
>
{item.label || item.title}
</h2>
{#if item.description}
<p data-t='{translationKey(item, "description")}'>
{item.description}
</p>
{/if}
{#if (item.items || item.links || []).length > 0}
<div
class="wrn-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" : "false"}'
class="wrn-footer__link"
@click='output.select({ item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex }); child.action && output.action({ item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex })'
>
{#if child.icon}
<span
class='{child.icon}'
aria-hidden="true"
>
</span>
{/if}
<span data-t='{translationKey(child, "label")}'>
{child.label || child.title}
</span>
{#if child.badge}
<span
class="wrn-badge wrn-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>
{:else if item.type === "link" || item.href}
<section
class="wrn-footer__column"
>
<div
class="wrn-footer__column-links"
>
<a
href='{item.href || "#"}'
target='{item.target || ""}'
rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
aria-current='{item.active ? "page" : "false"}'
class="wrn-footer__link"
@click='output.select({ item: item, itemIndex: itemIndex }); item.action && output.action({ item: item, itemIndex: itemIndex })'
>
{#if item.icon}
<span
class='{item.icon}'
aria-hidden="true"
>
</span>
{/if}
<span data-t='{translationKey(item, "label")}'>
{item.label || item.title}
</span>
{#if item.badge}
<span
class="wrn-badge wrn-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="wrn-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="wrn-footer__heading"
data-t='{translationKey(item, "label")}'
>
{item.title || item.label}
</h2>
{/if}
{#if item.description}
<p>
{item.description}
</p>
{/if}
{#if (item.items || item.links || []).length > 0}
<div
class="wrn-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="wrn-footer__link"
@click='output.select({ item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex }); child.action && output.action({ 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="wrn-badge wrn-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}
</nav>
{/if}
<div
class="wrn-footer__post"
>
<slot
name="post-footer"
>
</slot>
</div>
</div>
<div
class="wrn-footer__bottom wrn-footer__bottom--slots"
>
<div
class="wrn-footer__copyright-left"
>
<slot
name="copyright-left"
>
</slot>
</div>
{#if copyright}
<p
class="wrn-footer__copyright"
>
<span data-t='{copyrightKey}'>{copyright}</span>
</p>
{/if}
<div
class="wrn-footer__copyright-right"
>
<slot
name="copyright-right"
>
</slot>
</div>
</div>
</footer>
}
style {
.wrn-footer--columns-1 {
--wrn-footer-columns: 1;
}
.wrn-footer--columns-2 {
--wrn-footer-columns: 2;
}
.wrn-footer--columns-3 {
--wrn-footer-columns: 3;
}
.wrn-footer--columns-4 {
--wrn-footer-columns: 4;
}
.wrn-footer--columns-5 {
--wrn-footer-columns: 5;
}
.wrn-footer--columns-6 {
--wrn-footer-columns: 6;
}
@media (max-width: 900px) {
.wrn-footer--columns-1 .wrn-footer__links {
grid-template-columns: 1fr;
}
.wrn-footer--columns-2 .wrn-footer__links {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 600px) {
.wrn-footer--columns-1 .wrn-footer__links {
grid-template-columns: 1fr;
}
}
/* --- Badge ---------------------------------------------------------------- */
.wrn-badge {
display: inline-flex;
align-items: center;
font-size: 0.75rem;
font-weight: 700;
line-height: 1;
padding: 0.25rem 0.5rem;
border-radius: 999px;
}
.wrn-badge--default {
background: var(--wrn-color-surface-2);
color: var(--wrn-color-text);
}
/* Structured responsive footer */
.wrn-footer {
width: 100%;
color: var(--wrn-color-text);
background: var(--wrn-color-surface);
border-top: 1px solid var(--wrn-color-border);
font-size: 0.875rem;
font-weight: 400;
}
.wrn-footer__grid {
display: grid;
grid-template-columns: minmax(13rem, 1.2fr) minmax(0, 3fr) minmax(13rem, 1fr);
width: min(100%, 90rem);
margin-inline: auto;
padding: clamp(2rem, 5vw, 4rem) clamp(1rem, 4vw, 3rem);
gap: clamp(1.5rem, 4vw, 3.5rem);
}
.wrn-footer--width-full .wrn-footer__grid {
width: 100%;
max-width: none;
}
.wrn-footer__pre:empty,
.wrn-footer__post:empty,
.wrn-footer__pre:not(:has(*)),
.wrn-footer__post:not(:has(*)) {
display: none;
}
.wrn-footer__pre,
.wrn-footer__post {
min-width: 0;
}
.wrn-footer__pre strong,
.wrn-footer__post strong {
font-size: 0.875rem;
font-weight: 600;
}
.wrn-footer__pre p,
.wrn-footer__post p {
font-size: 0.8125rem;
font-weight: 400;
line-height: 1.6;
}
.wrn-footer__links {
display: grid;
grid-template-columns: repeat(var(--wrn-footer-columns, 3), minmax(0, 1fr));
align-content: start;
gap: 1.5rem;
}
.wrn-footer__column {
display: grid;
min-width: 0;
align-content: start;
gap: 0.8rem;
}
.wrn-footer__column-links {
display: grid;
align-content: start;
gap: 0.55rem;
}
.wrn-footer__heading {
display: block;
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
}
.wrn-footer__link {
display: flex;
width: fit-content;
min-height: 2rem;
align-items: center;
gap: 0.4rem;
color: var(--wrn-color-muted);
font-size: 0.8125rem;
font-weight: 400;
text-decoration: none;
}
.wrn-footer__link:hover {
color: var(--wrn-component-color, var(--wrn-color-primary));
text-decoration: underline;
text-underline-offset: 0.2em;
}
.wrn-footer__bottom {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
width: min(100%, 90rem);
margin-inline: auto;
align-items: center;
gap: 1rem;
padding: 1rem clamp(1rem, 4vw, 3rem);
color: var(--wrn-color-muted);
font-size: 0.75rem;
font-weight: 400;
}
.wrn-footer__bottom--slots {
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
border-top: 1px solid var(--wrn-color-border);
}
.wrn-footer__bottom--slots:not(
:has(.wrn-footer__copyright-left > *, .wrn-footer__copyright-right > *)
) {
display: none;
}
.wrn-footer__copyright {
margin: 0;
text-align: center;
white-space: nowrap;
}
.wrn-footer__copyright-left,
.wrn-footer__copyright-right {
display: flex;
flex-wrap: wrap;
min-width: 0;
align-items: center;
gap: 0.75rem;
}
.wrn-footer__copyright-left a,
.wrn-footer__copyright-right a {
color: inherit;
text-decoration: none;
}
.wrn-footer__copyright-left a:hover,
.wrn-footer__copyright-right a:hover {
color: var(--wrn-component-color, var(--wrn-color-primary));
text-decoration: underline;
text-underline-offset: 0.2em;
}
.wrn-footer__copyright-right {
justify-content: flex-end;
text-align: right;
}
.wrn-footer__copyright-left:empty,
.wrn-footer__copyright-right:empty,
.wrn-footer__copyright-left:not(:has(*)),
.wrn-footer__copyright-right:not(:has(*)) {
min-height: 0;
}
@media (max-width: 900px) {
.wrn-footer__grid {
grid-template-columns: 1fr;
}
.wrn-footer__links {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.wrn-footer__bottom--slots {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
.wrn-footer__copyright {
grid-column: 1 / -1;
grid-row: 1;
}
}
@media (max-width: 600px) {
.wrn-footer__links {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.wrn-footer__bottom,
.wrn-footer__bottom--slots {
grid-template-columns: 1fr;
justify-items: center;
text-align: center;
}
.wrn-footer__copyright-left,
.wrn-footer__copyright-right {
justify-content: center;
text-align: center;
}
.wrn-footer__copyright {
white-space: normal;
}
}
@media (max-width: 420px) {
.wrn-footer__links {
grid-template-columns: 1fr;
}
}
}
}