Files
WRNexusJS/.publish/ui/components/card.wrn
T
2026-08-01 01:09:58 +05:30

353 lines
13 KiB
Plaintext

component Card {
outputs {
click(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
action(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
navigate(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
dismiss(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
load(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
}
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: unknown[] = []
navigation: unknown[] = []
activeNav: string = ""
mobileNavigation: boolean = false
alertTitle: string = ""
alertDescription: string = ""
empty: boolean = false
emptyTitle: string = "No data to show"
emptyIcon: string = "icon-[lucide--inbox]"
items: unknown[] = []
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 = ""
}
state dismissed: boolean = false
functions {
client function dispatchCardNavigation(sourceEvent, item, index, root, customEvent) {
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("navigate", true, false, {
component: "Card",
item: item,
index: index
})
root.dispatchEvent(customEvent)
}
client function dispatchCardNavigationValue(sourceEvent, root, customEvent) {
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("navigate", true, false, {
component: "Card",
value: sourceEvent.currentTarget.value
})
root.dispatchEvent(customEvent)
}
client function dispatchCardHeaderAction(sourceEvent, action, index, root, customEvent) {
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("action", true, false, {
component: "Card",
action: action,
index: index
})
root.dispatchEvent(customEvent)
}
client function dismissCard(sourceEvent, root, customEvent) {
dismissed = true
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("dismiss", true, false, {
component: "Card",
title: title
})
root.dispatchEvent(customEvent)
}
}
view {
<div
{...attrs}
data-ui-component="Card"
data-wrn-card
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
data-layout='{layout}'
data-hover='{hover}'
data-align='{align}'
class='wire-next wire-next--card wire-next--color-{color} wire-next--size-{size} wire-next--variant-{variant} {class}'
>
{#if items.length > 0}
<div class="wire-next__card-group">
{#each items as item, itemIndex}
<article
class="wire-next__card-panel"
aria-label='{item.ariaLabel || item.title || item.label || "Card item"}'
data-index='{itemIndex}'
>
{#if item.imageSrc || item.image}
<div class="wire-next__card-media">
<img
src='{item.imageSrc || item.image}'
alt='{item.imageAlt || item.alt || ""}'
loading='{item.loading || "lazy"}'
decoding="async"
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { item: item, index: itemIndex } }))'
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { item: item, index: itemIndex } }))'
/>
</div>
{/if}
<div class="wire-next__card-content">
{#if item.title || item.label}
<h3>{item.title || item.label}</h3>
{/if}
{#if item.subtitle}
<p class="wire-next__card-subtitle">{item.subtitle}</p>
{/if}
{#if item.description}
<p class="wire-next__card-description">{item.description}</p>
{/if}
{#if item.actionLabel || item.href}
<a
href='{item.actionHref || item.href || "#"}'
class="wire-next__card-action"
@click='event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { item: item, index: itemIndex } }))'
>
<span>{item.actionLabel || "Learn more"}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
</a>
{/if}
</div>
</article>
{/each}
</div>
{:else}
<article
class="wire-next__card-panel"
aria-label='{ariaLabel || title || "Card"}'
hidden='{dismissed}'
>
{#if imageSrc && imagePosition === "overlay"}
<div class="wire-next__card-overlay">
<img
src='{imageSrc}'
alt='{imageAlt}'
loading="lazy"
decoding="async"
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { src: imageSrc } }))'
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { src: imageSrc } }))'
/>
<div class="wire-next__card-overlay-shade" aria-hidden="true"></div>
</div>
{/if}
{#if imageSrc && (imagePosition === "top" || imagePosition === "left")}
<div class="wire-next__card-media wire-next__card-media--{imagePosition}">
<img
src='{imageSrc}'
alt='{imageAlt}'
loading="lazy"
decoding="async"
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { src: imageSrc } }))'
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { src: imageSrc } }))'
/>
</div>
{/if}
<div class="wire-next__card-content">
{#if header || title || subtitle || headerActions.length > 0 || dismissible}
<header class="wire-next__card-header">
<div class="wire-next__card-heading">
{#if header}
<p class="wire-next__card-eyebrow">{header}</p>
{/if}
{#if title}
<h3>{title}</h3>
{/if}
{#if subtitle}
<p class="wire-next__card-subtitle">{subtitle}</p>
{/if}
</div>
{#if headerActions.length > 0 || dismissible}
<div class="wire-next__card-header-actions">
{#each headerActions as headerAction, actionIndex}
<button
type="button"
aria-label='{headerAction.ariaLabel || headerAction.label || "Card action"}'
@click='dispatchCardHeaderAction(event, headerAction, actionIndex)'
>
{#if headerAction.icon}
<span class='{headerAction.icon}' aria-hidden="true"></span>
{/if}
{#if headerAction.label && !headerAction.icon}
<span>{headerAction.label}</span>
{/if}
</button>
{/each}
{#if dismissible}
<button
type="button"
aria-label="Dismiss card"
@click='dismissCard(event)'
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
{/if}
</div>
{/if}
</header>
{/if}
{#if navigation.length > 0}
<nav aria-label='{ariaLabel || title || "Card navigation"}' class="wire-next__card-navigation">
{#if mobileNavigation}
<select
class="wire-next__card-navigation-select"
aria-label='{ariaLabel || title || "Card navigation"}'
@change='dispatchCardNavigationValue(event)'
>
{#each navigation as navItem}
<option
value='{navItem.value || navItem.href || navItem.label}'
selected='{activeNav === navItem.value || activeNav === navItem.href}'
>
{navItem.label}
</option>
{/each}
</select>
{/if}
<div class="wire-next__card-tabs" data-mobile-navigation='{mobileNavigation}'>
{#each navigation as navItem, navIndex}
<button
type="button"
aria-pressed='{activeNav === navItem.value || activeNav === navItem.href}'
@click='dispatchCardNavigation(event, navItem, navIndex)'
>
{#if navItem.icon}
<span class='{navItem.icon}' aria-hidden="true"></span>
{/if}
<span>{navItem.label}</span>
{#if navItem.badge}
<span class="wire-next__card-tab-badge">{navItem.badge}</span>
{/if}
</button>
{/each}
</div>
</nav>
{/if}
{#if alertTitle || alertDescription}
<div class="wire-next__card-alert" role="status">
{#if alertTitle}
<strong>{alertTitle}</strong>
{/if}
{#if alertDescription}
<p>{alertDescription}</p>
{/if}
</div>
{/if}
<div
class="wire-next__card-body"
data-scrollable='{scrollable}'
style='max-height: {scrollable ? maxHeight : "none"};'
>
{#if empty}
<div class="wire-next__card-empty">
<span class='{emptyIcon}' aria-hidden="true"></span>
<p>{emptyTitle}</p>
</div>
{:else}
{#if description}
<p class="wire-next__card-description">{description}</p>
{/if}
<slot></slot>
{/if}
</div>
{#if footer || actionLabel}
<footer class="wire-next__card-footer">
{#if footer}
<span>{footer}</span>
{/if}
{#if actionLabel}
<a
href='{actionHref || "#"}'
class="wire-next__card-action"
@click='event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { href: actionHref, label: actionLabel } }))'
>
<span>{actionLabel}</span>
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
</a>
{/if}
</footer>
{/if}
</div>
{#if imageSrc && (imagePosition === "bottom" || imagePosition === "right")}
<div class="wire-next__card-media wire-next__card-media--{imagePosition}">
<img
src='{imageSrc}'
alt='{imageAlt}'
loading="lazy"
decoding="async"
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { src: imageSrc } }))'
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { src: imageSrc } }))'
/>
</div>
{/if}
</article>
{/if}
</div>
}
}