Three separate problems, all traceable to `git add -A` sweeping up a working
tree I had not inspected.
Commit 69020b25 ("docs: make the component sections executable") committed far
more than docs: 79 files of a half-scaffolded inter-app example, and four of
those files were truncated mid-statement. That broke `bun run typecheck` on
main. The example is reverted to its last green six-file form. The truncated
fragments and the fuller working copy are NOT in this commit -- if any of that
workspace was wanted, it needs to be reconstructed deliberately and committed on
its own, not as a side effect of a docs change.
Separately, `scripts/generate-ui-complete-catalog.mjs` was run while checking
which helper scripts still work. It rewrites components in place, so it
flattened six of them to stubs, deleted 24 more and lower-cased four filenames
before crashing. Contents were restored from HEAD, but the renames survived
that restore: Windows is case-insensitive, so `git status` reported clean while
Card, Container, Divider and Grid sat on disk under the wrong names. The index
now tracks the capitalised names, which is what the components declare and what
ui-redesign-contract.test.ts reads -- that test would have failed on any
case-sensitive checkout.
Documented both as 4.7 and 4.8 in the remediation plan, with the general rule:
no script that rewrites packages/ui/components/ may write in place. Also fixes
the heading level on 4.6, which was rendering outside section 4.
bun run check is green: 1,433 pass, 0 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
313 lines
12 KiB
Plaintext
313 lines
12 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 {
|
|
// Outputs must go through output.*; a CustomEvent dispatched on the root
|
|
// bubbles past every parent @binding without being seen, because the
|
|
// runtime keeps parent handlers in a registry only the output proxy reads.
|
|
client function dispatchCardNavigation(sourceEvent, item, index) {
|
|
output.navigate({ component: "Card", item: item, index: index })
|
|
}
|
|
|
|
client function dispatchCardNavigationValue(sourceEvent) {
|
|
output.navigate({
|
|
component: "Card",
|
|
value: sourceEvent.currentTarget.value
|
|
})
|
|
}
|
|
|
|
client function dispatchCardHeaderAction(sourceEvent, action, index) {
|
|
output.action({ component: "Card", action: action, index: index })
|
|
}
|
|
|
|
client function dismissCard() {
|
|
dismissed = true
|
|
output.dismiss({ component: "Card", title: title })
|
|
}
|
|
}
|
|
|
|
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='output.load({ item: item, index: itemIndex })'
|
|
@error='output.error({ 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='output.action({ 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='output.load({ src: imageSrc })'
|
|
@error='output.error({ 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='output.load({ src: imageSrc })'
|
|
@error='output.error({ 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='output.action({ 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='output.load({ src: imageSrc })'
|
|
@error='output.error({ src: imageSrc })'
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</article>
|
|
{/if}
|
|
</div>
|
|
}
|
|
}
|