Files
WRNexusJS/.publish/ui/components/ToggleCount.wrn
T
Clintchiz db612df6cd
Quality / quality (ubuntu-latest) (push) Failing after 14m12s
Quality / quality (windows-latest) (push) Canceled after 0s
chore(release): refresh private package staging
2026-08-10 00:55:12 +05:30

475 lines
13 KiB
Plaintext

import ComponentBaseStyles from "../styles/ComponentBaseStyles.wrn"
component ToggleCount {
outputs {
change(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 })
}
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 {
<section
{...attrs}
data-wrn-toggle-count
data-variant="{variant}"
data-value="{selectedValue}"
data-disabled="{disabled ? 'true' : 'false'}"
class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--toggle-count wire-next--toggle-count-{variant} {fullWidth ? 'wire-next--toggle-count-full' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
>
<input type="hidden" name="{name}" value="{selectedValue}" />
<div class="wire-next__toggle-count-control wire-next__toggle-count-control--{align}">
{#if variant === "switch"}
<span data-selected="{isFirstSelected() ? 'true' : 'false'}">{firstLabel}</span>
<button
type="button"
role="switch"
aria-label="{ariaLabel}"
aria-checked="{isSecondSelected() ? 'true' : 'false'}"
disabled="{disabled}"
@click="toggleValue(event)"
class="wire-next__toggle-count-switch"
>
<span aria-hidden="true"></span>
</button>
<span data-selected="{isSecondSelected() ? 'true' : 'false'}">{secondLabel}</span>
{:else}
<div class="wire-next__toggle-count-segmented" role="group" aria-label="{ariaLabel}">
<button
type="button"
aria-pressed="{isFirstSelected() ? 'true' : 'false'}"
disabled="{disabled}"
@click="selectValue(event, firstValue)"
>{firstLabel}</button>
<button
type="button"
aria-pressed="{isSecondSelected() ? 'true' : 'false'}"
disabled="{disabled}"
@click="selectValue(event, secondValue)"
>{secondLabel}</button>
</div>
{/if}
</div>
{#if items.length > 0}
<div class="wire-next__toggle-count-items">
{#each items as item}
<article>
<span>{item.label || item.name}</span>
<strong>
{#if currency}<small>{currency}</small>{/if}
<span
data-toggle-count-value
data-first-value="{item[firstValueKey]}"
data-second-value="{item[secondValueKey]}"
>{displayValue(item)}</span>
{#if suffix}<small>{suffix}</small>{/if}
</strong>
{#if item.description}<small>{item.description}</small>{/if}
</article>
{/each}
</div>
{/if}
</section>
<ComponentBaseStyles hidden />
}
style {
.wire-next__toggle-count-control--start {
justify-content: flex-start;
}
.wire-next__toggle-count-control--center {
justify-content: center;
}
.wire-next__toggle-count-control--end {
justify-content: flex-end;
}
.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;
}
}
}
}