chore(release): prepare 0.8.6
Bumps all 47 packages, the root manifest and the VS Code extension to 0.8.6, and rebuilds the editor compiler, language server and extension bundles that embed the version. The release carries the output delivery fix: camelCase outputs now reach parent bindings, and 18 components emit through output.* instead of hand-built CustomEvents. See the 0.8.6 migration entry for what changes for consumers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+325
-53
@@ -1,90 +1,362 @@
|
||||
// Tabs -- a tablist over panels.
|
||||
//
|
||||
// <Tabs items='[{"label":"Overview","value":"overview"}]' active="overview" />
|
||||
//
|
||||
// With mode="url" the selection is mirrored into a query parameter using
|
||||
// history.pushState, so the panel swaps without a page load and the back
|
||||
// button works. The query parameter is used rather than the hash because it
|
||||
// survives a reload and does not collide with in-page anchors or Scrollspy.
|
||||
//
|
||||
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
||||
// and silently swallows the rule that follows it.
|
||||
component Tabs {
|
||||
outputs {
|
||||
change(payload: { value: string; item: object; index: number })
|
||||
select(payload: { value: string; item: object; index: number })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Tabs"
|
||||
size: string = "default"
|
||||
items: unknown[] = []
|
||||
active: string = ""
|
||||
orientation: string = "horizontal"
|
||||
mode: string = "client"
|
||||
param: string = "tab"
|
||||
label: string = "Tabs"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state activeValue = active || (items[0] ? (items[0].value || items[0].id || "0") : "")
|
||||
state activeValue = ""
|
||||
// Which way the panel should slide in. Set on every change so the animation
|
||||
// follows the direction of travel rather than always coming from one side.
|
||||
state slideFrom = "forward"
|
||||
|
||||
functions {
|
||||
shared function itemList() {
|
||||
return Array.isArray(items) ? items : []
|
||||
}
|
||||
|
||||
shared function valueOf(item, index) {
|
||||
return String(item.value || item.id || index)
|
||||
}
|
||||
|
||||
/*
|
||||
* In url mode the query parameter is the source of truth, not local state.
|
||||
*
|
||||
* The client router owns popstate and swaps the whole page on back and
|
||||
* forward, which discards component state anyway. Reading the URL means
|
||||
* the right tab simply falls out of whatever render happens next, with no
|
||||
* listener to lose and nothing to keep in step.
|
||||
*/
|
||||
shared function currentValue() {
|
||||
if (mode === "url" && typeof window !== "undefined" && window.location) {
|
||||
var fromUrl = new URLSearchParams(window.location.search).get(param || "tab")
|
||||
return fromUrl ? fromUrl : defaultValue()
|
||||
}
|
||||
if (activeValue) {
|
||||
return activeValue
|
||||
}
|
||||
return defaultValue()
|
||||
}
|
||||
|
||||
// The selection this instance started with. The runtime falls back to it
|
||||
// when the back button lands on a URL that has no tab parameter at all.
|
||||
shared function defaultValue() {
|
||||
if (active) {
|
||||
return active
|
||||
}
|
||||
var list = itemList()
|
||||
return list.length ? valueOf(list[0], 0) : ""
|
||||
}
|
||||
|
||||
shared function isSelected(item, index) {
|
||||
return valueOf(item, index) === currentValue()
|
||||
}
|
||||
|
||||
shared function rovingAxis() {
|
||||
return orientation === "vertical" ? "vertical" : "horizontal"
|
||||
}
|
||||
|
||||
client function selectTab(item, index, sourceEvent) {
|
||||
if (item.disabled) {
|
||||
return
|
||||
}
|
||||
var value = valueOf(item, index)
|
||||
var list = itemList()
|
||||
var previous = -1
|
||||
for (var scan = 0; scan < list.length; scan += 1) {
|
||||
if (valueOf(list[scan], scan) === currentValue()) {
|
||||
previous = scan
|
||||
}
|
||||
}
|
||||
slideFrom = previous > index ? "back" : "forward"
|
||||
activeValue = value
|
||||
|
||||
if (mode === "url" && window.history && window.history.pushState) {
|
||||
var url = new URL(window.location.href)
|
||||
url.searchParams.set(param || "tab", value)
|
||||
window.history.pushState({}, "", url.toString())
|
||||
}
|
||||
|
||||
output.change({ value: value, item: item, index: index })
|
||||
output.select({ value: value, item: item, index: index })
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<section
|
||||
{...attrs}
|
||||
data-ui-component="Tabs"
|
||||
class='w-full {class}'
|
||||
class:flex='orientation === "vertical"'
|
||||
class:items-start='orientation === "vertical"'
|
||||
class:gap-6='orientation === "vertical"'
|
||||
class='wire-tabs {class}'
|
||||
data-orientation='{orientation}'
|
||||
data-color='{color}'
|
||||
data-size='{size}'
|
||||
data-slide='{slideFrom}'
|
||||
data-mode='{mode}'
|
||||
data-param='{param}'
|
||||
>
|
||||
<div
|
||||
class="wire-tabs__list"
|
||||
role="tablist"
|
||||
aria-label='{label}'
|
||||
aria-orientation='{orientation}'
|
||||
class="flex min-w-0 gap-1 overflow-x-auto rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-soft)] p-1"
|
||||
class:flex-col='orientation === "vertical"'
|
||||
class:w-56='orientation === "vertical"'
|
||||
class:shrink-0='orientation === "vertical"'
|
||||
data-wrn-roving='{rovingAxis()}'
|
||||
>
|
||||
{#each items as item, index}
|
||||
{#each itemList() as item, index}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-tabs__tab"
|
||||
role="tab"
|
||||
id='tab-{item.value || item.id || index}'
|
||||
aria-selected='{activeValue === (item.value || item.id || String(index))}'
|
||||
aria-controls='panel-{item.value || item.id || index}'
|
||||
tabindex='{activeValue === (item.value || item.id || String(index)) ? "0" : "-1"}'
|
||||
disabled='{item.disabled || false}'
|
||||
class="inline-flex min-w-max flex-1 items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-semibold outline-none transition disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class:bg-[var(--wire-color-surface-raised)]='activeValue === (item.value || item.id || String(index))'
|
||||
class:text-[var(--wire-color-primary)]='activeValue === (item.value || item.id || String(index))'
|
||||
class:shadow-sm='activeValue === (item.value || item.id || String(index))'
|
||||
class:text-[var(--wire-color-text-muted)]='activeValue !== (item.value || item.id || String(index))'
|
||||
class:hover:text-[var(--wire-color-text)]='activeValue !== (item.value || item.id || String(index))'
|
||||
class:justify-start='orientation === "vertical"'
|
||||
class:px-3='size === "sm"'
|
||||
class:py-2='size === "sm"'
|
||||
class:px-5='size === "lg"'
|
||||
class:py-3='size === "lg"'
|
||||
@click='activeValue = item.value || item.id || String(index); event.currentTarget.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: { item: item, index: index, value: activeValue } })); event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, index: index, value: activeValue } }))'
|
||||
data-value='{valueOf(item, index)}'
|
||||
data-wrn-roving-item="true"
|
||||
id='wire-tab-{valueOf(item, index)}'
|
||||
aria-controls='wire-panel-{valueOf(item, index)}'
|
||||
aria-selected='{isSelected(item, index) ? "true" : "false"}'
|
||||
aria-disabled='{item.disabled ? "true" : "false"}'
|
||||
@click='selectTab(item, index, event)'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='{item.icon + " size-4"}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{item.label || item.title}</span>
|
||||
{#if item.badge}
|
||||
<span class="rounded-full bg-[var(--wire-color-primary-soft)] px-2 py-0.5 text-xs text-[var(--wire-color-primary)]">{item.badge}</span>
|
||||
{/if}
|
||||
<span class='wire-tabs__icon {item.icon}' data-show="item.icon" aria-hidden="true"></span>
|
||||
<span class="wire-tabs__label">{item.label || item.title}</span>
|
||||
<span class="wire-tabs__badge" data-show="item.badge">{item.badge}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1 pt-4" class:pt-0='orientation === "vertical"'>
|
||||
{#each items as item, index}
|
||||
<div class="wire-tabs__panels">
|
||||
{#each itemList() as item, index}
|
||||
<div
|
||||
class="wire-tabs__panel"
|
||||
role="tabpanel"
|
||||
id='panel-{item.value || item.id || index}'
|
||||
aria-labelledby='tab-{item.value || item.id || index}'
|
||||
id='wire-panel-{valueOf(item, index)}'
|
||||
aria-labelledby='wire-tab-{valueOf(item, index)}'
|
||||
tabindex="0"
|
||||
data-show='activeValue === (item.value || item.id || String(index))'
|
||||
class="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
|
||||
data-show='isSelected(item, index)'
|
||||
>
|
||||
{#if item.title && item.title !== item.label}
|
||||
<h3 class="text-lg font-bold text-[var(--wire-color-text)]">{item.title}</h3>
|
||||
{/if}
|
||||
{#if item.description}
|
||||
<p class="mt-2 leading-7 text-[var(--wire-color-text-muted)]">{item.description}</p>
|
||||
{/if}
|
||||
{#if item.content}
|
||||
<div class="mt-4 text-[var(--wire-color-text)]">{item.content}</div>
|
||||
{/if}
|
||||
<h3 class="wire-tabs__title" data-show="item.title && item.title !== item.label">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p class="wire-tabs__description" data-show="item.description">{item.description}</p>
|
||||
<div class="wire-tabs__content" data-show="item.content">{item.content}</div>
|
||||
<slot name="panel-{valueOf(item, index)}"></slot>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-tabs {
|
||||
--tabs-accent: var(--wire-color-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.wire-tabs[data-color="secondary"] {
|
||||
--tabs-accent: var(--wire-color-secondary);
|
||||
}
|
||||
|
||||
.wire-tabs[data-color="success"] {
|
||||
--tabs-accent: var(--wire-color-success);
|
||||
}
|
||||
|
||||
.wire-tabs[data-color="danger"] {
|
||||
--tabs-accent: var(--wire-color-danger);
|
||||
}
|
||||
|
||||
.wire-tabs[data-color="info"] {
|
||||
--tabs-accent: var(--wire-color-info);
|
||||
}
|
||||
|
||||
.wire-tabs[data-size="sm"] {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.wire-tabs[data-size="lg"] {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.wire-tabs[data-orientation="vertical"] {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.wire-tabs__list {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
min-width: 0;
|
||||
padding: 0.25rem;
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: var(--wire-radius-md);
|
||||
background: var(--wire-color-surface-soft);
|
||||
/* A long tablist scrolls rather than wrapping into an unusable stack. */
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.wire-tabs[data-orientation="vertical"] .wire-tabs__list {
|
||||
flex-direction: column;
|
||||
flex: 0 0 auto;
|
||||
width: 14rem;
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
.wire-tabs__tab {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
flex: 1 0 auto;
|
||||
padding: 0.55rem 0.9rem;
|
||||
border: 0;
|
||||
border-radius: var(--wire-radius-sm);
|
||||
background: transparent;
|
||||
color: var(--wire-color-text-muted);
|
||||
font: inherit;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 160ms ease,
|
||||
color 160ms ease;
|
||||
}
|
||||
|
||||
.wire-tabs[data-orientation="vertical"] .wire-tabs__tab {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.wire-tabs__tab:hover {
|
||||
color: var(--wire-color-text);
|
||||
}
|
||||
|
||||
.wire-tabs__tab:focus-visible {
|
||||
outline: 2px solid var(--tabs-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.wire-tabs__tab[aria-selected="true"] {
|
||||
background: var(--wire-color-surface);
|
||||
color: var(--tabs-accent);
|
||||
box-shadow: var(--wire-shadow-1);
|
||||
}
|
||||
|
||||
.wire-tabs__tab[aria-disabled="true"] {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wire-tabs__badge {
|
||||
padding: 0.05rem 0.4rem;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--tabs-accent) 16%, transparent);
|
||||
color: var(--tabs-accent);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.wire-tabs__panels {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.wire-tabs__panel {
|
||||
outline: none;
|
||||
animation: wire-tabs-slide-forward 220ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
.wire-tabs[data-slide="back"] .wire-tabs__panel {
|
||||
animation-name: wire-tabs-slide-back;
|
||||
}
|
||||
|
||||
/* The panels clip their own slide so it never widens the page. */
|
||||
.wire-tabs__panels {
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.wire-tabs__panel:focus-visible {
|
||||
outline: 2px solid var(--tabs-accent);
|
||||
outline-offset: 4px;
|
||||
border-radius: var(--wire-radius-sm);
|
||||
}
|
||||
|
||||
.wire-tabs__title {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wire-tabs__description {
|
||||
margin: 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.wire-tabs__content {
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
|
||||
@keyframes wire-tabs-slide-forward {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(1.25rem);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wire-tabs-slide-back {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-1.25rem);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Respect a reduced-motion preference: swap instantly instead. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-tabs__panel {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-tabs[data-orientation="vertical"] {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wire-tabs[data-orientation="vertical"] .wire-tabs__list {
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user