component ToggleCount { outputs { change(payload: { value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null) toggle(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object }) } props { size: string = "default" color: string = "primary" variant: string = "segmented" class: string = "" name: string = "billing-cycle" value: string = "monthly" firstValue: string = "monthly" firstLabel: string = "Monthly" secondValue: string = "annual" secondLabel: string = "Annual" ariaLabel: string = "Billing frequency" items: unknown[] = [] currency: string = "$" suffix: string = "" firstValueKey: string = "monthly" secondValueKey: string = "annual" emptyValue: string = "—" align: string = "end" fullWidth: boolean = true disabled: boolean = false animate: boolean = true animationDuration: number = 450 animationSteps: number = 18 } state selectedValue = value functions { shared function isFirstSelected() { return selectedValue === firstValue } shared function isSecondSelected() { return selectedValue === secondValue } shared function displayValue(item) { if (isSecondSelected()) { return item[secondValueKey] !== undefined ? item[secondValueKey] : emptyValue } return item[firstValueKey] !== undefined ? item[firstValueKey] : emptyValue } client function dispatchToggleEvent(sourceEvent, previousValue, payload) { payload = { component: "ToggleCount", name: name, value: selectedValue, previousValue: previousValue, firstValue: firstValue, secondValue: secondValue } output.change(payload) output.toggle(payload) } client function selectValue(sourceEvent, nextValue, previousValue) { if (disabled || selectedValue === nextValue) { return } previousValue = selectedValue selectedValue = nextValue if ( animate && !window.matchMedia("(prefers-reduced-motion: reduce)").matches ) { animateDisplayedValues(sourceEvent, previousValue) } dispatchToggleEvent(sourceEvent, previousValue) } client function animateDisplayedValues(sourceEvent, previousValue, root, nodes, animationToken) { root = sourceEvent.currentTarget.closest("[data-wrn-toggle-count]") if (!root) { return } animationToken = String(Date.now()) + ":" + String(selectedValue) root.setAttribute("data-animation-token", animationToken) nodes = root.querySelectorAll("[data-toggle-count-value]") animateValueAt(nodes, 0, previousValue, root, animationToken) } client function animateValueAt( nodes, index, previousValue, root, animationToken, node, fromValue, toValue ) { if (index >= nodes.length) { return } node = nodes[index] fromValue = Number( previousValue === secondValue ? node.getAttribute("data-second-value") : node.getAttribute("data-first-value") ) toValue = Number( selectedValue === secondValue ? node.getAttribute("data-second-value") : node.getAttribute("data-first-value") ) if (!Number.isNaN(fromValue) && !Number.isNaN(toValue)) { node.replaceChildren(String(fromValue)) scheduleValueFrame( node, fromValue, toValue, 1, Math.max(1, Number(animationSteps) || 1), Math.max(1, Number(animationDuration) || 1), root, animationToken ) } animateValueAt( nodes, index + 1, previousValue, root, animationToken ) } client function scheduleValueFrame( node, fromValue, toValue, frame, totalFrames, duration, root, animationToken, nextValue ) { if (frame > totalFrames) { return } nextValue = Math.round( fromValue + (toValue - fromValue) * (frame / totalFrames) ) setTimeout( applyAnimatedValue, Math.max(1, duration * frame / totalFrames), node, nextValue, root, animationToken ) scheduleValueFrame( node, fromValue, toValue, frame + 1, totalFrames, duration, root, animationToken ) } client function applyAnimatedValue( node, nextValue, root, animationToken ) { if ( root && root.getAttribute("data-animation-token") !== animationToken ) { return } node.replaceChildren(String(nextValue)) } client function toggleValue(sourceEvent) { selectValue( sourceEvent, isFirstSelected() ? secondValue : firstValue ) } } view {
{#if variant === "switch"} {firstLabel} {secondLabel} {:else}
{/if}
{#if items.length > 0}
{#each items as item}
{item.label || item.name} {#if currency}{currency}{/if} {displayValue(item)} {#if suffix}{suffix}{/if} {#if item.description}{item.description}{/if}
{/each}
{/if}
} style { .wire-next--toggle-count { --wire-toggle-count-accent: var(--wire-component-color); display: grid; width: fit-content; gap: 0.75rem; color: var(--wire-color-text); } .wire-next--toggle-count-full { width: 100%; } .wire-next__toggle-count-control { display: flex; } .wire-next__toggle-count-segmented { display: inline-flex; gap: 0.2rem; padding: 0.2rem; border-radius: calc(var(--wire-radius-sm) + 0.15rem); background: var(--wire-color-surface-2); box-shadow: inset 0 0 0 1px var(--wire-color-border); } .wire-next__toggle-count-segmented button { min-height: 2.35rem; padding: 0.45rem 0.8rem; border: 0; border-radius: var(--wire-radius-sm); background: transparent; color: var(--wire-color-muted); font: inherit; font-size: 0.78rem; font-weight: 700; cursor: pointer; } .wire-next__toggle-count-segmented button[aria-pressed="true"] { background: var(--wire-color-surface); color: var(--wire-color-text); box-shadow: var(--wire-shadow-1), inset 0 -2px var(--wire-toggle-count-accent); } .wire-next__toggle-count-segmented button:focus-visible, .wire-next__toggle-count-switch:focus-visible { outline: 2px solid var(--wire-toggle-count-accent); outline-offset: 2px; } .wire-next__toggle-count-segmented button:disabled, .wire-next__toggle-count-switch:disabled { cursor: not-allowed; } .wire-next__toggle-count-control:has(.wire-next__toggle-count-switch) { align-items: center; gap: 0.7rem; font-size: 0.78rem; font-weight: 700; } .wire-next__toggle-count-control > span { color: var(--wire-color-muted); } .wire-next__toggle-count-control > span[data-selected="true"] { color: var(--wire-color-text); } .wire-next__toggle-count-switch { position: relative; width: 2.65rem; height: 1.45rem; padding: 0.15rem; border: 1px solid var(--wire-color-border); border-radius: 999px; background: var(--wire-color-surface-2); cursor: pointer; transition: background-color var(--wire-motion-fast) var(--wire-ease-standard); } .wire-next__toggle-count-switch > span { display: block; width: 1rem; height: 1rem; border-radius: 50%; background: var(--wire-color-text); box-shadow: var(--wire-shadow-1); transition: transform var(--wire-motion-fast) var(--wire-ease-standard); } .wire-next__toggle-count-switch[aria-checked="true"] { border-color: var(--wire-toggle-count-accent); background: var(--wire-toggle-count-accent); } .wire-next__toggle-count-switch[aria-checked="true"] > span { background: white; transform: translateX(1.15rem); } .wire-next__toggle-count-items { display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); overflow: hidden; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius-md); background: var(--wire-color-surface); } .wire-next__toggle-count-items article { display: grid; align-content: start; gap: 0.35rem; min-width: 0; padding: 1rem; } .wire-next__toggle-count-items article + article { border-left: 1px solid var(--wire-color-border); } .wire-next__toggle-count-items article > span { color: var(--wire-color-muted); font-size: 0.75rem; font-weight: 600; } .wire-next__toggle-count-items strong { display: flex; align-items: baseline; gap: 0.2rem; font-size: 1.5rem; line-height: 1; } .wire-next__toggle-count-items [data-toggle-count-value] { min-width: 2ch; font-variant-numeric: tabular-nums; transition: color var(--wire-motion-fast) var(--wire-ease-standard), transform var(--wire-motion-fast) var(--wire-ease-standard); } .wire-next__toggle-count-items strong small { color: inherit; font-size: 0.65em; } .wire-next__toggle-count-items article > small { color: var(--wire-color-muted); font-size: 0.68rem; } @media (max-width: 36rem) { .wire-next__toggle-count-control { justify-content: center; } .wire-next__toggle-count-items { grid-template-columns: 1fr; } .wire-next__toggle-count-items article + article { border-top: 1px solid var(--wire-color-border); border-left: 0; } } @media (prefers-reduced-motion: reduce) { .wire-next__toggle-count-items [data-toggle-count-value] { transition: none; } } } }