feat(ui): build remaining component scaffolds
This commit is contained in:
@@ -1,25 +1,107 @@
|
||||
component Chart {
|
||||
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)
|
||||
dataPointClick(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
legendToggle(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
select(payload: { item: object; index: number; sourceEvent?: Event })
|
||||
dataPointClick(payload: { item: object; index: number; sourceEvent?: Event })
|
||||
legendToggle(payload: { item: object; index: number; hidden: boolean; sourceEvent?: Event })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Chart"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
valueKey: string = "value"
|
||||
labelKey: string = "label"
|
||||
height: number = 240
|
||||
showLegend: boolean = true
|
||||
showValues: boolean = true
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
variant: string = "bar"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state hiddenIndexes = []
|
||||
|
||||
functions {
|
||||
shared function numericValue(item) { return Math.max(0, Number(item[valueKey]) || 0) }
|
||||
shared function maximumValue() {
|
||||
return Math.max(1, ...items.map(function (item) { return numericValue(item) }))
|
||||
}
|
||||
shared function isHidden(index) { return hiddenIndexes.includes(index) }
|
||||
shared function barWidth() { return items.length ? Math.max(4, 80 / items.length) : 80 }
|
||||
shared function barX(index) { return 10 + index * (80 / Math.max(1, items.length)) }
|
||||
shared function barHeight(item) { return numericValue(item) / maximumValue() * 72 }
|
||||
shared function barY(item) { return 84 - barHeight(item) }
|
||||
|
||||
client function selectPoint(item, index, sourceEvent) {
|
||||
output.dataPointClick({ item: item, index: index, sourceEvent: sourceEvent })
|
||||
output.select({ item: item, index: index, sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
client function toggleLegend(item, index, sourceEvent, next) {
|
||||
next = hiddenIndexes.slice()
|
||||
if (next.includes(index)) {
|
||||
next = next.filter(function (value) { return value !== index })
|
||||
} else {
|
||||
next.push(index)
|
||||
}
|
||||
hiddenIndexes = next
|
||||
output.legendToggle({ item: item, index: index, hidden: next.includes(index), sourceEvent: sourceEvent })
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--chart wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<figure {...attrs} data-ui-component="Chart" data-variant='{variant}' class='wire-chart {class}'>
|
||||
{#if title || description}
|
||||
<figcaption class="wire-chart__header">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<span>{description}</span>{/if}
|
||||
</figcaption>
|
||||
{/if}
|
||||
<svg class="wire-chart__plot" viewBox="0 0 100 100" role="img" aria-label='{title}' style='height:{height}px'>
|
||||
<line x1="8" y1="84" x2="96" y2="84" class="wire-chart__axis" />
|
||||
{#each items as item, index}
|
||||
<g data-hidden='{isHidden(index) ? "true" : "false"}'>
|
||||
<rect
|
||||
x='{barX(index)}' y='{barY(item)}' width='{barWidth()}' height='{barHeight(item)}'
|
||||
rx="1.5" class="wire-chart__bar" role="button" tabindex="0"
|
||||
aria-label='{item[labelKey] + ": " + numericValue(item)}'
|
||||
@click='selectPoint(item, index, event)'
|
||||
@keydown='if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectPoint(item, index, event) }'
|
||||
/>
|
||||
{#if showValues}<text x='{barX(index) + barWidth() / 2}' y='{barY(item) - 3}' class="wire-chart__value">{numericValue(item)}</text>{/if}
|
||||
<text x='{barX(index) + barWidth() / 2}' y="94" class="wire-chart__label">{item[labelKey]}</text>
|
||||
</g>
|
||||
{/each}
|
||||
</svg>
|
||||
{#if showLegend}
|
||||
<div class="wire-chart__legend" aria-label="Chart legend">
|
||||
{#each items as item, index}
|
||||
<button type="button" aria-pressed='{isHidden(index) ? "false" : "true"}' @click='toggleLegend(item, index, event)'>
|
||||
<span aria-hidden="true"></span>{item[labelKey]}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<slot />
|
||||
</section>
|
||||
</figure>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-chart { display: grid; gap: 1rem; min-width: 0; margin: 0; padding: 1rem; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius); color: var(--wire-color-text); background: var(--wire-color-surface); box-shadow: var(--wire-shadow-1); }
|
||||
.wire-chart__header { display: grid; gap: 0.2rem; }
|
||||
.wire-chart__header > span { color: var(--wire-color-muted); font-size: 0.8rem; }
|
||||
.wire-chart__plot { display: block; width: 100%; min-height: 12rem; overflow: visible; }
|
||||
.wire-chart__axis { stroke: var(--wire-color-border); stroke-width: 0.5; }
|
||||
.wire-chart__bar { fill: var(--wire-color-primary); cursor: pointer; transition: opacity var(--wire-motion-fast), transform var(--wire-motion-fast); transform-box: fill-box; transform-origin: bottom; }
|
||||
.wire-chart__bar:hover { opacity: 0.82; transform: scaleY(1.02); }
|
||||
.wire-chart__bar:focus-visible { outline: 1.5px solid var(--wire-color-focus); outline-offset: 1px; }
|
||||
.wire-chart__plot g[data-hidden="true"] { opacity: 0.18; pointer-events: none; }
|
||||
.wire-chart__value, .wire-chart__label { fill: var(--wire-color-text); font-size: 3.5px; text-anchor: middle; }
|
||||
.wire-chart__label { fill: var(--wire-color-muted); }
|
||||
.wire-chart__legend { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.wire-chart__legend button { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.3rem 0.5rem; border: 1px solid var(--wire-color-border); border-radius: 999px; color: var(--wire-color-text); background: transparent; font: inherit; font-size: 0.75rem; cursor: pointer; }
|
||||
.wire-chart__legend button > span { width: 0.65rem; height: 0.65rem; border-radius: 0.2rem; background: var(--wire-color-primary); }
|
||||
.wire-chart__legend button[aria-pressed="false"] { opacity: 0.5; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,97 @@
|
||||
component Clipboard {
|
||||
outputs {
|
||||
copy(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
success(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)
|
||||
copy(payload: { value: string; sourceEvent?: Event })
|
||||
success(payload: { value: string; sourceEvent?: Event })
|
||||
error(payload: { error: Error | string; value: string; sourceEvent?: Event })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
value: string = ""
|
||||
label: string = "Copy"
|
||||
copiedLabel: string = "Copied"
|
||||
errorLabel: string = "Copy failed"
|
||||
title: string = "Clipboard"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
code: boolean = true
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
disabled: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state status = "idle"
|
||||
|
||||
functions {
|
||||
client function resetStatus() { status = "idle" }
|
||||
client function copySucceeded() {
|
||||
status = "success"
|
||||
output.success({ value: value })
|
||||
setTimeout(resetStatus, 1600)
|
||||
}
|
||||
client function copyFailed(error) {
|
||||
status = "error"
|
||||
output.error({ error: error, value: value })
|
||||
setTimeout(resetStatus, 1600)
|
||||
}
|
||||
client function copyValue(sourceEvent) {
|
||||
if (disabled || status === "copying") {
|
||||
return
|
||||
}
|
||||
status = "copying"
|
||||
output.copy({ value: value, sourceEvent: sourceEvent })
|
||||
if (!window.navigator.clipboard || !window.navigator.clipboard.writeText) {
|
||||
copyFailed(new Error("Clipboard API is unavailable"))
|
||||
return
|
||||
}
|
||||
return window.navigator.clipboard.writeText(value).then(copySucceeded).catch(copyFailed)
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--clipboard wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<section
|
||||
{...attrs}
|
||||
data-ui-component="Clipboard"
|
||||
data-status='{status}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
class='wire-clipboard {class}'
|
||||
>
|
||||
{#if title}<strong class="wire-clipboard__title">{title}</strong>{/if}
|
||||
{#if description}<p class="wire-clipboard__description">{description}</p>{/if}
|
||||
<div class="wire-clipboard__control">
|
||||
{#if code}
|
||||
<code class="wire-clipboard__value">{value}</code>
|
||||
{:else}
|
||||
<span class="wire-clipboard__value">{value}</span>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-clipboard__button"
|
||||
disabled='{disabled}'
|
||||
aria-label='{status === "success" ? copiedLabel : status === "error" ? errorLabel : label}'
|
||||
@click='copyValue(event)'
|
||||
>
|
||||
<span class="icon-[lucide--copy] wire-clipboard__icon" data-show='status !== "success"' aria-hidden="true"></span>
|
||||
<span class="icon-[lucide--check] wire-clipboard__icon" data-show='status === "success"' aria-hidden="true"></span>
|
||||
<span>{status === "success" ? copiedLabel : status === "error" ? errorLabel : label}</span>
|
||||
</button>
|
||||
</div>
|
||||
<slot />
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-clipboard { display: grid; gap: 0.5rem; color: var(--wire-color-text); }
|
||||
.wire-clipboard__title { font-size: 0.875rem; }
|
||||
.wire-clipboard__description { margin: 0; color: var(--wire-color-muted); font-size: 0.8rem; }
|
||||
.wire-clipboard__control { display: flex; min-width: 0; align-items: stretch; overflow: hidden; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius); background: var(--wire-color-surface); box-shadow: var(--wire-shadow-1); }
|
||||
.wire-clipboard__value { min-width: 0; flex: 1; overflow: auto; padding: 0.7rem 0.85rem; color: var(--wire-color-text); background: var(--wire-color-surface-2); font: inherit; font-family: var(--wire-font-mono, ui-monospace, monospace); white-space: nowrap; }
|
||||
.wire-clipboard__button { display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.6rem 0.85rem; border: 0; border-left: 1px solid var(--wire-color-border); color: var(--wire-color-primary); background: transparent; font: inherit; font-size: 0.8rem; font-weight: 700; cursor: pointer; }
|
||||
.wire-clipboard__button:hover { background: var(--wire-color-primary-soft); }
|
||||
.wire-clipboard__button:focus-visible { outline: 2px solid var(--wire-color-focus); outline-offset: -3px; }
|
||||
.wire-clipboard__button:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
.wire-clipboard[data-status="success"] .wire-clipboard__button { color: var(--wire-color-success); }
|
||||
.wire-clipboard[data-status="error"] .wire-clipboard__button { color: var(--wire-color-danger); }
|
||||
.wire-clipboard__icon { width: 1rem; height: 1rem; flex: none; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,72 @@
|
||||
component Confetti {
|
||||
outputs {
|
||||
start(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
complete(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
start(payload: { sourceEvent?: Event; duration: number })
|
||||
complete(payload: { sourceEvent?: Event; duration: number })
|
||||
}
|
||||
|
||||
props {
|
||||
label: string = "Celebrate"
|
||||
duration: number = 1200
|
||||
count: number = 24
|
||||
autoStart: boolean = false
|
||||
disabled: boolean = false
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Confetti"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state running = false
|
||||
|
||||
functions {
|
||||
shared function pieces() { return Array.from({ length: Math.max(1, Math.min(60, count)) }) }
|
||||
shared function pieceStyle(index) {
|
||||
return "--wire-confetti-index:" + index + ";--wire-confetti-x:" + ((index * 37) % 100) + ";--wire-confetti-delay:" + ((index * 29) % 240) + "ms;"
|
||||
}
|
||||
client function finish(sourceEvent) {
|
||||
running = false
|
||||
output.complete({ sourceEvent: sourceEvent, duration: duration })
|
||||
}
|
||||
client function start(sourceEvent) {
|
||||
if (disabled || running) {
|
||||
return
|
||||
}
|
||||
running = true
|
||||
output.start({ sourceEvent: sourceEvent, duration: duration })
|
||||
setTimeout(finish, duration, sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
mount { if (autoStart) { start(null) } }
|
||||
}
|
||||
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--confetti wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<div {...attrs} data-ui-component="Confetti" data-running='{running ? "true" : "false"}' class='wire-confetti {class}' style='--wire-confetti-duration:{duration}ms'>
|
||||
<button type="button" class="wire-confetti__trigger" disabled='{disabled}' @click='start(event)'>
|
||||
<span class="icon-[lucide--party-popper]" aria-hidden="true"></span><span>{label}</span>
|
||||
</button>
|
||||
<div class="wire-confetti__burst" aria-hidden="true">
|
||||
{#each pieces() as piece, index}<i style='{pieceStyle(index)}'></i>{/each}
|
||||
</div>
|
||||
<slot />
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-confetti { position: relative; display: inline-flex; }
|
||||
.wire-confetti__trigger { display: inline-flex; min-height: 2.5rem; align-items: center; gap: 0.5rem; padding: 0.55rem 0.85rem; border: 1px solid var(--wire-color-primary); border-radius: var(--wire-radius); color: var(--wire-color-on-primary); background: var(--wire-color-primary); font: inherit; font-weight: 700; cursor: pointer; }
|
||||
.wire-confetti__trigger > span:first-child { width: 1rem; height: 1rem; }
|
||||
.wire-confetti__trigger:focus-visible { outline: 2px solid var(--wire-color-focus); outline-offset: 2px; }
|
||||
.wire-confetti__trigger:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
.wire-confetti__burst { position: absolute; inset: 50% 0 auto; height: 0; pointer-events: none; }
|
||||
.wire-confetti__burst > i { position: absolute; left: calc(var(--wire-confetti-x) * 1%); width: 0.45rem; height: 0.7rem; border-radius: 0.1rem; opacity: 0; background: hsl(calc(var(--wire-confetti-index) * 47deg) 80% 55%); transform: translate(-50%, 0) rotate(calc(var(--wire-confetti-index) * 23deg)); }
|
||||
.wire-confetti[data-running="true"] .wire-confetti__burst > i { animation: wire-confetti-burst var(--wire-confetti-duration) cubic-bezier(0.2, 0.7, 0.2, 1) var(--wire-confetti-delay) both; }
|
||||
@keyframes wire-confetti-burst {
|
||||
0% { opacity: 1; transform: translate(-50%, 0) rotate(0); }
|
||||
100% { opacity: 0; transform: translate(calc(-50% + (var(--wire-confetti-x) - 50) * 0.9px), 9rem) rotate(540deg); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-confetti[data-running="true"] .wire-confetti__burst > i { animation-duration: 1ms; animation-delay: 0ms; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
component CopyMarkup {
|
||||
outputs {
|
||||
copy(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
success(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 {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Copy Markup"
|
||||
name: string = ""
|
||||
value: string = ""
|
||||
placeholder: string = ""
|
||||
type: string = "text"
|
||||
min: string = ""
|
||||
max: string = ""
|
||||
step: string = ""
|
||||
disabled: boolean = false
|
||||
required: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--copy-markup {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,103 @@
|
||||
component TreeView {
|
||||
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)
|
||||
toggle(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
expand(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
collapse(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
select(payload: { item: object; value: string; sourceEvent?: Event })
|
||||
toggle(payload: { item: object; value: string; expanded: boolean; sourceEvent?: Event })
|
||||
expand(payload: { item: object; value: string; sourceEvent?: Event })
|
||||
collapse(payload: { item: object; value: string; sourceEvent?: Event })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Tree View"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
valueKey: string = "value"
|
||||
labelKey: string = "label"
|
||||
defaultExpanded: unknown[] = []
|
||||
selected: string = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state expandedValues = defaultExpanded
|
||||
state selectedValue = selected
|
||||
|
||||
functions {
|
||||
shared function nodeValue(item, index, parent) { return String(item[valueKey] || item.id || parent + index) }
|
||||
shared function isExpanded(value) { return expandedValues.includes(value) }
|
||||
|
||||
client function toggleNode(item, value, sourceEvent, next, expanded) {
|
||||
next = expandedValues.slice()
|
||||
expanded = !next.includes(value)
|
||||
if (expanded) {
|
||||
next.push(value)
|
||||
} else {
|
||||
next = next.filter(function (entry) { return entry !== value })
|
||||
}
|
||||
expandedValues = next
|
||||
output.toggle({ item: item, value: value, expanded: expanded, sourceEvent: sourceEvent })
|
||||
if (expanded) {
|
||||
output.expand({ item: item, value: value, sourceEvent: sourceEvent })
|
||||
} else {
|
||||
output.collapse({ item: item, value: value, sourceEvent: sourceEvent })
|
||||
}
|
||||
}
|
||||
|
||||
client function selectNode(item, value, sourceEvent) {
|
||||
selectedValue = value
|
||||
output.select({ item: item, value: value, sourceEvent: sourceEvent })
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--tree-view wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<section {...attrs} data-ui-component="TreeView" class='wire-tree-view {class}'>
|
||||
{#if title}<strong class="wire-tree-view__title">{title}</strong>{/if}
|
||||
{#if description}<p class="wire-tree-view__description">{description}</p>{/if}
|
||||
<div class="wire-tree-view__tree" role="tree" aria-label='{title}' data-wrn-roving="vertical">
|
||||
{#each items as item, index}
|
||||
<div class="wire-tree-view__branch" role="none">
|
||||
<div class="wire-tree-view__row" role="treeitem" aria-level="1" aria-expanded='{item.children && item.children.length ? isExpanded(nodeValue(item, index, "root-")) : ""}' aria-selected='{selectedValue === nodeValue(item, index, "root-") ? "true" : "false"}'>
|
||||
{#if item.children && item.children.length}
|
||||
<button type="button" class="wire-tree-view__toggle" aria-label='{isExpanded(nodeValue(item, index, "root-")) ? "Collapse " + item[labelKey] : "Expand " + item[labelKey]}' @click='toggleNode(item, nodeValue(item, index, "root-"), event)'><span class="icon-[lucide--chevron-right]" aria-hidden="true"></span></button>
|
||||
{:else}<span class="wire-tree-view__toggle" aria-hidden="true"></span>{/if}
|
||||
<button type="button" class="wire-tree-view__node" data-wrn-roving-item @click='selectNode(item, nodeValue(item, index, "root-"), event)'>
|
||||
{#if item.icon}<span class='{item.icon}' aria-hidden="true"></span>{/if}<span>{item[labelKey]}</span>
|
||||
</button>
|
||||
</div>
|
||||
{#if item.children && item.children.length}
|
||||
<div class="wire-tree-view__group" role="group" data-show='isExpanded(nodeValue(item, index, "root-"))'>
|
||||
{#each item.children as child, childIndex}
|
||||
<div class="wire-tree-view__row" role="treeitem" aria-level="2" aria-selected='{selectedValue === nodeValue(child, childIndex, nodeValue(item, index, "root-") + "-") ? "true" : "false"}'>
|
||||
<span class="wire-tree-view__toggle" aria-hidden="true"></span>
|
||||
<button type="button" class="wire-tree-view__node" data-wrn-roving-item @click='selectNode(child, nodeValue(child, childIndex, nodeValue(item, index, "root-") + "-"), event)'>
|
||||
{#if child.icon}<span class='{child.icon}' aria-hidden="true"></span>{/if}<span>{child[labelKey]}</span>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:empty}<p class="wire-tree-view__empty">No tree items available.</p>{/each}
|
||||
</div>
|
||||
<slot />
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-tree-view { display: grid; gap: 0.5rem; color: var(--wire-color-text); }
|
||||
.wire-tree-view__title { font-size: 0.95rem; }
|
||||
.wire-tree-view__description, .wire-tree-view__empty { margin: 0; color: var(--wire-color-muted); font-size: 0.8rem; }
|
||||
.wire-tree-view__tree { display: grid; gap: 0.15rem; padding: 0.4rem; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius); background: var(--wire-color-surface); }
|
||||
.wire-tree-view__row { display: flex; min-width: 0; align-items: center; gap: 0.2rem; border-radius: var(--wire-radius-sm); }
|
||||
.wire-tree-view__row[aria-selected="true"] { background: var(--wire-color-primary-soft); }
|
||||
.wire-tree-view__toggle { display: inline-grid; width: 1.8rem; height: 1.8rem; flex: none; padding: 0; place-items: center; border: 0; border-radius: 0.35rem; color: var(--wire-color-muted); background: transparent; }
|
||||
button.wire-tree-view__toggle { cursor: pointer; }
|
||||
.wire-tree-view__toggle > span { width: 0.9rem; height: 0.9rem; transition: transform var(--wire-motion-fast); }
|
||||
.wire-tree-view__row[aria-expanded="true"] > .wire-tree-view__toggle > span { transform: rotate(90deg); }
|
||||
.wire-tree-view__node { display: flex; min-width: 0; flex: 1; align-items: center; gap: 0.45rem; padding: 0.45rem 0.5rem; border: 0; border-radius: var(--wire-radius-sm); color: inherit; background: transparent; font: inherit; text-align: left; cursor: pointer; }
|
||||
.wire-tree-view__node:hover, button.wire-tree-view__toggle:hover { background: var(--wire-color-surface-2); }
|
||||
.wire-tree-view__node:focus-visible, button.wire-tree-view__toggle:focus-visible { outline: 2px solid var(--wire-color-focus); outline-offset: -2px; }
|
||||
.wire-tree-view__node > [class*="icon-"] { width: 1rem; height: 1rem; flex: none; color: var(--wire-color-primary); }
|
||||
.wire-tree-view__group { display: grid; margin-left: 1.8rem; padding-left: 0.45rem; border-left: 1px solid var(--wire-color-border); }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user