diff --git a/README.md b/README.md index 8c2bff2..369a9bd 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Add `corex` to your `mix.exs` dependencies: ```elixir def deps do [ - {:corex, "~> 0.1.0-alpha.22"} + {:corex, "~> 0.1.0-alpha.23"} ] end ``` diff --git a/assets/components/angle-slider.ts b/assets/components/angle-slider.ts new file mode 100644 index 0000000..5e3da02 --- /dev/null +++ b/assets/components/angle-slider.ts @@ -0,0 +1,69 @@ +import { connect, machine, type Props, type Api } from "@zag-js/angle-slider"; +import { VanillaMachine, normalizeProps } from "@zag-js/vanilla"; +import { Component } from "../lib/core"; + +export class AngleSlider extends Component { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initMachine(props: Props): VanillaMachine { + return new VanillaMachine(machine, props); + } + + initApi(): Api { + return connect(this.machine.service, normalizeProps); + } + + render(): void { + const rootEl = + this.el.querySelector('[data-scope="angle-slider"][data-part="root"]') ?? + this.el; + this.spreadProps(rootEl, this.api.getRootProps()); + + const labelEl = this.el.querySelector( + '[data-scope="angle-slider"][data-part="label"]' + ); + if (labelEl) this.spreadProps(labelEl, this.api.getLabelProps()); + + const hiddenInputEl = this.el.querySelector( + '[data-scope="angle-slider"][data-part="hidden-input"]' + ); + if (hiddenInputEl) this.spreadProps(hiddenInputEl, this.api.getHiddenInputProps()); + + const controlEl = this.el.querySelector( + '[data-scope="angle-slider"][data-part="control"]' + ); + if (controlEl) this.spreadProps(controlEl, this.api.getControlProps()); + + const thumbEl = this.el.querySelector( + '[data-scope="angle-slider"][data-part="thumb"]' + ); + if (thumbEl) this.spreadProps(thumbEl, this.api.getThumbProps()); + + const valueTextEl = this.el.querySelector( + '[data-scope="angle-slider"][data-part="value-text"]' + ); + if (valueTextEl) { + this.spreadProps(valueTextEl, this.api.getValueTextProps()); + const valueSpan = valueTextEl.querySelector( + '[data-scope="angle-slider"][data-part="value"]' + ); + if (valueSpan && valueSpan.textContent !== String(this.api.value)) { + valueSpan.textContent = String(this.api.value); + } + } + + const markerGroupEl = this.el.querySelector( + '[data-scope="angle-slider"][data-part="marker-group"]' + ); + if (markerGroupEl) this.spreadProps(markerGroupEl, this.api.getMarkerGroupProps()); + + this.el + .querySelectorAll('[data-scope="angle-slider"][data-part="marker"]') + .forEach((markerEl) => { + const valueStr = markerEl.dataset.value; + if (valueStr == null) return; + const value = Number(valueStr); + if (Number.isNaN(value)) return; + this.spreadProps(markerEl, this.api.getMarkerProps({ value })); + }); + } +} diff --git a/assets/components/avatar.ts b/assets/components/avatar.ts new file mode 100644 index 0000000..db4d37a --- /dev/null +++ b/assets/components/avatar.ts @@ -0,0 +1,39 @@ +import { connect, machine, type Props, type Api } from "@zag-js/avatar"; +import { VanillaMachine, normalizeProps } from "@zag-js/vanilla"; +import { Component } from "../lib/core"; + +export class Avatar extends Component { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initMachine(props: Props): VanillaMachine { + return new VanillaMachine(machine, props); + } + + initApi(): Api { + return connect(this.machine.service, normalizeProps); + } + + render(): void { + const rootEl = + this.el.querySelector('[data-scope="avatar"][data-part="root"]') ?? this.el; + this.spreadProps(rootEl, this.api.getRootProps()); + + const imageEl = this.el.querySelector('[data-scope="avatar"][data-part="image"]'); + if (imageEl) this.spreadProps(imageEl, this.api.getImageProps()); + + const fallbackEl = this.el.querySelector( + '[data-scope="avatar"][data-part="fallback"]' + ); + if (fallbackEl) this.spreadProps(fallbackEl, this.api.getFallbackProps()); + + const skeletonEl = this.el.querySelector( + '[data-scope="avatar"][data-part="skeleton"]' + ); + if (skeletonEl) { + const state = this.machine.service.state; + const loaded = state.matches("loaded"); + const error = state.matches("error"); + skeletonEl.hidden = loaded || error; + skeletonEl.setAttribute("data-state", loaded || error ? "hidden" : "visible"); + } + } +} diff --git a/assets/components/carousel.ts b/assets/components/carousel.ts new file mode 100644 index 0000000..c6821a7 --- /dev/null +++ b/assets/components/carousel.ts @@ -0,0 +1,73 @@ +import { connect, machine, type Props, type Api } from "@zag-js/carousel"; +import type { ItemProps, IndicatorProps } from "@zag-js/carousel"; +import { VanillaMachine, normalizeProps } from "@zag-js/vanilla"; +import { Component } from "../lib/core"; + +export class Carousel extends Component { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initMachine(props: Props): VanillaMachine { + return new VanillaMachine(machine, props); + } + + initApi(): Api { + return connect(this.machine.service, normalizeProps); + } + + render(): void { + const rootEl = + this.el.querySelector('[data-scope="carousel"][data-part="root"]') ?? this.el; + this.spreadProps(rootEl, this.api.getRootProps()); + + const controlEl = this.el.querySelector( + '[data-scope="carousel"][data-part="control"]' + ); + if (controlEl) this.spreadProps(controlEl, this.api.getControlProps()); + + const itemGroupEl = this.el.querySelector( + '[data-scope="carousel"][data-part="item-group"]' + ); + if (itemGroupEl) this.spreadProps(itemGroupEl, this.api.getItemGroupProps()); + + const slideCount = Number(this.el.dataset.slideCount) || 0; + for (let i = 0; i < slideCount; i++) { + const itemEl = this.el.querySelector( + `[data-scope="carousel"][data-part="item"][data-index="${i}"]` + ); + if (itemEl) this.spreadProps(itemEl, this.api.getItemProps({ index: i } as ItemProps)); + } + + const prevTriggerEl = this.el.querySelector( + '[data-scope="carousel"][data-part="prev-trigger"]' + ); + if (prevTriggerEl) this.spreadProps(prevTriggerEl, this.api.getPrevTriggerProps()); + + const nextTriggerEl = this.el.querySelector( + '[data-scope="carousel"][data-part="next-trigger"]' + ); + if (nextTriggerEl) this.spreadProps(nextTriggerEl, this.api.getNextTriggerProps()); + + const autoplayTriggerEl = this.el.querySelector( + '[data-scope="carousel"][data-part="autoplay-trigger"]' + ); + if (autoplayTriggerEl) this.spreadProps(autoplayTriggerEl, this.api.getAutoplayTriggerProps()); + + const indicatorGroupEl = this.el.querySelector( + '[data-scope="carousel"][data-part="indicator-group"]' + ); + if (indicatorGroupEl) this.spreadProps(indicatorGroupEl, this.api.getIndicatorGroupProps()); + + const indicatorCount = this.api.pageSnapPoints.length; + for (let i = 0; i < indicatorCount; i++) { + const indicatorEl = this.el.querySelector( + `[data-scope="carousel"][data-part="indicator"][data-index="${i}"]` + ); + if (indicatorEl) + this.spreadProps(indicatorEl, this.api.getIndicatorProps({ index: i } as IndicatorProps)); + } + + const progressTextEl = this.el.querySelector( + '[data-scope="carousel"][data-part="progress-text"]' + ); + if (progressTextEl) this.spreadProps(progressTextEl, this.api.getProgressTextProps()); + } +} diff --git a/assets/components/combobox.ts b/assets/components/combobox.ts index b872aed..627b105 100644 --- a/assets/components/combobox.ts +++ b/assets/components/combobox.ts @@ -31,7 +31,7 @@ export class Combobox extends Component { itemToValue: (item: ComboboxItem) => item.id ?? "", itemToString: (item: ComboboxItem) => item.label, isItemDisabled: (item: ComboboxItem) => item.disabled ?? false, - groupBy: (item: ComboboxItem) => item.group, + groupBy: (item: ComboboxItem) => item.group ?? "", }); } diff --git a/assets/components/editable.ts b/assets/components/editable.ts new file mode 100644 index 0000000..5bd2480 --- /dev/null +++ b/assets/components/editable.ts @@ -0,0 +1,53 @@ +import { connect, machine, type Props, type Api } from "@zag-js/editable"; +import { VanillaMachine, normalizeProps } from "@zag-js/vanilla"; +import { Component } from "../lib/core"; + +export class Editable extends Component { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initMachine(props: Props): VanillaMachine { + return new VanillaMachine(machine, props); + } + + initApi(): Api { + return connect(this.machine.service, normalizeProps); + } + + render(): void { + const rootEl = + this.el.querySelector('[data-scope="editable"][data-part="root"]') ?? this.el; + this.spreadProps(rootEl, this.api.getRootProps()); + + const areaEl = this.el.querySelector('[data-scope="editable"][data-part="area"]'); + if (areaEl) this.spreadProps(areaEl, this.api.getAreaProps()); + + const labelEl = this.el.querySelector( + '[data-scope="editable"][data-part="label"]' + ); + if (labelEl) this.spreadProps(labelEl, this.api.getLabelProps()); + + const inputEl = this.el.querySelector( + '[data-scope="editable"][data-part="input"]' + ); + if (inputEl) this.spreadProps(inputEl, this.api.getInputProps()); + + const previewEl = this.el.querySelector( + '[data-scope="editable"][data-part="preview"]' + ); + if (previewEl) this.spreadProps(previewEl, this.api.getPreviewProps()); + + const editTriggerEl = this.el.querySelector( + '[data-scope="editable"][data-part="edit-trigger"]' + ); + if (editTriggerEl) this.spreadProps(editTriggerEl, this.api.getEditTriggerProps()); + + const submitTriggerEl = this.el.querySelector( + '[data-scope="editable"][data-part="submit-trigger"]' + ); + if (submitTriggerEl) this.spreadProps(submitTriggerEl, this.api.getSubmitTriggerProps()); + + const cancelTriggerEl = this.el.querySelector( + '[data-scope="editable"][data-part="cancel-trigger"]' + ); + if (cancelTriggerEl) this.spreadProps(cancelTriggerEl, this.api.getCancelTriggerProps()); + } +} diff --git a/assets/components/floating-panel.ts b/assets/components/floating-panel.ts new file mode 100644 index 0000000..3f57b7d --- /dev/null +++ b/assets/components/floating-panel.ts @@ -0,0 +1,83 @@ +import { connect, machine, type Props, type Api } from "@zag-js/floating-panel"; +import type { ResizeTriggerProps, StageTriggerProps } from "@zag-js/floating-panel"; +import { VanillaMachine, normalizeProps } from "@zag-js/vanilla"; +import { Component } from "../lib/core"; + +export class FloatingPanel extends Component { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initMachine(props: Props): VanillaMachine { + return new VanillaMachine(machine, props); + } + + initApi(): Api { + return connect(this.machine.service, normalizeProps); + } + + render(): void { + const triggerEl = this.el.querySelector( + '[data-scope="floating-panel"][data-part="trigger"]' + ); + if (triggerEl) this.spreadProps(triggerEl, this.api.getTriggerProps()); + + const positionerEl = this.el.querySelector( + '[data-scope="floating-panel"][data-part="positioner"]' + ); + if (positionerEl) this.spreadProps(positionerEl, this.api.getPositionerProps()); + + const contentEl = this.el.querySelector( + '[data-scope="floating-panel"][data-part="content"]' + ); + if (contentEl) this.spreadProps(contentEl, this.api.getContentProps()); + + const titleEl = this.el.querySelector( + '[data-scope="floating-panel"][data-part="title"]' + ); + if (titleEl) this.spreadProps(titleEl, this.api.getTitleProps()); + + const headerEl = this.el.querySelector( + '[data-scope="floating-panel"][data-part="header"]' + ); + if (headerEl) this.spreadProps(headerEl, this.api.getHeaderProps()); + + const bodyEl = this.el.querySelector( + '[data-scope="floating-panel"][data-part="body"]' + ); + if (bodyEl) this.spreadProps(bodyEl, this.api.getBodyProps()); + + const dragTriggerEl = this.el.querySelector( + '[data-scope="floating-panel"][data-part="drag-trigger"]' + ); + if (dragTriggerEl) this.spreadProps(dragTriggerEl, this.api.getDragTriggerProps()); + + const resizeAxes = ["s", "w", "e", "n", "sw", "nw", "se", "ne"] as const; + resizeAxes.forEach((axis) => { + const resizeEl = this.el.querySelector( + `[data-scope="floating-panel"][data-part="resize-trigger"][data-axis="${axis}"]` + ); + if (resizeEl) + this.spreadProps(resizeEl, this.api.getResizeTriggerProps({ axis } as ResizeTriggerProps)); + }); + + const closeTriggerEl = this.el.querySelector( + '[data-scope="floating-panel"][data-part="close-trigger"]' + ); + if (closeTriggerEl) this.spreadProps(closeTriggerEl, this.api.getCloseTriggerProps()); + + const controlEl = this.el.querySelector( + '[data-scope="floating-panel"][data-part="control"]' + ); + if (controlEl) this.spreadProps(controlEl, this.api.getControlProps()); + + const stages = ["minimized", "maximized", "default"] as const; + stages.forEach((stage) => { + const stageTriggerEl = this.el.querySelector( + `[data-scope="floating-panel"][data-part="stage-trigger"][data-stage="${stage}"]` + ); + if (stageTriggerEl) + this.spreadProps( + stageTriggerEl, + this.api.getStageTriggerProps({ stage } as StageTriggerProps) + ); + }); + } +} diff --git a/assets/components/listbox.ts b/assets/components/listbox.ts new file mode 100644 index 0000000..de4edb8 --- /dev/null +++ b/assets/components/listbox.ts @@ -0,0 +1,143 @@ +import { connect, machine, collection, type Props, type Api } from "@zag-js/listbox"; +import type { ListCollection } from "@zag-js/collection"; +import { VanillaMachine, normalizeProps } from "@zag-js/vanilla"; +import { Component } from "../lib/core"; + +type Item = { + id?: string; + value?: string; + label: string; + disabled?: boolean; + group?: string; +}; + +export class Listbox extends Component, Api> { + private _options: Item[] = []; + hasGroups: boolean = false; + + constructor(el: HTMLElement | null, props: Props) { + super(el, props); + const collectionFromProps = (props as Props & { collection?: { items: Item[] } }) + .collection; + this._options = collectionFromProps?.items ?? []; + } + + get options(): Item[] { + return Array.isArray(this._options) ? this._options : []; + } + + setOptions(options: Item[]) { + this._options = Array.isArray(options) ? options : []; + } + + getCollection(): ListCollection { + const items = this.options; + if (this.hasGroups) { + return collection({ + items, + itemToValue: (item) => item.id ?? item.value ?? "", + itemToString: (item) => item.label, + isItemDisabled: (item) => !!item.disabled, + groupBy: (item: Item) => item.group ?? "", + }); + } + return collection({ + items, + itemToValue: (item) => item.id ?? item.value ?? "", + itemToString: (item) => item.label, + isItemDisabled: (item) => !!item.disabled, + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initMachine(props: Props): VanillaMachine { + const getCollection = this.getCollection.bind(this); + const collectionFromProps = (props as Props & { collection?: ListCollection }) + .collection; + return new VanillaMachine(machine, { + ...props, + get collection() { + return collectionFromProps ?? getCollection(); + }, + }); + } + + initApi(): Api { + return connect(this.machine.service, normalizeProps); + } + + init = (): void => { + this.machine.start(); + this.render(); + this.machine.subscribe(() => { + this.api = this.initApi(); + this.render(); + }); + }; + + applyItemProps(): void { + const contentEl = this.el.querySelector( + '[data-scope="listbox"][data-part="content"]' + ); + if (!contentEl) return; + + contentEl + .querySelectorAll('[data-scope="listbox"][data-part="item-group"]') + .forEach((groupEl) => { + const groupId = groupEl.dataset.id ?? ""; + this.spreadProps(groupEl, this.api.getItemGroupProps({ id: groupId })); + const labelEl = groupEl.querySelector( + '[data-scope="listbox"][data-part="item-group-label"]' + ); + if (labelEl) { + this.spreadProps(labelEl, this.api.getItemGroupLabelProps({ htmlFor: groupId })); + } + }); + + contentEl + .querySelectorAll('[data-scope="listbox"][data-part="item"]') + .forEach((itemEl) => { + const value = itemEl.dataset.value ?? ""; + const item = this.options.find((i) => String(i.id ?? i.value ?? "") === String(value)); + if (!item) return; + this.spreadProps(itemEl, this.api.getItemProps({ item })); + const textEl = itemEl.querySelector( + '[data-scope="listbox"][data-part="item-text"]' + ); + if (textEl) { + this.spreadProps(textEl, this.api.getItemTextProps({ item })); + } + const indicatorEl = itemEl.querySelector( + '[data-scope="listbox"][data-part="item-indicator"]' + ); + if (indicatorEl) { + this.spreadProps(indicatorEl, this.api.getItemIndicatorProps({ item })); + } + }); + } + + render(): void { + const rootEl = + this.el.querySelector('[data-scope="listbox"][data-part="root"]') ?? this.el; + this.spreadProps(rootEl, this.api.getRootProps()); + + const labelEl = this.el.querySelector('[data-scope="listbox"][data-part="label"]'); + if (labelEl) this.spreadProps(labelEl, this.api.getLabelProps()); + + const valueTextEl = this.el.querySelector( + '[data-scope="listbox"][data-part="value-text"]' + ); + if (valueTextEl) this.spreadProps(valueTextEl, this.api.getValueTextProps()); + + const inputEl = this.el.querySelector('[data-scope="listbox"][data-part="input"]'); + if (inputEl) this.spreadProps(inputEl, this.api.getInputProps()); + + const contentEl = this.el.querySelector( + '[data-scope="listbox"][data-part="content"]' + ); + if (contentEl) { + this.spreadProps(contentEl, this.api.getContentProps()); + this.applyItemProps(); + } + } +} diff --git a/assets/components/menu.ts b/assets/components/menu.ts index 7eef4e0..774c6d8 100644 --- a/assets/components/menu.ts +++ b/assets/components/menu.ts @@ -25,10 +25,6 @@ export class Menu extends Component { this.api.setParent(parent.machine.service); } - /** - * Check if an element belongs to THIS menu instance. - * Uses the nearest phx-hook="Menu" ancestor to determine ownership. - */ private isOwnElement(el: HTMLElement): boolean { const nearestHook = el.closest('[phx-hook="Menu"]'); return nearestHook === this.el; @@ -107,7 +103,6 @@ export class Menu extends Component { } }); - // Handle item groups const itemGroups = contentEl.querySelectorAll( '[data-scope="menu"][data-part="item-group"]' ); @@ -120,7 +115,6 @@ export class Menu extends Component { } }); - // Handle separators const separators = contentEl.querySelectorAll( '[data-scope="menu"][data-part="separator"]' ); diff --git a/assets/components/number-input.ts b/assets/components/number-input.ts new file mode 100644 index 0000000..39e1f96 --- /dev/null +++ b/assets/components/number-input.ts @@ -0,0 +1,60 @@ +import { connect, machine, type Props, type Api } from "@zag-js/number-input"; +import { VanillaMachine, normalizeProps } from "@zag-js/vanilla"; +import { Component } from "../lib/core"; + +export class NumberInput extends Component { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initMachine(props: Props): VanillaMachine { + return new VanillaMachine(machine, props); + } + + initApi(): Api { + return connect(this.machine.service, normalizeProps); + } + + render(): void { + const rootEl = + this.el.querySelector('[data-scope="number-input"][data-part="root"]') ?? + this.el; + this.spreadProps(rootEl, this.api.getRootProps()); + + const labelEl = this.el.querySelector( + '[data-scope="number-input"][data-part="label"]' + ); + if (labelEl) this.spreadProps(labelEl, this.api.getLabelProps()); + + const controlEl = this.el.querySelector( + '[data-scope="number-input"][data-part="control"]' + ); + if (controlEl) this.spreadProps(controlEl, this.api.getControlProps()); + + const valueTextEl = this.el.querySelector( + '[data-scope="number-input"][data-part="value-text"]' + ); + if (valueTextEl) this.spreadProps(valueTextEl, this.api.getValueTextProps()); + + const inputEl = this.el.querySelector( + '[data-scope="number-input"][data-part="input"]' + ); + if (inputEl) this.spreadProps(inputEl, this.api.getInputProps()); + + const decrementEl = this.el.querySelector( + '[data-scope="number-input"][data-part="decrement-trigger"]' + ); + if (decrementEl) this.spreadProps(decrementEl, this.api.getDecrementTriggerProps()); + + const incrementEl = this.el.querySelector( + '[data-scope="number-input"][data-part="increment-trigger"]' + ); + if (incrementEl) this.spreadProps(incrementEl, this.api.getIncrementTriggerProps()); + + const scrubberEl = this.el.querySelector( + '[data-scope="number-input"][data-part="scrubber"]' + ); + if (scrubberEl) { + this.spreadProps(scrubberEl, this.api.getScrubberProps()); + scrubberEl.setAttribute("aria-label", "Scrub to adjust value"); + scrubberEl.removeAttribute("role"); + } + } +} diff --git a/assets/components/password-input.ts b/assets/components/password-input.ts new file mode 100644 index 0000000..179e0c4 --- /dev/null +++ b/assets/components/password-input.ts @@ -0,0 +1,46 @@ +import { connect, machine, type Props, type Api } from "@zag-js/password-input"; +import { VanillaMachine, normalizeProps } from "@zag-js/vanilla"; +import { Component } from "../lib/core"; + +export class PasswordInput extends Component { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initMachine(props: Props): VanillaMachine { + return new VanillaMachine(machine, props); + } + + initApi(): Api { + return connect(this.machine.service, normalizeProps); + } + + render(): void { + const rootEl = + this.el.querySelector('[data-scope="password-input"][data-part="root"]') ?? + this.el; + this.spreadProps(rootEl, this.api.getRootProps()); + + const labelEl = this.el.querySelector( + '[data-scope="password-input"][data-part="label"]' + ); + if (labelEl) this.spreadProps(labelEl, this.api.getLabelProps()); + + const controlEl = this.el.querySelector( + '[data-scope="password-input"][data-part="control"]' + ); + if (controlEl) this.spreadProps(controlEl, this.api.getControlProps()); + + const inputEl = this.el.querySelector( + '[data-scope="password-input"][data-part="input"]' + ); + if (inputEl) this.spreadProps(inputEl, this.api.getInputProps()); + + const triggerEl = this.el.querySelector( + '[data-scope="password-input"][data-part="visibility-trigger"]' + ); + if (triggerEl) this.spreadProps(triggerEl, this.api.getVisibilityTriggerProps()); + + const indicatorEl = this.el.querySelector( + '[data-scope="password-input"][data-part="indicator"]' + ); + if (indicatorEl) this.spreadProps(indicatorEl, this.api.getIndicatorProps()); + } +} diff --git a/assets/components/pin-input.ts b/assets/components/pin-input.ts new file mode 100644 index 0000000..f1d93dc --- /dev/null +++ b/assets/components/pin-input.ts @@ -0,0 +1,42 @@ +import { connect, machine, type Props, type Api } from "@zag-js/pin-input"; +import { VanillaMachine, normalizeProps } from "@zag-js/vanilla"; +import { Component } from "../lib/core"; + +export class PinInput extends Component { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initMachine(props: Props): VanillaMachine { + return new VanillaMachine(machine, props); + } + + initApi(): Api { + return connect(this.machine.service, normalizeProps); + } + + render(): void { + const rootEl = + this.el.querySelector('[data-scope="pin-input"][data-part="root"]') ?? this.el; + this.spreadProps(rootEl, this.api.getRootProps()); + + const labelEl = this.el.querySelector( + '[data-scope="pin-input"][data-part="label"]' + ); + if (labelEl) this.spreadProps(labelEl, this.api.getLabelProps()); + + const hiddenInputEl = this.el.querySelector( + '[data-scope="pin-input"][data-part="hidden-input"]' + ); + if (hiddenInputEl) this.spreadProps(hiddenInputEl, this.api.getHiddenInputProps()); + + const controlEl = this.el.querySelector( + '[data-scope="pin-input"][data-part="control"]' + ); + if (controlEl) this.spreadProps(controlEl, this.api.getControlProps()); + + this.api.items.forEach((i) => { + const inputEl = this.el.querySelector( + `[data-scope="pin-input"][data-part="input"][data-index="${i}"]` + ); + if (inputEl) this.spreadProps(inputEl, this.api.getInputProps({ index: i })); + }); + } +} diff --git a/assets/components/radio-group.ts b/assets/components/radio-group.ts new file mode 100644 index 0000000..8a785e2 --- /dev/null +++ b/assets/components/radio-group.ts @@ -0,0 +1,73 @@ +import { connect, machine, type Props, type Api } from "@zag-js/radio-group"; +import type { ItemProps } from "@zag-js/radio-group"; +import { VanillaMachine, normalizeProps } from "@zag-js/vanilla"; +import { Component } from "../lib/core"; + +export class RadioGroup extends Component { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initMachine(props: Props): VanillaMachine { + return new VanillaMachine(machine, props); + } + + initApi(): Api { + return connect(this.machine.service, normalizeProps); + } + + render(): void { + const rootEl = + this.el.querySelector('[data-scope="radio-group"][data-part="root"]') ?? this.el; + this.spreadProps(rootEl, this.api.getRootProps()); + + const labelEl = this.el.querySelector( + '[data-scope="radio-group"][data-part="label"]' + ); + if (labelEl) this.spreadProps(labelEl, this.api.getLabelProps()); + + const indicatorEl = this.el.querySelector( + '[data-scope="radio-group"][data-part="indicator"]' + ); + if (indicatorEl) this.spreadProps(indicatorEl, this.api.getIndicatorProps()); + + this.el + .querySelectorAll('[data-scope="radio-group"][data-part="item"]') + .forEach((itemEl) => { + const value = itemEl.dataset.value; + if (value == null) return; + const disabled = itemEl.dataset.disabled === "true"; + const invalid = itemEl.dataset.invalid === "true"; + this.spreadProps(itemEl, this.api.getItemProps({ value, disabled, invalid })); + const textEl = itemEl.querySelector( + '[data-scope="radio-group"][data-part="item-text"]' + ); + if (textEl) + this.spreadProps( + textEl, + this.api.getItemTextProps({ value, disabled, invalid } as ItemProps) + ); + const controlEl = itemEl.querySelector( + '[data-scope="radio-group"][data-part="item-control"]' + ); + if (controlEl) + this.spreadProps( + controlEl, + this.api.getItemControlProps({ + value, + disabled, + invalid, + } as ItemProps) + ); + const hiddenInputEl = itemEl.querySelector( + '[data-scope="radio-group"][data-part="item-hidden-input"]' + ); + if (hiddenInputEl) + this.spreadProps( + hiddenInputEl, + this.api.getItemHiddenInputProps({ + value, + disabled, + invalid, + } as ItemProps) + ); + }); + } +} diff --git a/assets/components/timer.ts b/assets/components/timer.ts new file mode 100644 index 0000000..8918781 --- /dev/null +++ b/assets/components/timer.ts @@ -0,0 +1,67 @@ +import { connect, machine, type Props, type Api } from "@zag-js/timer"; +import type { ItemProps, ActionTriggerProps } from "@zag-js/timer"; +import { VanillaMachine, normalizeProps } from "@zag-js/vanilla"; +import { Component } from "../lib/core"; + +export class Timer extends Component { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initMachine(props: Props): VanillaMachine { + return new VanillaMachine(machine, props); + } + + initApi(): Api { + return connect(this.machine.service, normalizeProps); + } + + init = (): void => { + this.machine.subscribe(() => { + (this as { api: Api }).api = this.initApi(); + this.render(); + }); + this.machine.start(); + (this as { api: Api }).api = this.initApi(); + this.render(); + }; + + render(): void { + const rootEl = + this.el.querySelector('[data-scope="timer"][data-part="root"]') ?? this.el; + this.spreadProps(rootEl, this.api.getRootProps()); + + const areaEl = this.el.querySelector('[data-scope="timer"][data-part="area"]'); + if (areaEl) this.spreadProps(areaEl, this.api.getAreaProps()); + + const controlEl = this.el.querySelector( + '[data-scope="timer"][data-part="control"]' + ); + if (controlEl) this.spreadProps(controlEl, this.api.getControlProps()); + + const timeParts = ["days", "hours", "minutes", "seconds"] as const; + timeParts.forEach((type) => { + const itemEl = this.el.querySelector( + `[data-scope="timer"][data-part="item"][data-type="${type}"]` + ); + if (itemEl) { + this.spreadProps(itemEl, this.api.getItemProps({ type } as ItemProps)); + } + }); + + this.el + .querySelectorAll('[data-scope="timer"][data-part="separator"]') + .forEach((separatorEl) => { + this.spreadProps(separatorEl, this.api.getSeparatorProps()); + }); + + const actions = ["start", "pause", "resume", "reset"] as const; + actions.forEach((action) => { + const triggerEl = this.el.querySelector( + `[data-scope="timer"][data-part="action-trigger"][data-action="${action}"]` + ); + if (triggerEl) + this.spreadProps( + triggerEl, + this.api.getActionTriggerProps({ action } as ActionTriggerProps) + ); + }); + } +} diff --git a/assets/hooks/angle-slider.ts b/assets/hooks/angle-slider.ts new file mode 100644 index 0000000..7227260 --- /dev/null +++ b/assets/hooks/angle-slider.ts @@ -0,0 +1,156 @@ +import type { Hook } from "phoenix_live_view"; +import type { HookInterface, CallbackRef } from "phoenix_live_view/assets/js/types/view_hook"; +import { AngleSlider } from "../components/angle-slider"; +import type { Props, ValueChangeDetails } from "@zag-js/angle-slider"; +import { getString, getBoolean, getNumber } from "../lib/util"; + +type AngleSliderHookState = { + angleSlider?: AngleSlider; + handlers?: Array; + onSetValue?: (event: Event) => void; +}; + +const AngleSliderHook: Hook = { + mounted(this: object & HookInterface & AngleSliderHookState) { + const el = this.el; + const value = getNumber(el, "value"); + const defaultValue = getNumber(el, "defaultValue"); + const controlled = getBoolean(el, "controlled"); + let skipNextOnValueChange = false; + + const zag = new AngleSlider(el, { + id: el.id, + ...(controlled && value !== undefined ? { value } : { defaultValue: defaultValue ?? 0 }), + step: getNumber(el, "step") ?? 1, + disabled: getBoolean(el, "disabled"), + readOnly: getBoolean(el, "readOnly"), + invalid: getBoolean(el, "invalid"), + name: getString(el, "name"), + dir: getString<"ltr" | "rtl">(el, "dir", ["ltr", "rtl"]), + onValueChange: (details: ValueChangeDetails) => { + if (skipNextOnValueChange) { + skipNextOnValueChange = false; + return; + } + if (controlled) { + skipNextOnValueChange = true; + zag.api.setValue(details.value); + } else { + const hiddenInput = el.querySelector( + '[data-scope="angle-slider"][data-part="hidden-input"]' + ); + if (hiddenInput) { + hiddenInput.value = String(details.value); + hiddenInput.dispatchEvent(new Event("input", { bubbles: true })); + hiddenInput.dispatchEvent(new Event("change", { bubbles: true })); + } + } + const eventName = getString(el, "onValueChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { + value: details.value, + valueAsDegree: details.valueAsDegree, + id: el.id, + }); + } + const eventNameClient = getString(el, "onValueChangeClient"); + if (eventNameClient) { + el.dispatchEvent( + new CustomEvent(eventNameClient, { + bubbles: true, + detail: { value: details, id: el.id }, + }) + ); + } + }, + onValueChangeEnd: (details: ValueChangeDetails) => { + if (controlled) { + const hiddenInput = el.querySelector( + '[data-scope="angle-slider"][data-part="hidden-input"]' + ); + if (hiddenInput) { + hiddenInput.value = String(details.value); + hiddenInput.dispatchEvent(new Event("input", { bubbles: true })); + hiddenInput.dispatchEvent(new Event("change", { bubbles: true })); + } + } + const eventName = getString(el, "onValueChangeEnd"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { + value: details.value, + valueAsDegree: details.valueAsDegree, + id: el.id, + }); + } + const eventNameClient = getString(el, "onValueChangeEndClient"); + if (eventNameClient) { + el.dispatchEvent( + new CustomEvent(eventNameClient, { + bubbles: true, + detail: { value: details, id: el.id }, + }) + ); + } + }, + } as Props); + zag.init(); + this.angleSlider = zag; + this.handlers = []; + + this.onSetValue = (event: Event) => { + const { value } = (event as CustomEvent<{ value: number }>).detail; + zag.api.setValue(value); + }; + el.addEventListener("phx:angle-slider:set-value", this.onSetValue); + + this.handlers.push( + this.handleEvent( + "angle_slider_set_value", + (payload: { angle_slider_id?: string; value: number }) => { + const targetId = payload.angle_slider_id; + if (targetId) { + const matches = el.id === targetId || el.id === `angle-slider:${targetId}`; + if (!matches) return; + } + zag.api.setValue(payload.value); + } + ) + ); + + this.handlers.push( + this.handleEvent("angle_slider_value", () => { + this.pushEvent("angle_slider_value_response", { + value: zag.api.value, + valueAsDegree: zag.api.valueAsDegree, + dragging: zag.api.dragging, + }); + }) + ); + }, + + updated(this: object & HookInterface & AngleSliderHookState) { + const value = getNumber(this.el, "value"); + const controlled = getBoolean(this.el, "controlled"); + this.angleSlider?.updateProps({ + id: this.el.id, + ...(controlled && value !== undefined ? { value } : {}), + step: getNumber(this.el, "step") ?? 1, + disabled: getBoolean(this.el, "disabled"), + readOnly: getBoolean(this.el, "readOnly"), + invalid: getBoolean(this.el, "invalid"), + name: getString(this.el, "name"), + } as Partial); + }, + + destroyed(this: object & HookInterface & AngleSliderHookState) { + if (this.onSetValue) { + this.el.removeEventListener("phx:angle-slider:set-value", this.onSetValue); + } + if (this.handlers) { + for (const h of this.handlers) this.removeHandleEvent(h); + } + this.angleSlider?.destroy(); + }, +}; + +export { AngleSliderHook as AngleSlider }; diff --git a/assets/hooks/avatar.ts b/assets/hooks/avatar.ts new file mode 100644 index 0000000..8750463 --- /dev/null +++ b/assets/hooks/avatar.ts @@ -0,0 +1,58 @@ +import type { Hook } from "phoenix_live_view"; +import type { HookInterface, CallbackRef } from "phoenix_live_view/assets/js/types/view_hook"; +import { Avatar } from "../components/avatar"; +import type { Props, StatusChangeDetails } from "@zag-js/avatar"; +import { getString } from "../lib/util"; + +type AvatarHookState = { + avatar?: Avatar; + handlers?: Array; +}; + +const AvatarHook: Hook = { + mounted(this: object & HookInterface & AvatarHookState) { + const el = this.el; + const src = getString(el, "src"); + const zag = new Avatar(el, { + id: el.id, + onStatusChange: (details: StatusChangeDetails) => { + const eventName = getString(el, "onStatusChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { status: details.status, id: el.id }); + } + const clientName = getString(el, "onStatusChangeClient"); + if (clientName) { + el.dispatchEvent( + new CustomEvent(clientName, { + bubbles: true, + detail: { value: details, id: el.id }, + }) + ); + } + }, + ...(src !== undefined ? {} : {}), + } as Props); + zag.init(); + this.avatar = zag; + if (src !== undefined) { + zag.api.setSrc(src); + } + this.handlers = []; + }, + + updated(this: object & HookInterface & AvatarHookState) { + const src = getString(this.el, "src"); + if (src !== undefined && this.avatar) { + this.avatar.api.setSrc(src); + } + }, + + destroyed(this: object & HookInterface & AvatarHookState) { + if (this.handlers) { + for (const h of this.handlers) this.removeHandleEvent(h); + } + this.avatar?.destroy(); + }, +}; + +export { AvatarHook as Avatar }; diff --git a/assets/hooks/carousel.ts b/assets/hooks/carousel.ts new file mode 100644 index 0000000..337b889 --- /dev/null +++ b/assets/hooks/carousel.ts @@ -0,0 +1,97 @@ +import type { Hook } from "phoenix_live_view"; +import type { HookInterface, CallbackRef } from "phoenix_live_view/assets/js/types/view_hook"; +import { Carousel } from "../components/carousel"; +import type { Props, PageChangeDetails } from "@zag-js/carousel"; +import { getString, getBoolean, getNumber, getDir } from "../lib/util"; + +type CarouselHookState = { + carousel?: Carousel; + handlers?: Array; +}; + +const CarouselHook: Hook = { + mounted(this: object & HookInterface & CarouselHookState) { + const el = this.el; + const page = getNumber(el, "page"); + const defaultPage = getNumber(el, "defaultPage"); + const controlled = getBoolean(el, "controlled"); + const slideCount = getNumber(el, "slideCount"); + if (slideCount == null || slideCount < 1) { + return; + } + const zag = new Carousel(el, { + id: el.id, + slideCount, + ...(controlled && page !== undefined ? { page } : { defaultPage: defaultPage ?? 0 }), + dir: getDir(el), + orientation: getString<"horizontal" | "vertical">(el, "orientation", [ + "horizontal", + "vertical", + ]), + slidesPerPage: getNumber(el, "slidesPerPage") ?? 1, + slidesPerMove: + getString(el, "slidesPerMove") === "auto" ? "auto" : getNumber(el, "slidesPerMove"), + loop: getBoolean(el, "loop"), + autoplay: getBoolean(el, "autoplay") + ? { delay: getNumber(el, "autoplayDelay") ?? 4000 } + : false, + allowMouseDrag: getBoolean(el, "allowMouseDrag"), + spacing: getString(el, "spacing") ?? "0px", + padding: getString(el, "padding"), + inViewThreshold: getNumber(el, "inViewThreshold") ?? 0.6, + snapType: getString<"proximity" | "mandatory">(el, "snapType", ["proximity", "mandatory"]), + autoSize: getBoolean(el, "autoSize"), + onPageChange: (details: PageChangeDetails) => { + const eventName = getString(el, "onPageChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { + page: details.page, + pageSnapPoint: details.pageSnapPoint, + id: el.id, + }); + } + const clientName = getString(el, "onPageChangeClient"); + if (clientName) { + el.dispatchEvent( + new CustomEvent(clientName, { + bubbles: true, + detail: { value: details, id: el.id }, + }) + ); + } + }, + } as Props); + zag.init(); + this.carousel = zag; + this.handlers = []; + }, + + updated(this: object & HookInterface & CarouselHookState) { + const slideCount = getNumber(this.el, "slideCount"); + if (slideCount == null || slideCount < 1) return; + const page = getNumber(this.el, "page"); + const controlled = getBoolean(this.el, "controlled"); + this.carousel?.updateProps({ + id: this.el.id, + slideCount, + ...(controlled && page !== undefined ? { page } : {}), + dir: getDir(this.el), + orientation: getString<"horizontal" | "vertical">(this.el, "orientation", [ + "horizontal", + "vertical", + ]), + slidesPerPage: getNumber(this.el, "slidesPerPage") ?? 1, + loop: getBoolean(this.el, "loop"), + allowMouseDrag: getBoolean(this.el, "allowMouseDrag"), + } as Partial); + }, + + destroyed(this: object & HookInterface & CarouselHookState) { + if (this.handlers) { + for (const h of this.handlers) this.removeHandleEvent(h); + } + this.carousel?.destroy(); + }, +}; + +export { CarouselHook as Carousel }; diff --git a/assets/hooks/components.d.ts b/assets/hooks/components.d.ts index 7235e71..ac6222f 100644 --- a/assets/hooks/components.d.ts +++ b/assets/hooks/components.d.ts @@ -7,6 +7,15 @@ type CorexHook = import("phoenix_live_view").Hook; declare module "corex/accordion" { export const Accordion: CorexHook; } +declare module "corex/angle-slider" { + export const AngleSlider: CorexHook; +} +declare module "corex/avatar" { + export const Avatar: CorexHook; +} +declare module "corex/carousel" { + export const Carousel: CorexHook; +} declare module "corex/checkbox" { export const Checkbox: CorexHook; } @@ -25,9 +34,30 @@ declare module "corex/date-picker" { declare module "corex/dialog" { export const Dialog: CorexHook; } +declare module "corex/editable" { + export const Editable: CorexHook; +} +declare module "corex/floating-panel" { + export const FloatingPanel: CorexHook; +} +declare module "corex/listbox" { + export const Listbox: CorexHook; +} declare module "corex/menu" { export const Menu: CorexHook; } +declare module "corex/number-input" { + export const NumberInput: CorexHook; +} +declare module "corex/password-input" { + export const PasswordInput: CorexHook; +} +declare module "corex/pin-input" { + export const PinInput: CorexHook; +} +declare module "corex/radio-group" { + export const RadioGroup: CorexHook; +} declare module "corex/select" { export const Select: CorexHook; } @@ -40,6 +70,9 @@ declare module "corex/switch" { declare module "corex/tabs" { export const Tabs: CorexHook; } +declare module "corex/timer" { + export const Timer: CorexHook; +} declare module "corex/toast" { export const Toast: CorexHook; } diff --git a/assets/hooks/corex.ts b/assets/hooks/corex.ts index 4a37a3f..33d914e 100644 --- a/assets/hooks/corex.ts +++ b/assets/hooks/corex.ts @@ -2,12 +2,12 @@ import type { Hook } from "phoenix_live_view"; type HookModule = Record | undefined>; -function hooks(importFn: () => Promise, exportName: string): Hook { +function createLazyHook(importFn: () => Promise, exportName: string): Hook { return { async mounted() { const mod = await importFn(); const real = mod[exportName]; - (this as { _realHook?: Hook })._realHook = real; + (this as { _realHook?: Hook })._realHook = real; if (real?.mounted) return real.mounted.call(this); }, updated() { @@ -29,23 +29,40 @@ function hooks(importFn: () => Promise, exportName: string): Hook { } export const Hooks = { - Accordion: hooks(() => import("corex/accordion"), "Accordion"), - Checkbox: hooks(() => import("corex/checkbox"), "Checkbox"), - Clipboard: hooks(() => import("corex/clipboard"), "Clipboard"), - Collapsible: hooks(() => import("corex/collapsible"), "Collapsible"), - Combobox: hooks(() => import("corex/combobox"), "Combobox"), - DatePicker: hooks(() => import("corex/date-picker"), "DatePicker"), - Dialog: hooks(() => import("corex/dialog"), "Dialog"), - Menu: hooks(() => import("corex/menu"), "Menu"), - Select: hooks(() => import("corex/select"), "Select"), - SignaturePad: hooks(() => import("corex/signature-pad"), "SignaturePad"), - Switch: hooks(() => import("corex/switch"), "Switch"), - Tabs: hooks(() => import("corex/tabs"), "Tabs"), - Toast: hooks(() => import("corex/toast"), "Toast"), - ToggleGroup: hooks(() => import("corex/toggle-group"), "ToggleGroup"), - TreeView: hooks(() => import("corex/tree-view"), "TreeView"), + Accordion: createLazyHook(() => import("corex/accordion"), "Accordion"), + AngleSlider: createLazyHook(() => import("corex/angle-slider"), "AngleSlider"), + Avatar: createLazyHook(() => import("corex/avatar"), "Avatar"), + Carousel: createLazyHook(() => import("corex/carousel"), "Carousel"), + Checkbox: createLazyHook(() => import("corex/checkbox"), "Checkbox"), + Clipboard: createLazyHook(() => import("corex/clipboard"), "Clipboard"), + Collapsible: createLazyHook(() => import("corex/collapsible"), "Collapsible"), + Combobox: createLazyHook(() => import("corex/combobox"), "Combobox"), + DatePicker: createLazyHook(() => import("corex/date-picker"), "DatePicker"), + Dialog: createLazyHook(() => import("corex/dialog"), "Dialog"), + Editable: createLazyHook(() => import("corex/editable"), "Editable"), + FloatingPanel: createLazyHook(() => import("corex/floating-panel"), "FloatingPanel"), + Listbox: createLazyHook(() => import("corex/listbox"), "Listbox"), + Menu: createLazyHook(() => import("corex/menu"), "Menu"), + NumberInput: createLazyHook(() => import("corex/number-input"), "NumberInput"), + PasswordInput: createLazyHook(() => import("corex/password-input"), "PasswordInput"), + PinInput: createLazyHook(() => import("corex/pin-input"), "PinInput"), + RadioGroup: createLazyHook(() => import("corex/radio-group"), "RadioGroup"), + Select: createLazyHook(() => import("corex/select"), "Select"), + SignaturePad: createLazyHook(() => import("corex/signature-pad"), "SignaturePad"), + Switch: createLazyHook(() => import("corex/switch"), "Switch"), + Tabs: createLazyHook(() => import("corex/tabs"), "Tabs"), + Timer: createLazyHook(() => import("corex/timer"), "Timer"), + Toast: createLazyHook(() => import("corex/toast"), "Toast"), + ToggleGroup: createLazyHook(() => import("corex/toggle-group"), "ToggleGroup"), + TreeView: createLazyHook(() => import("corex/tree-view"), "TreeView"), }; -export { hooks }; +export function hooks( + componentNames: readonly T[] +): Pick { + return Object.fromEntries( + componentNames.filter((name): name is T => name in Hooks).map((name) => [name, Hooks[name]]) + ) as Pick; +} export default Hooks; diff --git a/assets/hooks/editable.ts b/assets/hooks/editable.ts new file mode 100644 index 0000000..813c12a --- /dev/null +++ b/assets/hooks/editable.ts @@ -0,0 +1,91 @@ +import type { Hook } from "phoenix_live_view"; +import type { HookInterface, CallbackRef } from "phoenix_live_view/assets/js/types/view_hook"; +import { Editable } from "../components/editable"; +import type { Props, ValueChangeDetails } from "@zag-js/editable"; +import { getString, getBoolean, getDir } from "../lib/util"; + +type EditableHookState = { + editable?: Editable; + handlers?: Array; +}; + +const EditableHook: Hook = { + mounted(this: object & HookInterface & EditableHookState) { + const el = this.el; + const value = getString(el, "value"); + const defaultValue = getString(el, "defaultValue"); + const controlled = getBoolean(el, "controlled"); + const placeholder = getString(el, "placeholder"); + const activationMode = getString(el, "activationMode") as "focus" | "dblclick" | undefined; + const selectOnFocus = getBoolean(el, "selectOnFocus"); + + const zag = new Editable(el, { + id: el.id, + ...(controlled && value !== undefined ? { value } : { defaultValue: defaultValue ?? "" }), + disabled: getBoolean(el, "disabled"), + readOnly: getBoolean(el, "readOnly"), + required: getBoolean(el, "required"), + invalid: getBoolean(el, "invalid"), + name: getString(el, "name"), + form: getString(el, "form"), + dir: getDir(el), + ...(placeholder !== undefined ? { placeholder } : {}), + ...(activationMode !== undefined ? { activationMode } : {}), + ...(selectOnFocus !== undefined ? { selectOnFocus } : {}), + ...(getBoolean(el, "controlledEdit") + ? { edit: getBoolean(el, "edit") } + : { defaultEdit: getBoolean(el, "defaultEdit") }), + onValueChange: (details: ValueChangeDetails) => { + const inputEl = el.querySelector('[data-scope="editable"][data-part="input"]') as { + value: string; + dispatchEvent: (e: Event) => boolean; + } | null; + if (inputEl) { + inputEl.value = details.value; + inputEl.dispatchEvent(new Event("input", { bubbles: true })); + inputEl.dispatchEvent(new Event("change", { bubbles: true })); + } + const eventName = getString(el, "onValueChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { value: details.value, id: el.id }); + } + const clientName = getString(el, "onValueChangeClient"); + if (clientName) { + el.dispatchEvent( + new CustomEvent(clientName, { + bubbles: true, + detail: { value: details, id: el.id }, + }) + ); + } + }, + } as Props); + zag.init(); + this.editable = zag; + this.handlers = []; + }, + + updated(this: object & HookInterface & EditableHookState) { + const value = getString(this.el, "value"); + const controlled = getBoolean(this.el, "controlled"); + this.editable?.updateProps({ + id: this.el.id, + ...(controlled && value !== undefined ? { value } : {}), + disabled: getBoolean(this.el, "disabled"), + readOnly: getBoolean(this.el, "readOnly"), + required: getBoolean(this.el, "required"), + invalid: getBoolean(this.el, "invalid"), + name: getString(this.el, "name"), + form: getString(this.el, "form"), + } as Partial); + }, + + destroyed(this: object & HookInterface & EditableHookState) { + if (this.handlers) { + for (const h of this.handlers) this.removeHandleEvent(h); + } + this.editable?.destroy(); + }, +}; + +export { EditableHook as Editable }; diff --git a/assets/hooks/floating-panel.ts b/assets/hooks/floating-panel.ts new file mode 100644 index 0000000..e374c3d --- /dev/null +++ b/assets/hooks/floating-panel.ts @@ -0,0 +1,138 @@ +import type { Hook } from "phoenix_live_view"; +import type { HookInterface, CallbackRef } from "phoenix_live_view/assets/js/types/view_hook"; +import { FloatingPanel } from "../components/floating-panel"; +import type { + Props, + OpenChangeDetails, + PositionChangeDetails, + SizeChangeDetails, + StageChangeDetails, +} from "@zag-js/floating-panel"; +import { getString, getBoolean, getDir } from "../lib/util"; + +type FloatingPanelHookState = { + floatingPanel?: FloatingPanel; + handlers?: Array; +}; + +function parseSize(val: string | undefined): { width: number; height: number } | undefined { + if (!val) return undefined; + try { + const parsed = JSON.parse(val) as { width?: number; height?: number }; + if (typeof parsed.width === "number" && typeof parsed.height === "number") { + return { width: parsed.width, height: parsed.height }; + } + } catch { + // + } + return undefined; +} + +function parsePoint(val: string | undefined): { x: number; y: number } | undefined { + if (!val) return undefined; + try { + const parsed = JSON.parse(val) as { x?: number; y?: number }; + if (typeof parsed.x === "number" && typeof parsed.y === "number") { + return { x: parsed.x, y: parsed.y }; + } + } catch { + // + } + return undefined; +} + +const FloatingPanelHook: Hook = { + mounted(this: object & HookInterface & FloatingPanelHookState) { + const el = this.el; + const open = getBoolean(el, "open"); + const defaultOpen = getBoolean(el, "defaultOpen"); + const controlled = getBoolean(el, "controlled"); + const size = parseSize(el.dataset.size); + const defaultSize = parseSize(el.dataset.defaultSize); + const position = parsePoint(el.dataset.position); + const defaultPosition = parsePoint(el.dataset.defaultPosition); + const zag = new FloatingPanel(el, { + id: el.id, + ...(controlled ? { open } : { defaultOpen }), + draggable: getBoolean(el, "draggable") !== false, + resizable: getBoolean(el, "resizable") !== false, + allowOverflow: getBoolean(el, "allowOverflow") !== false, + closeOnEscape: getBoolean(el, "closeOnEscape") !== false, + disabled: getBoolean(el, "disabled"), + dir: getDir(el), + size, + defaultSize, + position, + defaultPosition, + minSize: parseSize(el.dataset.minSize), + maxSize: parseSize(el.dataset.maxSize), + persistRect: getBoolean(el, "persistRect"), + gridSize: Number(el.dataset.gridSize) || 1, + onOpenChange: (details: OpenChangeDetails) => { + const eventName = getString(el, "onOpenChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { open: details.open, id: el.id }); + } + const clientName = getString(el, "onOpenChangeClient"); + if (clientName) { + el.dispatchEvent( + new CustomEvent(clientName, { + bubbles: true, + detail: { value: details, id: el.id }, + }) + ); + } + }, + onPositionChange: (details: PositionChangeDetails) => { + const eventName = getString(el, "onPositionChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { + position: details.position, + id: el.id, + }); + } + }, + onSizeChange: (details: SizeChangeDetails) => { + const eventName = getString(el, "onSizeChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { + size: details.size, + id: el.id, + }); + } + }, + onStageChange: (details: StageChangeDetails) => { + const eventName = getString(el, "onStageChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { + stage: details.stage, + id: el.id, + }); + } + }, + } as Props); + zag.init(); + this.floatingPanel = zag; + this.handlers = []; + }, + + updated(this: object & HookInterface & FloatingPanelHookState) { + const open = getBoolean(this.el, "open"); + const controlled = getBoolean(this.el, "controlled"); + this.floatingPanel?.updateProps({ + id: this.el.id, + ...(controlled ? { open } : {}), + disabled: getBoolean(this.el, "disabled"), + dir: getDir(this.el), + } as Partial); + }, + + destroyed(this: object & HookInterface & FloatingPanelHookState) { + if (this.handlers) { + for (const h of this.handlers) this.removeHandleEvent(h); + } + this.floatingPanel?.destroy(); + }, +}; + +export { FloatingPanelHook as FloatingPanel }; diff --git a/assets/hooks/listbox.ts b/assets/hooks/listbox.ts new file mode 100644 index 0000000..211b0e4 --- /dev/null +++ b/assets/hooks/listbox.ts @@ -0,0 +1,128 @@ +import type { Hook } from "phoenix_live_view"; +import type { HookInterface, CallbackRef } from "phoenix_live_view/assets/js/types/view_hook"; +import { collection } from "@zag-js/listbox"; +import { Listbox } from "../components/listbox"; +import type { Props, ValueChangeDetails } from "@zag-js/listbox"; +import type { Direction } from "@zag-js/types"; +import { getString, getBoolean, getStringList } from "../lib/util"; + +type ListboxItem = { + id?: string; + value?: string; + label: string; + disabled?: boolean; + group?: string; +}; + +function buildCollection(items: ListboxItem[], hasGroups: boolean) { + if (hasGroups) { + return collection({ + items, + itemToValue: (item) => item.id ?? item.value ?? "", + itemToString: (item) => item.label, + isItemDisabled: (item) => !!item.disabled, + groupBy: (item) => item.group ?? "", + }); + } + return collection({ + items, + itemToValue: (item) => item.id ?? item.value ?? "", + itemToString: (item) => item.label, + isItemDisabled: (item) => !!item.disabled, + }); +} + +type ListboxHookState = { + listbox?: Listbox; + handlers?: Array; +}; + +const ListboxHook: Hook = { + mounted(this: object & HookInterface & ListboxHookState) { + const el = this.el; + const allItems = JSON.parse(el.dataset.collection ?? "[]") as ListboxItem[]; + const hasGroups = allItems.some((item) => item.group !== undefined); + const valueList = getStringList(el, "value"); + const defaultValueList = getStringList(el, "defaultValue"); + const controlled = getBoolean(el, "controlled"); + const zag = new Listbox(el, { + id: el.id, + collection: buildCollection(allItems, hasGroups), + ...(controlled && valueList + ? { value: valueList } + : { defaultValue: defaultValueList ?? [] }), + disabled: getBoolean(el, "disabled"), + dir: getString(el, "dir", ["ltr", "rtl"]), + orientation: getString<"horizontal" | "vertical">(el, "orientation", [ + "horizontal", + "vertical", + ]), + loopFocus: getBoolean(el, "loopFocus"), + selectionMode: getString<"single" | "multiple" | "extended">(el, "selectionMode", [ + "single", + "multiple", + "extended", + ]), + selectOnHighlight: getBoolean(el, "selectOnHighlight"), + deselectable: getBoolean(el, "deselectable"), + typeahead: getBoolean(el, "typeahead"), + onValueChange: (details: ValueChangeDetails) => { + const eventName = getString(el, "onValueChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { + value: details.value, + items: details.items, + id: el.id, + }); + } + const clientName = getString(el, "onValueChangeClient"); + if (clientName) { + el.dispatchEvent( + new CustomEvent(clientName, { + bubbles: true, + detail: { value: details, id: el.id }, + }) + ); + } + }, + } as Props); + zag.hasGroups = hasGroups; + zag.setOptions(allItems); + zag.init(); + + this.listbox = zag; + this.handlers = []; + }, + + updated(this: object & HookInterface & ListboxHookState) { + const newItems = JSON.parse(this.el.dataset.collection ?? "[]") as ListboxItem[]; + const hasGroups = newItems.some((item) => item.group !== undefined); + const valueList = getStringList(this.el, "value"); + const controlled = getBoolean(this.el, "controlled"); + + if (this.listbox) { + this.listbox.hasGroups = hasGroups; + this.listbox.setOptions(newItems); + this.listbox.updateProps({ + collection: buildCollection(newItems, hasGroups), + id: this.el.id, + ...(controlled && valueList ? { value: valueList } : {}), + disabled: getBoolean(this.el, "disabled"), + dir: getString(this.el, "dir", ["ltr", "rtl"]), + orientation: getString<"horizontal" | "vertical">(this.el, "orientation", [ + "horizontal", + "vertical", + ]), + } as Partial>); + } + }, + + destroyed(this: object & HookInterface & ListboxHookState) { + if (this.handlers) { + for (const h of this.handlers) this.removeHandleEvent(h); + } + this.listbox?.destroy(); + }, +}; + +export { ListboxHook as Listbox }; diff --git a/assets/hooks/number-input.ts b/assets/hooks/number-input.ts new file mode 100644 index 0000000..1d38dd1 --- /dev/null +++ b/assets/hooks/number-input.ts @@ -0,0 +1,92 @@ +import type { Hook } from "phoenix_live_view"; +import type { HookInterface, CallbackRef } from "phoenix_live_view/assets/js/types/view_hook"; +import { NumberInput } from "../components/number-input"; +import type { Props, ValueChangeDetails } from "@zag-js/number-input"; +import { getString, getBoolean, getNumber } from "../lib/util"; + +type NumberInputHookState = { + numberInput?: NumberInput; + handlers?: Array; +}; + +const NumberInputHook: Hook = { + mounted(this: object & HookInterface & NumberInputHookState) { + const el = this.el; + const valueStr = getString(el, "value"); + const defaultValueStr = getString(el, "defaultValue"); + const controlled = getBoolean(el, "controlled"); + const zag = new NumberInput(el, { + id: el.id, + ...(controlled && valueStr !== undefined + ? { value: valueStr } + : { defaultValue: defaultValueStr }), + min: getNumber(el, "min"), + max: getNumber(el, "max"), + step: getNumber(el, "step"), + disabled: getBoolean(el, "disabled"), + readOnly: getBoolean(el, "readOnly"), + invalid: getBoolean(el, "invalid"), + required: getBoolean(el, "required"), + allowMouseWheel: getBoolean(el, "allowMouseWheel"), + name: getString(el, "name"), + form: getString(el, "form"), + onValueChange: (details: ValueChangeDetails) => { + const inputEl = el.querySelector( + '[data-scope="number-input"][data-part="input"]' + ); + if (inputEl) { + inputEl.value = details.value; + inputEl.dispatchEvent(new Event("input", { bubbles: true })); + inputEl.dispatchEvent(new Event("change", { bubbles: true })); + } + const eventName = getString(el, "onValueChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { + value: details.value, + valueAsNumber: details.valueAsNumber, + id: el.id, + }); + } + const clientName = getString(el, "onValueChangeClient"); + if (clientName) { + el.dispatchEvent( + new CustomEvent(clientName, { + bubbles: true, + detail: { value: details, id: el.id }, + }) + ); + } + }, + } as Props); + zag.init(); + this.numberInput = zag; + this.handlers = []; + }, + + updated(this: object & HookInterface & NumberInputHookState) { + const valueStr = getString(this.el, "value"); + const controlled = getBoolean(this.el, "controlled"); + this.numberInput?.updateProps({ + id: this.el.id, + ...(controlled && valueStr !== undefined ? { value: valueStr } : {}), + min: getNumber(this.el, "min"), + max: getNumber(this.el, "max"), + step: getNumber(this.el, "step"), + disabled: getBoolean(this.el, "disabled"), + readOnly: getBoolean(this.el, "readOnly"), + invalid: getBoolean(this.el, "invalid"), + required: getBoolean(this.el, "required"), + name: getString(this.el, "name"), + form: getString(this.el, "form"), + } as Partial); + }, + + destroyed(this: object & HookInterface & NumberInputHookState) { + if (this.handlers) { + for (const h of this.handlers) this.removeHandleEvent(h); + } + this.numberInput?.destroy(); + }, +}; + +export { NumberInputHook as NumberInput }; diff --git a/assets/hooks/password-input.ts b/assets/hooks/password-input.ts new file mode 100644 index 0000000..e9c733e --- /dev/null +++ b/assets/hooks/password-input.ts @@ -0,0 +1,76 @@ +import type { Hook } from "phoenix_live_view"; +import type { HookInterface, CallbackRef } from "phoenix_live_view/assets/js/types/view_hook"; +import { PasswordInput } from "../components/password-input"; +import type { Props, VisibilityChangeDetails } from "@zag-js/password-input"; +import { getString, getBoolean, getDir } from "../lib/util"; + +type PasswordInputHookState = { + passwordInput?: PasswordInput; + handlers?: Array; +}; + +const PasswordInputHook: Hook = { + mounted(this: object & HookInterface & PasswordInputHookState) { + const el = this.el; + const zag = new PasswordInput(el, { + id: el.id, + ...(getBoolean(el, "controlledVisible") + ? { visible: getBoolean(el, "visible") } + : { defaultVisible: getBoolean(el, "defaultVisible") }), + disabled: getBoolean(el, "disabled"), + invalid: getBoolean(el, "invalid"), + readOnly: getBoolean(el, "readOnly"), + required: getBoolean(el, "required"), + ignorePasswordManagers: getBoolean(el, "ignorePasswordManagers"), + name: getString(el, "name"), + dir: getDir(el), + autoComplete: getString<"current-password" | "new-password">(el, "autoComplete", [ + "current-password", + "new-password", + ]), + onVisibilityChange: (details: VisibilityChangeDetails) => { + const eventName = getString(el, "onVisibilityChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { visible: details.visible, id: el.id }); + } + const clientName = getString(el, "onVisibilityChangeClient"); + if (clientName) { + el.dispatchEvent( + new CustomEvent(clientName, { + bubbles: true, + detail: { value: details, id: el.id }, + }) + ); + } + }, + } as Props); + zag.init(); + this.passwordInput = zag; + this.handlers = []; + }, + + updated(this: object & HookInterface & PasswordInputHookState) { + this.passwordInput?.updateProps({ + id: this.el.id, + ...(getBoolean(this.el, "controlledVisible") + ? { visible: getBoolean(this.el, "visible") } + : {}), + disabled: getBoolean(this.el, "disabled"), + invalid: getBoolean(this.el, "invalid"), + readOnly: getBoolean(this.el, "readOnly"), + required: getBoolean(this.el, "required"), + name: getString(this.el, "name"), + form: getString(this.el, "form"), + dir: getDir(this.el), + } as Partial); + }, + + destroyed(this: object & HookInterface & PasswordInputHookState) { + if (this.handlers) { + for (const h of this.handlers) this.removeHandleEvent(h); + } + this.passwordInput?.destroy(); + }, +}; + +export { PasswordInputHook as PasswordInput }; diff --git a/assets/hooks/pin-input.ts b/assets/hooks/pin-input.ts new file mode 100644 index 0000000..88e7a6b --- /dev/null +++ b/assets/hooks/pin-input.ts @@ -0,0 +1,109 @@ +import type { Hook } from "phoenix_live_view"; +import type { HookInterface, CallbackRef } from "phoenix_live_view/assets/js/types/view_hook"; +import { PinInput } from "../components/pin-input"; +import type { Props, ValueChangeDetails } from "@zag-js/pin-input"; +import type { Direction } from "@zag-js/types"; +import { getString, getBoolean, getStringList, getNumber } from "../lib/util"; + +type PinInputHookState = { + pinInput?: PinInput; + handlers?: Array; +}; + +const PinInputHook: Hook = { + mounted(this: object & HookInterface & PinInputHookState) { + const el = this.el; + const valueList = getStringList(el, "value"); + const defaultValueList = getStringList(el, "defaultValue"); + const controlled = getBoolean(el, "controlled"); + const zag = new PinInput(el, { + id: el.id, + count: getNumber(el, "count") ?? 4, + ...(controlled && valueList + ? { value: valueList } + : { defaultValue: defaultValueList ?? [] }), + disabled: getBoolean(el, "disabled"), + invalid: getBoolean(el, "invalid"), + required: getBoolean(el, "required"), + readOnly: getBoolean(el, "readOnly"), + mask: getBoolean(el, "mask"), + otp: getBoolean(el, "otp"), + blurOnComplete: getBoolean(el, "blurOnComplete"), + selectOnFocus: getBoolean(el, "selectOnFocus"), + name: getString(el, "name"), + form: getString(el, "form"), + dir: getString(el, "dir", ["ltr", "rtl"]), + type: getString<"alphanumeric" | "numeric" | "alphabetic">(el, "type", [ + "alphanumeric", + "numeric", + "alphabetic", + ]), + placeholder: getString(el, "placeholder"), + onValueChange: (details: ValueChangeDetails) => { + const hiddenInput = el.querySelector( + '[data-scope="pin-input"][data-part="hidden-input"]' + ); + if (hiddenInput) { + hiddenInput.value = details.valueAsString; + hiddenInput.dispatchEvent(new Event("input", { bubbles: true })); + hiddenInput.dispatchEvent(new Event("change", { bubbles: true })); + } + const eventName = getString(el, "onValueChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { + value: details.value, + valueAsString: details.valueAsString, + id: el.id, + }); + } + const clientName = getString(el, "onValueChangeClient"); + if (clientName) { + el.dispatchEvent( + new CustomEvent(clientName, { + bubbles: true, + detail: { value: details, id: el.id }, + }) + ); + } + }, + onValueComplete: (details: ValueChangeDetails) => { + const eventName = getString(el, "onValueComplete"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { + value: details.value, + valueAsString: details.valueAsString, + id: el.id, + }); + } + }, + } as Props); + zag.init(); + this.pinInput = zag; + this.handlers = []; + }, + + updated(this: object & HookInterface & PinInputHookState) { + const valueList = getStringList(this.el, "value"); + const controlled = getBoolean(this.el, "controlled"); + this.pinInput?.updateProps({ + id: this.el.id, + count: getNumber(this.el, "count") ?? this.pinInput?.api.count ?? 4, + ...(controlled && valueList ? { value: valueList } : {}), + disabled: getBoolean(this.el, "disabled"), + invalid: getBoolean(this.el, "invalid"), + required: getBoolean(this.el, "required"), + readOnly: getBoolean(this.el, "readOnly"), + name: getString(this.el, "name"), + form: getString(this.el, "form"), + } as Partial); + }, + + destroyed(this: object & HookInterface & PinInputHookState) { + if (this.handlers) { + for (const h of this.handlers) this.removeHandleEvent(h); + } + this.pinInput?.destroy(); + }, +}; + +export { PinInputHook as PinInput }; diff --git a/assets/hooks/radio-group.ts b/assets/hooks/radio-group.ts new file mode 100644 index 0000000..f3a2fb8 --- /dev/null +++ b/assets/hooks/radio-group.ts @@ -0,0 +1,86 @@ +import type { Hook } from "phoenix_live_view"; +import type { HookInterface, CallbackRef } from "phoenix_live_view/assets/js/types/view_hook"; +import { RadioGroup } from "../components/radio-group"; +import type { Props, ValueChangeDetails } from "@zag-js/radio-group"; +import type { Direction } from "@zag-js/types"; +import { getString, getBoolean } from "../lib/util"; + +type RadioGroupHookState = { + radioGroup?: RadioGroup; + handlers?: Array; +}; + +const RadioGroupHook: Hook = { + mounted(this: object & HookInterface & RadioGroupHookState) { + const el = this.el; + const value = getString(el, "value"); + const defaultValue = getString(el, "defaultValue"); + const controlled = getBoolean(el, "controlled"); + const zag = new RadioGroup(el, { + id: el.id, + ...(controlled && value !== undefined + ? { value: value ?? null } + : { defaultValue: defaultValue ?? null }), + name: getString(el, "name"), + form: getString(el, "form"), + disabled: getBoolean(el, "disabled"), + invalid: getBoolean(el, "invalid"), + required: getBoolean(el, "required"), + readOnly: getBoolean(el, "readOnly"), + dir: getString(el, "dir", ["ltr", "rtl"]), + orientation: getString<"horizontal" | "vertical">(el, "orientation", [ + "horizontal", + "vertical", + ]), + onValueChange: (details: ValueChangeDetails) => { + const eventName = getString(el, "onValueChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { + value: details.value, + id: el.id, + }); + } + const clientName = getString(el, "onValueChangeClient"); + if (clientName) { + el.dispatchEvent( + new CustomEvent(clientName, { + bubbles: true, + detail: { value: details, id: el.id }, + }) + ); + } + }, + } as Props); + zag.init(); + this.radioGroup = zag; + this.handlers = []; + }, + + updated(this: object & HookInterface & RadioGroupHookState) { + const value = getString(this.el, "value"); + const controlled = getBoolean(this.el, "controlled"); + this.radioGroup?.updateProps({ + id: this.el.id, + ...(controlled && value !== undefined ? { value: value ?? null } : {}), + name: getString(this.el, "name"), + form: getString(this.el, "form"), + disabled: getBoolean(this.el, "disabled"), + invalid: getBoolean(this.el, "invalid"), + required: getBoolean(this.el, "required"), + readOnly: getBoolean(this.el, "readOnly"), + orientation: getString<"horizontal" | "vertical">(this.el, "orientation", [ + "horizontal", + "vertical", + ]), + } as Partial); + }, + + destroyed(this: object & HookInterface & RadioGroupHookState) { + if (this.handlers) { + for (const h of this.handlers) this.removeHandleEvent(h); + } + this.radioGroup?.destroy(); + }, +}; + +export { RadioGroupHook as RadioGroup }; diff --git a/assets/hooks/timer.ts b/assets/hooks/timer.ts new file mode 100644 index 0000000..94af14e --- /dev/null +++ b/assets/hooks/timer.ts @@ -0,0 +1,64 @@ +import type { Hook } from "phoenix_live_view"; +import type { HookInterface, CallbackRef } from "phoenix_live_view/assets/js/types/view_hook"; +import { Timer } from "../components/timer"; +import type { Props, TickDetails } from "@zag-js/timer"; +import { getString, getBoolean, getNumber } from "../lib/util"; + +type TimerHookState = { + timer?: Timer; + handlers?: Array; +}; + +const TimerHook: Hook = { + mounted(this: object & HookInterface & TimerHookState) { + const el = this.el; + const zag = new Timer(el, { + id: el.id, + countdown: getBoolean(el, "countdown"), + startMs: getNumber(el, "startMs"), + targetMs: getNumber(el, "targetMs"), + autoStart: getBoolean(el, "autoStart"), + interval: getNumber(el, "interval") ?? 1000, + onTick: (details: TickDetails) => { + const eventName = getString(el, "onTick"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { + value: details.value, + time: details.time, + formattedTime: details.formattedTime, + id: el.id, + }); + } + }, + onComplete: () => { + const eventName = getString(el, "onComplete"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { id: el.id }); + } + }, + } as Props); + zag.init(); + this.timer = zag; + this.handlers = []; + }, + + updated(this: object & HookInterface & TimerHookState) { + this.timer?.updateProps({ + id: this.el.id, + countdown: getBoolean(this.el, "countdown"), + startMs: getNumber(this.el, "startMs"), + targetMs: getNumber(this.el, "targetMs"), + autoStart: getBoolean(this.el, "autoStart"), + interval: getNumber(this.el, "interval") ?? 1000, + } as Partial); + }, + + destroyed(this: object & HookInterface & TimerHookState) { + if (this.handlers) { + for (const h of this.handlers) this.removeHandleEvent(h); + } + this.timer?.destroy(); + }, +}; + +export { TimerHook as Timer }; diff --git a/config/config.exs b/config/config.exs index 8970681..1416e5d 100644 --- a/config/config.exs +++ b/config/config.exs @@ -12,7 +12,7 @@ config :phoenix, if Mix.env() == :dev do corex_externals = - ~w(accordion checkbox clipboard collapsible combobox date-picker dialog menu select signature-pad switch tabs toast toggle-group tree-view) + ~w(accordion angle-slider avatar carousel checkbox clipboard collapsible combobox date-picker dialog editable floating-panel listbox menu number-input password-input pin-input radio-group select signature-pad switch tabs timer toast toggle-group tree-view) |> Enum.map(fn name -> "--external:corex/#{name}" end) esbuild = fn args -> @@ -26,17 +26,28 @@ if Mix.env() == :dev do hooks_entries = ~w( ./hooks/accordion.ts + ./hooks/angle-slider.ts + ./hooks/avatar.ts + ./hooks/carousel.ts ./hooks/checkbox.ts ./hooks/clipboard.ts ./hooks/collapsible.ts ./hooks/combobox.ts ./hooks/date-picker.ts ./hooks/dialog.ts + ./hooks/editable.ts + ./hooks/floating-panel.ts + ./hooks/listbox.ts ./hooks/menu.ts + ./hooks/number-input.ts + ./hooks/password-input.ts + ./hooks/pin-input.ts + ./hooks/radio-group.ts ./hooks/select.ts ./hooks/signature-pad.ts ./hooks/switch.ts ./hooks/tabs.ts + ./hooks/timer.ts ./hooks/toast.ts ./hooks/toggle-group.ts ./hooks/tree-view.ts diff --git a/design/components/accordion.css b/design/components/accordion.css new file mode 100644 index 0000000..3ae7645 --- /dev/null +++ b/design/components/accordion.css @@ -0,0 +1,147 @@ +@import "../main.css"; + +@layer components { + + .accordion { + @apply ui-root; + width: 100%; + max-width: var(--container-ui); + + } + .accordion [data-scope="accordion"][data-part="root"] { + @apply ui-root; + + } + + .accordion [data-scope="accordion"][data-part="item"] h3 { + margin: 0; + padding: 0; + } + + .accordion [data-scope="accordion"][data-part="item-trigger"] { + @apply ui-item; + + border-radius: var(--radius-ui); + border: 1px solid var(--color-ui--border); + } + + .accordion [data-scope="accordion"][data-part="item"] { + gap: var(--spacing-ui-gap); + display: flex; + flex-direction: column; + width: 100%; + } + + .accordion [data-scope="accordion"][data-part="item-text"] { + + width: 100%; + } + + .accordion [data-scope="accordion"][data-part="item"][data-state="closed"] [data-scope="accordion"][data-part="item-indicator"][data-open], + .accordion [data-scope="accordion"][data-part="item"][data-state="open"] [data-scope="accordion"][data-part="item-indicator"][data-closed] { + display: none; + transform: none; + } + + .accordion [data-scope="accordion"][data-part="item"][data-state="open"] [data-scope="accordion"][data-part="item-indicator"][data-open], + .accordion [data-scope="accordion"][data-part="item"][data-state="closed"] [data-scope="accordion"][data-part="item-indicator"][data-closed] { + transform: none; + } + + .accordion [data-scope="accordion"][data-part="item-content"] { + @apply ui-content; + } + + .accordion [data-scope="accordion"][data-part="root"][data-loading] { + pointer-events: none; + user-select: none; + min-width: var(--container-ui); + + } + + .accordion [data-scope="accordion"][data-part="root"][data-loading] [data-scope="accordion"][data-part="item-text"]>*, + .accordion [data-scope="accordion"][data-part="root"][data-loading] [data-scope="accordion"][data-part="item-indicator"]>* { + animation: accordion-skeleton 1.4s ease-in-out infinite; + } + + @keyframes accordion-skeleton { + 0% { + opacity: 0.6; + } + + 50% { + opacity: 1; + } + + 100% { + opacity: 0.6; + } + } + + .accordion [data-scope="accordion"][data-part="root"][data-loading] [data-scope="accordion"][data-part="item-text"]::before, + .accordion [data-scope="accordion"][data-part="root"][data-loading] [data-scope="accordion"][data-part="item-text"]::after { + content: ""; + display: block; + height: var(--spacing-micro); + background: var(--color-ui-active); + border-radius: var(--radius-ui); + } + + .accordion [data-scope="accordion"][data-part="root"][data-loading] [data-scope="accordion"][data-part="item-text"]::before { + width: var(--spacing-ui); + margin-block: var(--spacing-ui-padding); + } +} + +@utility accordion--* { + [data-scope="accordion"][data-part="root"] { + max-width: --value(--container-ui-*, [length]); + gap: --value(--spacing-ui-gap-*, [length]); + } + + [data-scope="accordion"][data-part="item"] { + gap: --value(--spacing-ui-gap-*, [length]); + } + + [data-scope="accordion"][data-part="item"][data-disabled] [data-scope="accordion"][data-part="item-trigger"] { + color: --value(--color-ui-* --muted, [color]); + } + + [data-scope="accordion"][data-part="item-trigger"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + } + + [data-scope="accordion"][data-part="item-trigger"][data-disabled] { + background-color: --alpha(--value(--color-ui-*, [color]) / 60%); + color: --value(--color-ui-* --muted, [color]); + } + + [data-scope="accordion"][data-part="item-trigger"][data-selected], + [data-scope="accordion"][data-part="item-trigger"][data-in-range], + [data-scope="accordion"][data-part="item-trigger"][data-highlighted], + [data-scope="accordion"][data-part="item-trigger"][data-state="checked"], + [data-scope="accordion"][data-part="item-trigger"][data-state="on"], + [data-scope="accordion"][data-part="item-trigger"][data-state="open"], + [data-scope="accordion"][data-part="item-trigger"]:hover, + [data-scope="accordion"][data-part="item-trigger"]:active { + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="accordion"][data-part="item-trigger"][data-highlighted]:not( :hover) { + outline: 2px solid --value(--color-ui-* --text, [color]); + } + + [data-scope="accordion"][data-part="item-content"] { + padding: --value(--spacing-ui-padding-*, [length]); + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + } + + [data-scope="accordion"][data-part="item-content"]>p { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + } +} \ No newline at end of file diff --git a/design/components/badge.css b/design/components/badge.css new file mode 100644 index 0000000..b194c1f --- /dev/null +++ b/design/components/badge.css @@ -0,0 +1,71 @@ +@import "../main.css"; + +@layer components { + .badge { + display: inline-flex; + align-items: center; + justify-content: center; + text-align: center; + cursor: unset; + width: fit-content; + max-width: 100%; + font-size: calc(var(--text-ui) * 0.8); + line-height: var(--text-ui--line-height); + font-weight: var(--font-weight-ui); + border-radius: var(--radius-full); + border: 1px solid var(--color-ui--border); + padding-inline: var(--spacing-mini-padding); + gap: var(--spacing-mini-gap); + height: calc(var(--spacing-ui) * 0.8); + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; + color: var(--color-ui--text); + background-color: var(--color-ui); + + &:disabled, + &[data-disabled], + &[data-disabled="true"], + &[disabled="true"] { + color: var(--color-ui--muted); + background-color: --alpha(var(--color-ui) / 60%); + cursor: not-allowed; + } + + & svg { + @apply ui-icon; + + padding: 0; + } + } + + .badge.badge--square, + .badge.badge--circle { + display: inline-flex; + aspect-ratio: 1 / 1; + justify-content: center; + align-items: center; + width: auto; + padding: 0; + } + + .badge.badge--circle { + border-radius: var(--radius-full); + } +} + +@utility badge--* { + font-size: calc(--value(--text-ui-*, [length]) * 0.8); + line-height: calc(--value(--text-ui-* --line-height, [length]) * 0.8); + height: calc(--value(--spacing-ui-*, [length]) * 0.8); + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + padding-inline: calc(--value(--spacing-mini-padding-*, [length]) * 0.8); + gap: calc(--value(--spacing-mini-gap-*, [length]) * 0.8); + + :disabled, + [data-disabled] { + color: --value(--color-ui-* --muted, [color]); + background-color: --alpha(--value(--color-ui-*, [color]) / 60%); + } +} diff --git a/design/components/button.css b/design/components/button.css new file mode 100644 index 0000000..a061e22 --- /dev/null +++ b/design/components/button.css @@ -0,0 +1,52 @@ +@import "../main.css"; + +@layer components { + .button { + @apply ui-trigger; + + justify-content: center; + appearance: none; + } + + .button.button--square, + .button.button--circle { + display: inline-flex; + aspect-ratio: 1 / 1; + justify-content: center; + align-items: center; + width: auto; + padding: 0; + } + + .button.button--circle { + border-radius: var(--radius-full); + } +} + +@utility button--* { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + padding-inline: --value(--spacing-ui-padding-*, [length]); + gap: --value(--spacing-ui-gap-*, [length]); + + :hover { + background-color: --value(--color-ui-* -hover, [color]); + } + + :active { + background-color: --value(--color-ui-* -active, [color]); + } + + :focus-visible { + outline-color: --value(--color-ui-* --text, [color]); + } + + :disabled, + [data-disabled] { + color: --value(--color-ui-* --muted, [color]); + background-color: --alpha(--value(--color-ui-*, [color]) / 80%); + } +} diff --git a/design/components/checkbox.css b/design/components/checkbox.css new file mode 100644 index 0000000..49990d2 --- /dev/null +++ b/design/components/checkbox.css @@ -0,0 +1,166 @@ +@import "../main.css"; + +@layer components { + .checkbox [data-scope="checkbox"][data-part="root"] { + @apply ui-root; + + flex-direction: row; + max-width: var(--container-ui); + align-items: center; + width: auto; + gap: var(--spacing-ui-gap); + position: relative; + } + + .checkbox [data-scope="checkbox"][data-part="label"] { + @apply ui-label; + cursor: pointer; + + &[data-invalid] { + color: var(--color-ui-alert); + } + } + + .checkbox [data-scope="checkbox"][data-part="control"] { + height: var(--spacing-mini); + width: var(--spacing-mini); + background: var(--color-ui); + color: var(--color-ui--text); + font-weight: var(--font-weight-ui); + border-radius: var(--radius-ui); + border: 1px solid var(--color-ui--border); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + padding: var(--spacing-micro-padding-sm); + + &[data-state="checked"], + &[data-state="indeterminate"] { + background: var(--color-ui--text); + color: var(--color-ui); + border-color: var(--color-ui--text); + } + + &[data-state="unchecked"] { + background: var(--color-ui); + color: var(--color-ui--text); + border-color: var(--color-ui--border); + } + + &:hover { + background-color: var(--color-ui-hover); + } + + &:active { + background-color: var(--color-ui-active); + } + + &[data-state="checked"]:hover, + &[data-state="indeterminate"]:hover { + background-color: var(--color-ui--text); + opacity: 0.9; + } + + &:focus-visible, + &[data-focus] { + outline: 2px solid var(--color-ui--text); + outline-offset: -3px; + } + + &:disabled, + &[data-disabled], + &[data-disabled="true"], + &[disabled="true"] { + color: var(--color-ui--muted); + background-color: --alpha(var(--color-ui) / 60%); + cursor: not-allowed; + } + + &[data-invalid] { + color: var(--color-ui-alert); + border-color: var(--color-ui-alert); + } + + &[data-required] { + color: var(--color-ui-alert); + border-color: var(--color-ui-alert); + } + } + + .checkbox [data-scope="checkbox"][data-part="control"] .data-indeterminate { + display: none !important; + } + + .checkbox [data-scope="checkbox"][data-part="control"] .data-checked { + display: none !important; + } + + .checkbox + [data-scope="checkbox"][data-part="control"][data-state="checked"] + .data-checked { + display: block !important; + @apply ui-icon; + color: var(--color-ui-accent--text); + width: 100%; + height: 100%; + } + + .checkbox + [data-scope="checkbox"][data-part="control"][data-state="indeterminate"] + .data-indeterminate { + display: block !important; + @apply ui-icon; + color: var(--color-ui-accent--text); + width: 100%; + height: 100%; + } + .checkbox [data-scope="checkbox"][data-part="error"] { + @apply ui-error; + } +} + +@utility checkbox--* { + [data-scope="checkbox"][data-part="root"] { + max-width: --value(--container-ui-*, [length]); + } + + [data-scope="checkbox"][data-part="label"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + } + + [data-scope="checkbox"][data-part="control"] { + max-width: --value(--container-mini-*, [length]); + padding: --value(--spacing-mini-padding-*, [length]); + gap: --value(--spacing-mini-gap-*, [length]); + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-mini-*, [length]); + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + border-color: --value(--color-ui-* --border, [color]); + } + + [data-scope="checkbox"][data-part="control"]:hover { + background-color: --value(--color-ui-* -hover, [color]); + } + + [data-scope="checkbox"][data-part="control"]:active { + background-color: --value(--color-ui-* -active, [color]); + } + + [data-scope="checkbox"][data-part="control"]:focus-visible, + [data-scope="checkbox"][data-part="control"][data-focus] { + outline-color: --value(--color-ui-* --text, [color]); + } + + [data-scope="checkbox"][data-part="control"]:disabled, + [data-scope="checkbox"][data-part="control"][data-disabled], + [data-scope="checkbox"][data-part="control"][data-disabled="true"], + [data-scope="checkbox"][data-part="control"][disabled="true"] { + color: var(--color-ui--muted); + background-color: --alpha(var(--color-ui) / 60%); + cursor: not-allowed; + } +} diff --git a/design/components/clipboard.css b/design/components/clipboard.css new file mode 100644 index 0000000..eba8c2f --- /dev/null +++ b/design/components/clipboard.css @@ -0,0 +1,63 @@ +@import "../main.css"; + +@layer components { + .clipboard [data-scope="clipboard"][data-part="root"] { + @apply ui-root; + + gap: var(--spacing-ui-gap); + max-width: var(--container-ui); + } + + .clipboard [data-scope="clipboard"][data-part="trigger"] { + @apply ui-trigger; + } + + .clipboard [data-scope="clipboard"][data-part="label"] { + @apply ui-label; + } + + .clipboard [data-scope="clipboard"][data-part="control"] { + display: flex; + flex-flow: row wrap; + justify-content: center; + gap: var(--spacing-ui-gap); + } + + .clipboard [data-scope="clipboard"][data-part="input"] { + @apply ui-input; + + max-width: var(--container-mini); + } + + .clipboard [data-scope="clipboard"][data-part="input"][data-copied] { + background-color: var(--color-ui-success) !important; + color: var(--color-ui-success--text) !important; + border-color: var(--color-ui-success--border) !important; + } + + .clipboard [data-scope="clipboard"][data-part="trigger"] .data-copy { + display: inline-block; + } + + .clipboard [data-scope="clipboard"][data-part="trigger"] .data-copied { + display: none !important; + } + + .clipboard [data-scope="clipboard"][data-part="trigger"][data-copied] { + background-color: var(--color-ui-success); + color: var(--color-ui-success--text); + outline-color: var(--color-ui-success--text); + } + + .clipboard + [data-scope="clipboard"][data-part="trigger"][data-copied] + .data-copy { + display: none !important; + } + + .clipboard + [data-scope="clipboard"][data-part="trigger"][data-copied] + .data-copied { + display: inline-block !important; + } +} diff --git a/design/components/collapsible.css b/design/components/collapsible.css new file mode 100644 index 0000000..67a1f3b --- /dev/null +++ b/design/components/collapsible.css @@ -0,0 +1,120 @@ +@import "../main.css"; + +@layer components { + + .collapsible [data-scope="collapsible"][data-part="root"] { + @apply ui-root; + + /* max-width: var(--container-ui); + gap: var(--spacing-ui-gap); */ + } + + .collapsible [data-scope="collapsible"][data-part="trigger"] { + @apply ui-trigger; + + justify-content: space-between; + max-width: var(--container-mini); + place-self: flex-start; + width: auto; + box-shadow: var(--shadow-ui); + } + + .collapsible [data-scope="collapsible"][data-part="trigger"] svg { + @apply ui-icon; + } + + .collapsible + [data-scope="collapsible"][data-part="trigger"][data-state="closed"] + [data-closed] { + display: none; + } + + .collapsible + [data-scope="collapsible"][data-part="trigger"][data-state="open"] + [data-open] { + display: none; + } + + .collapsible + [data-scope="collapsible"][data-part="content"][data-state="open"] { + animation: slide-down 200ms ease; + } + + .collapsible + [data-scope="collapsible"][data-part="content"][data-state="closed"] { + animation: slide-up 200ms ease; + } + + .collapsible + [data-scope="collapsible"][data-part="indicator"][data-state="open"] { + transform: rotate(90deg); + } + + .collapsible [data-scope="collapsible"][data-part="content"] { + @apply ui-content; + } + + @keyframes slide-down { + from { + opacity: 0.01; + height: 0; + } + + to { + opacity: 1; + height: var(--height); + } + } + + @keyframes slide-up { + from { + opacity: 1; + height: var(--height); + } + + to { + opacity: 0.01; + height: 0; + } + } +} + +@utility collapsible--* { + [data-scope="collapsible"][data-part="root"] { + max-width: --value(--container-ui-*, [length]); + gap: --value(--spacing-gap-ui-*, [length]); + } + + [data-scope="collapsible"][data-part="trigger"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="collapsible"][data-part="trigger"]:hover { + background-color: --value(--color-ui-* -hover, [color]); + } + + [data-scope="collapsible"][data-part="trigger"]:active { + background-color: --value(--color-ui-* -active, [color]); + } + + [data-scope="collapsible"][data-part="trigger"]:focus-visible { + outline-color: --value(--color-ui-* --text, [color]); + } + + [data-scope="collapsible"][data-part="trigger"] + [data-scope="collapsible"][data-part="item-indicator"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="collapsible"][data-part="content"], + [data-scope="collapsible"][data-part="content"] > p { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + } +} diff --git a/design/components/combobox.css b/design/components/combobox.css new file mode 100644 index 0000000..7731191 --- /dev/null +++ b/design/components/combobox.css @@ -0,0 +1,164 @@ +@import "../main.css"; +@import "./scrollbar.css"; + +@layer components { + .combobox [data-scope="combobox"][data-part="root"] { + @apply ui-root; + + max-width: var(--container-mini); + gap: var(--spacing-ui-gap); + width: auto; + } + + .combobox [data-scope="combobox"][data-part="label"] { + @apply ui-label; + } + + .combobox [data-scope="combobox"][data-part="control"] { + display: flex; + width: 100%; + position: relative; + } + + .combobox [data-scope="combobox"][data-part="input"] { + @apply ui-input; + + max-width: 100%; + width: 100%; + padding-inline: var(--spacing-ui-padding); + border-inline-end: none; + border-end-end-radius: 0; + border-start-end-radius: 0; + } + + .combobox [data-scope="combobox"][data-part="trigger"] { + @apply ui-trigger ui-trigger--square; + + border-inline-start: none; + border-start-start-radius: 0; + border-end-start-radius: 0; + flex-shrink: 0; + } + + .combobox [data-scope="combobox"][data-part="clear-trigger"] { + @apply ui-trigger ui-trigger--square; + + border-inline: none; + border-radius: 0; + flex-shrink: 0; + } + + .combobox [data-scope="combobox"][data-part="positioner"] { + position: relative; + z-index: 50; + } + + .combobox [data-scope="combobox"][data-part="content"] { + @apply ui-content scrollbar scrollbar--sm; + + z-index: 50; + max-height: calc(var(--spacing) * 96); + overflow: auto; + padding: 0; + box-shadow: var(--shadow-ui); + } + + .combobox [data-scope="combobox"][data-part="item-group"] { + display: flex; + flex-direction: column; + } + + .combobox [data-scope="combobox"][data-part="item-group-label"] { + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + text-align: start; + padding-inline: var(--spacing-ui-padding); + padding-block: var(--spacing-mini-padding-sm); + background-color: var(--color-root); + color: var(--color-root--text); + border-bottom: 1px solid var(--color-ui--border); + } + + .combobox [data-scope="combobox"][data-part="item"] { + @apply ui-item; + } + + .combobox [data-scope="combobox"][data-part="error"] { + @apply ui-error; + } +} + +@utility combobox--* { + [data-scope="combobox"][data-part="root"] { + max-width: --value(--container-mini-*, [length]); + gap: --value(--spacing-mini-gap-*, [length]); + } + + [data-scope="combobox"][data-part="label"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + } + + [data-scope="combobox"][data-part="input"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + color: --value(--color-root--*, [color]); + border-color: --value(--color-root-* --border, [color]); + + ::placeholder { + color: --value(--color-root--*, [color]); + } + } + + [data-scope="combobox"][data-part="trigger"], + [data-scope="combobox"][data-part="clear-trigger"] { + color: --value(--color-root--*, [color]); + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + padding-inline: --value(--spacing-ui-padding-*, [length]); + gap: --value(--spacing-ui-gap-*, [length]); + } + + [data-scope="combobox"][data-part="item"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + } + + [data-scope="combobox"][data-part="item"][data-selected], + [data-scope="combobox"][data-part="item"][data-state="checked"] { + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="combobox"][data-part="item"][data-selected]:hover, + [data-scope="combobox"][data-part="item"][data-state="checked"]:hover { + background-color: --value(--color-ui-* -hover, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="combobox"][data-part="item"][data-selected]:active { + background-color: --value(--color-ui-* -active, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="combobox"][data-part="item"][data-highlighted]:not(:hover) { + outline: 2px solid --value(--color-ui-* --text, [color]); + background-color: --value(--color-ui-* -hover, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="combobox"][data-part="item"][data-selected][data-highlighted], + [data-scope="combobox"][data-part="item"][data-state="checked"][data-highlighted] { + outline: 2px solid var(--color-ui-accent--text); + background-color: --value(--color-ui-* -hover, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="combobox"][data-part="trigger"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + } +} diff --git a/design/components/date-picker.css b/design/components/date-picker.css new file mode 100644 index 0000000..0134800 --- /dev/null +++ b/design/components/date-picker.css @@ -0,0 +1,177 @@ +@import "../main.css"; + +@layer components { + + .date-picker [data-scope="date-picker"][data-part="root"] { + @apply ui-root; + width: var(--container-mini); + gap: var(--spacing-ui-gap); + } + + .date-picker [data-scope="date-picker"][data-part="label"] { + @apply ui-label; + + &[data-invalid] { + color: var(--color-root-alert); + } + } + + .date-picker [data-scope="date-picker"][data-part="control"] { + display: flex; + flex-flow: row nowrap; + place-items: center; + padding: 0; + width: var(--container-mini); + overflow: hidden; + gap: var(--spacing-mini-gap); + justify-content: flex-start; + border-radius: var(--radius-ui); + } + + .date-picker [data-scope="date-picker"][data-part="trigger"] { + @apply ui-trigger; + + min-height: var(--spacing-ui); + padding: 0; + aspect-ratio: 1 / 1; + width: auto; + position: relative; + } + + + .date-picker [data-scope="date-picker"][data-part="input"] { + @apply ui-input; + + width: 100%; + min-width: 0; + flex: 1 1 0%; + } + + + + /* .date-picker [data-scope="date-picker"][data-part="positioner"] { + position: absolute; + width: 0; + height: 0; + overflow: hidden; + } */ + + .date-picker [data-scope="date-picker"][data-part="content"] { + @apply ui-content; + + flex-direction: column; + z-index: 30; + padding: 0; + box-shadow: var(--shadow-ui); + max-width: fit-content; + gap: var(--spacing-mini-gap); + } + + .date-picker[data-inline="true"] + [data-scope="date-picker"][data-part="content"] { + z-index: 0; + } + + .date-picker [data-scope="date-picker"][data-part="table"] { + width: 100%; + border-collapse: collapse; + padding: 0; + margin: 0; + place-items: center; + } + + .date-picker [data-scope="date-picker"][data-part="table-header"], + .date-picker [data-scope="date-picker"][data-part="table-header"] th { + border: 0; + } + + .date-picker [data-scope="date-picker"][data-part="table-header"] th { + font-weight: var(--font-weight-ui); + padding: var(--spacing-mini-padding); + text-align: center; + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + color: var(--color-ui--text); + text-transform: uppercase; + } + + .date-picker [data-scope="date-picker"][data-part="view-control"] { + display: flex; + width: 100%; + justify-content: space-between; + align-items: center; + padding-inline: var(--spacing-mini-padding); + padding-block: var(--spacing-mini-padding); + } + + .date-picker [data-scope="date-picker"][data-part="table-cell"] { + position: relative; + padding: 0; + border: 0; + } + + .date-picker [data-scope="date-picker"][data-part="table-cell-trigger"] { + @apply ui-item; + + text-align: center; + justify-items: center; + justify-content: center; + min-height: var(--spacing-ui-sm); + width: var(--spacing-ui-sm); + padding: var(--spacing-ui-padding); + aspect-ratio: 1/1; + overflow: hidden; + text-overflow: ellipsis; + } + + .date-picker + [data-scope="date-picker"][data-part="table-cell-trigger"][data-view="month"], + .date-picker + [data-scope="date-picker"][data-part="table-cell-trigger"][data-view="year"] { + min-height: var(--spacing-ui-xl); + width: var(--spacing-ui-xl); + } + + .date-picker + [data-scope="date-picker"][data-part="table-cell-trigger"][data-today]:not( + [data-selected] + ) { + font-weight: var(--font-weight-ui-lg); + text-decoration: underline; + text-underline-offset: var(--spacing-micro-sm); + position: relative; + } + + .date-picker + [data-scope="date-picker"][data-part="table-cell-trigger"][data-outside-month] { + color: var(--color-ui--muted); + opacity: 0.5; + } + + .date-picker + [data-scope="date-picker"][data-part="table-cell-trigger"][data-disabled] { + color: var(--color-ui--muted); + cursor: not-allowed; + pointer-events: none; + } + + .date-picker + [data-scope="date-picker"][data-part="table-cell-trigger"][data-unavailable] { + text-decoration: line-through; + cursor: not-allowed; + color: var(--color-ui--muted); + } + + .date-picker [data-scope="date-picker"][data-part="prev-trigger"], + .date-picker [data-scope="date-picker"][data-part="next-trigger"] { + @apply ui-trigger ui-trigger--sm ui-trigger--square; + } + + .date-picker [data-scope="date-picker"][data-part="view-trigger"] { + @apply ui-trigger ui-trigger--sm; + } + + .date-picker [data-scope="date-picker"][data-part="error"] { + @apply ui-error; + } +} diff --git a/design/components/dialog.css b/design/components/dialog.css new file mode 100644 index 0000000..202e30c --- /dev/null +++ b/design/components/dialog.css @@ -0,0 +1,82 @@ +@import "../main.css"; + +@layer components { + .dialog [data-scope="dialog"][data-part="trigger"] { + @apply ui-trigger; + } + + .dialog [data-scope="dialog"][data-part="close-trigger"] { + @apply ui-trigger ui-trigger--circle ui-trigger--sm; + + flex-shrink: 0; + height: fit-content; + } + + .dialog [data-scope="dialog"][data-part="backdrop"] { + position: fixed; + inset: 0; + width: 100vw; + height: 100vh; + background: --alpha(var(--color-ui-accent) / 95%); + z-index: 10; + } + + .dialog [data-scope="dialog"][data-part="positioner"] { + position: fixed; + inset: 0; + width: 100vw; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + } + + .dialog [data-scope="dialog"][data-part="content"] { + @apply ui-content; + + max-width: var(--container-ui); + margin: var(--spacing-ui-padding); + padding: var(--spacing-ui-padding); + } + + + .dialog [data-scope="dialog"][data-part="header"] { + display: flex; + align-items: center; + gap: var(--spacing-ui-gap); + } + + .dialog [data-scope="dialog"][data-part="header"] [data-scope="dialog"][data-part="title"] { + flex: 1 1 0%; + min-width: 0; + } + + .dialog [data-part="row"] { + display: inline-flex; + flex-direction: row; + justify-content: space-between; + } + + .dialog [data-scope="dialog"][data-part="title"], + .dialog [data-scope="dialog"][data-part="description"] { + margin-block: 0; + } + + .dialog.dialog--side [data-scope="dialog"][data-part="positioner"] { + align-items: stretch; + justify-content: flex-start; + height: 100vh; + } + + .dialog.dialog--side [data-scope="dialog"][data-part="content"] { + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + width: auto; + padding: 0; + margin: 0; + border-radius: var(--radius-none); + } +} diff --git a/design/components/icon.css b/design/components/icon.css new file mode 100644 index 0000000..297cba1 --- /dev/null +++ b/design/components/icon.css @@ -0,0 +1,17 @@ +@import "../main.css"; + +@layer components { + .icon { + @apply ui-icon; + font-size: 1rem; + + } +} + +@utility icon--* { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + height: --value(--spacing-ui-*, [length])!important; + width: --value(--spacing-ui-*, [length])!important; + color: --value(--color-ui-*, [color]); +} diff --git a/design/components/layout.css b/design/components/layout.css new file mode 100644 index 0000000..0c2d88b --- /dev/null +++ b/design/components/layout.css @@ -0,0 +1,203 @@ +@import "../main.css"; +@import "./scrollbar.css"; + +@layer components { + .layout { + @apply scrollbar; + + display: flex; + flex-direction: column; + min-height: 100vh; + color: var(--color-root--text); + background: var(--color-root); + overflow-y: scroll; + + scrollbar-gutter: stable both-edges; + } + + .layout__header { + position: sticky; + top: 0; + z-index: 10; + height: var(--spacing-ui-lg); + background: var(--color-layer); + color: var(--color-layer--text); + border-bottom: 1px solid var(--color-layer--border); + display: flex; + align-items: center; + } + + .layout__header__content, + .layout__footer__content, + .layout__side__content { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + margin-inline: auto; + width: 100%; + padding-inline: var(--spacing-ui-padding); + padding-block: var(--spacing-ui-padding-sm); + } + + .layout__footer { + background: var(--color-layer); + color: var(--color-layer--text); + border-bottom: 1px solid var(--color-layer--border); + display: flex; + align-items: center; + min-height: var(--spacing-ui-lg); + } + + .layout__wrapper { + display: flex; + flex: 1; + width: 100%; + justify-content: center; + margin-inline: auto; + background-color: var(--color-root); + position: relative; + } + + .layout__side, + .layout__aside { + @apply scrollbar scrollbar--sm; + + position: sticky; + display: flex; + flex-direction: column; + top: calc(var(--spacing-ui-lg)); + align-self: flex-start; + background: var(--color-layer); + color: var(--color-layer--text); + min-width: var(--container-micro); + max-width: var(--container-ui); + width: auto; + height: calc(100vh - var(--spacing-ui-lg)); + overflow-y: scroll; + scrollbar-gutter: stable both-edges; + + } + + .layout__aside-mobile { + background: var(--color-layer); + color: var(--color-layer--text); + width: 100%; + } + + .layout__main { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; + width: 100%; + margin-inline: auto; + margin-block-end: var(--container-micro); + position: relative; + } + + .layout__main__wrapper { + display: flex; + flex-direction: row; + width: 100%; + flex: 1; + justify-content: flex-start; + align-items: center; + } + + .layout__content { + display: flex; + flex-direction: column; + width: 100%; + flex: 1; + justify-content: flex-start; + align-items: center; + padding: var(--spacing-ui-padding); + gap: var(--spacing-ui-gap); + } + + .layout__article { + width: 100%; + min-width: 0; + max-width: var(--breakpoint-lg); + padding: var(--spacing-ui-padding); + color: var(--color-layer--text); + border-radius: var(--radius-ui); + margin-inline: auto; + justify-items: center; + align-items: center; + gap: var(--spacing-ui-gap); + display: flex; + flex-direction: column; + } + + .layout .layout__row { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + align-items: center; + gap: var(--spacing-ui-gap); + width: auto + + } + + .layout .layout__section { + display: flex; + flex-direction: column; + align-items: start; + justify-content: center; + gap: var(--spacing-ui-gap); + margin-block: var(--spacing-ui); + min-width: var(--container-ui) + } + + .layout .layout__side, + .layout .layout__aside { + display: none; + } + + @media (min-width: theme("breakpoint.md")) { + .layout .layout__side { + display: flex; + } + } + + @media (min-width: theme("breakpoint.lg")) { + .layout .layout__aside { + display: flex; + } + } + + .layout .layout__aside-mobile { + display: flex; + } + + @media (min-width: theme("breakpoint.lg")) { + .layout .layout__aside-mobile { + display: none; + } + } + + .layout .layout__side__mobile { + display: flex; + } + + @media (min-width: theme("breakpoint.md")) { + .layout .layout__side__mobile { + display: none; + } + } + + .layout--boxed { + .layout__header__content, + .layout__footer__content, + .layout__wrapper { + max-width: var(--breakpoint-xl); + } + + .layout__article { + max-width: var(--breakpoint-lg); + } + } +} diff --git a/design/components/link.css b/design/components/link.css new file mode 100644 index 0000000..36474b0 --- /dev/null +++ b/design/components/link.css @@ -0,0 +1,78 @@ +@import "../main.css"; + +@layer components { + .link { + display: inline-flex; + justify-content: start; + align-items: center; + cursor: pointer; + position: relative; + color: var(--color-layer--info); + height: inherit; + font-size: inherit; + line-height: inherit; + gap: var(--spacing-ui-gap); + padding-inline: var(--spacing-ui-padding); + border-radius: var(--radius-ui); + text-decoration: none; + max-width: fit-content; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + } + + .link::after { + content: ""; + position: absolute; + left: 50%; + bottom: 0; + height: 1px; + width: 80%; + background-color: currentcolor; + transform: translateX(-50%); + transition: width 0.3s ease; + } + + .link:hover::after { + width: 90%; + } + + .link:focus-visible { + outline: 2px solid var(--color-ui--focus); + outline-offset: 1px; + box-shadow: none; + } + + .link:focus-visible::after { + display: none; + } + + .link:disabled, + .link[data-disabled] { + opacity: 0.6; + pointer-events: none; + box-shadow: none; + } + + .link .icon, + .link svg, + .link img { + @apply ui-icon; + } + + .link[aria-current="page"], + .link[aria-current="location"] { + font-weight: var(--font-weight-ui-lg); + pointer-events: none; + } +} + +@utility link--* { + color: --value(--color-layer--*, [color]); + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + padding-inline: --value(--spacing-ui-padding-*, [length]); + gap: --value(--spacing-ui-gap-*, [length]); +} diff --git a/design/components/menu.css b/design/components/menu.css new file mode 100644 index 0000000..5132211 --- /dev/null +++ b/design/components/menu.css @@ -0,0 +1,59 @@ +@import "../main.css"; + +@layer components { + .menu [data-scope="menu"][data-part="content"] { + @apply ui-content; + + border: 1px solid var(--color-ui--border); + z-index: 50; + padding: 0; + border-radius: var(--radius-ui); + overflow: hidden; + box-shadow: var(--shadow-ui); + + &:focus-visible { + outline: none; + } + } + + .menu [data-scope="menu"][data-part="trigger"] { + @apply ui-trigger; + + margin-inline-end: var(--spacing-ui-padding); + border: none; + min-width: var(--container-micro); + } + + .menu [data-scope="menu"][data-part="context-trigger"] { + @apply ui-trigger; + + border: 1px dashed var(--color-ui--muted); + cursor: context-menu; + } + + .menu [data-scope="menu"][data-part="separator"] { + height: 1px; + width: 100%; + background: var(--color-ui--border); + } + + .menu [data-scope="menu"][data-part="item-group"] { + display: flex; + flex-direction: column; + background-color: transparent; + } + + .menu [data-scope="menu"][data-part="item-group-label"] { + @apply ui-label; + + border-block-end: 1px solid var(--color-ui--border); + padding-inline: var(--spacing-ui-padding) + } + + .menu [data-scope="menu"][data-part="item"], + .menu [data-scope="menu"][data-part="trigger-item"] { + @apply ui-item; + + min-width: var(--container-micro); + } +} diff --git a/design/components/scrollbar.css b/design/components/scrollbar.css new file mode 100644 index 0000000..6ea4d22 --- /dev/null +++ b/design/components/scrollbar.css @@ -0,0 +1,35 @@ +@import "../main.css"; + +@utility scrollbar { + &::-webkit-scrollbar { + width: var(--spacing-micro); + height: var(--spacing-micro); + } + + &::-webkit-scrollbar-track { + background: var(--color-ui); + } + + &::-webkit-scrollbar-thumb { + background: var(--color-ui--border); + } + + &::-webkit-scrollbar-corner { + background: var(--color-ui--border); + } +} + +@utility scrollbar--* { + &::-webkit-scrollbar { + width: --value(--spacing-micro-*, [integer]); + height: --value(--spacing-micro-*, [integer]); + } + + &::-webkit-scrollbar-thumb { + background: --value(--color-ui-*, [color]); + } + + &::-webkit-scrollbar-corner { + background: --value(--color-ui-*, [color]); + } +} diff --git a/design/components/select.css b/design/components/select.css new file mode 100644 index 0000000..e7c48eb --- /dev/null +++ b/design/components/select.css @@ -0,0 +1,134 @@ +@import "../main.css"; + +@layer components { + .select [data-scope="select"][data-part="root"] { + @apply ui-root; + width: var(--container-mini); + gap: var(--spacing-ui-gap); + } + + + .select [data-scope="select"][data-part="label"] { + @apply ui-label; + } + + .select [data-scope="select"][data-part="control"] { + display: flex; + width: 100%; + position: relative; + } + + .select [data-scope="select"][data-part="trigger"] { + @apply ui-trigger; + + justify-content: space-between; + width: 100%; + } + + .select [data-scope="select"][data-part="positioner"] { + position: relative; + } + + .select [data-scope="select"][data-part="content"] { + @apply ui-content; + + border: 1px solid var(--color-ui--border); + z-index: 50; + padding: 0; + border-radius: var(--radius-ui); + overflow: hidden; + box-shadow: var(--shadow-ui); + + &:focus-visible { + outline: none; + } + } + + .select [data-scope="select"][data-part="item-group"] { + display: flex; + flex-direction: column; + } + + .select [data-scope="select"][data-part="item-group-label"] { + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + text-align: start; + padding-inline: var(--spacing-ui-padding); + padding-block: var(--spacing-mini-padding-sm); + background-color: var(--color-root); + color: var(--color-root--text); + border-bottom: 1px solid var(--color-ui--border); + } + + .select [data-scope="select"][data-part="item"] { + @apply ui-item; + } + + .select [data-scope="select"][data-part="item-text"] { + width: 100%; + text-align: start; + } + + .select [data-scope="select"][data-part="error"] { + @apply ui-error; + } +} + +@utility select--* { + [data-scope="select"][data-part="root"] { + width: --value(--container-*, [length]); + } + + [data-scope="select"][data-part="label"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + } + + [data-scope="select"][data-part="trigger"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + } + + [data-scope="select"][data-part="control"] { + max-width: --value(--container-mini-*, [length]); + gap: --value(--spacing-mini-gap-*, [length]); + } + + [data-scope="select"][data-part="item"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + } + + /* States for item */ + [data-scope="select"][data-part="item"][data-selected], + [data-scope="select"][data-part="item"][data-state="checked"] { + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="select"][data-part="item"][data-selected]:hover, + [data-scope="select"][data-part="item"][data-state="checked"]:hover { + background-color: --value(--color-ui-* -hover, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="select"][data-part="item"][data-selected]:active { + background-color: --value(--color-ui-* -active, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="select"][data-part="item"][data-highlighted] { + outline: 2px solid --value(--color-ui-* --text, [color]); + background-color: --value(--color-ui-* -hover, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="select"][data-part="item"][data-selected][data-highlighted], + [data-scope="select"][data-part="item"][data-state="checked"][data-highlighted] { + outline: 2px solid var(--color-ui-accent--text); + background-color: --value(--color-ui-* -hover, [color]); + color: --value(--color-ui-* --text, [color]); + } +} diff --git a/design/components/signature-pad.css b/design/components/signature-pad.css new file mode 100644 index 0000000..2850c51 --- /dev/null +++ b/design/components/signature-pad.css @@ -0,0 +1,77 @@ +@import "../main.css"; + +@layer components { + [data-scope="signature-pad"][data-part="root"] { + @apply ui-root; + + width: var(--container-mini); + gap: var(--spacing-ui-gap); + } + + [data-scope="signature-pad"][data-part="label"] { + @apply ui-label; + } + + [data-scope="signature-pad"][data-part="control"] { + position: relative; + width: 100%; + height: calc(var(--container-mini) * 0.7); + background-color: var(--color-ui); + border-radius: var(--radius-ui); + border: 1px solid var(--color-ui--border); + } + + [data-scope="signature-pad"][data-part="guide"] { + position: absolute; + bottom: var(--spacing-mini-gap); + left: var(--spacing-mini-gap); + right: var(--spacing-mini-gap); + border-bottom: 2px dashed var(--color-gray-800); + } + + [data-scope="signature-pad"][data-part="segment"] { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + background-color: var(--color-gray-100); + border-radius: var(--radius-ui); + } + + [data-scope="signature-pad"][data-part="clear-trigger"] { + @apply ui-trigger ui-trigger--sm ui-trigger--circle; + + position: absolute; + top: var(--spacing-mini-gap); + right: var(--spacing-mini-gap); + } + + .signature-pad [data-part="preview"] { + display: flex; + flex-direction: column; + align-items: center; + width: var(--container-mini); + padding: var(--spacing-mini-padding); + gap: var(--spacing-mini-gap); + background-color: var(--color-layer); + + & img { + width: 100%; + height: var(--container-micro); + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + padding: var(--spacing-mini-padding); + border: 1px solid var(--color-ui--border); + border-radius: var(--radius-ui); + background-color: var(--color-root); + } + } + + .signature-pad [data-scope="signature-pad"][data-part="error"] { + @apply ui-error; + } +} diff --git a/design/components/switch.css b/design/components/switch.css new file mode 100644 index 0000000..2ed0d02 --- /dev/null +++ b/design/components/switch.css @@ -0,0 +1,116 @@ +@import "../main.css"; + +@layer components { + .switch [data-scope="switch"][data-part="root"] { + --switch-track-width: calc(calc(var(--spacing-micro) * 1.5) * 2.5); + --switch-track-height: calc(var(--spacing-micro) * 1.5); + --switch-track-diff: calc( + var(--switch-track-width) - var(--switch-track-height) + ); + --switch-thumb-x: var(--switch-track-diff); + + display: flex; + align-items: center; + position: relative; + width: fit-content; + gap: var(--spacing-ui-gap); + } + + .switch [data-scope="switch"][data-part="control"] { + display: inline-flex; + flex-shrink: 0; + cursor: pointer; + justify-content: flex-start; + box-sizing: content-box; + border-radius: var(--radius-full); + border: 1px solid var(--color-ui--border); + padding: var(--spacing-micro-padding); + width: var(--switch-track-width); + height: var(--switch-track-height); + transition-property: + background, border-color, color, fill, stroke, opacity, box-shadow, + transform; + transition-duration: 150ms; + background: var(--color-ui); + } + + .switch [data-scope="switch"][data-part="control"][data-disabled] { + opacity: 0.6; + pointer-events: none; + box-shadow: none; + } + + .switch [data-scope="switch"][data-part="control"][data-invalid] { + border-color: var(--color-ui-alert); + } + + .switch [data-scope="switch"][data-part="thumb"][data-invalid] { + background: var(--color-ui-alert); + } + + .switch [data-scope="switch"][data-part="control"][data-state="checked"] { + background-color: var(--color-ui-accent); + + & [data-scope="switch"][data-part="thumb"] { + background-color: var(--color-ui-accent--text); + } + } + + .switch [data-scope="switch"][data-part="thumb"] { + background: var(--color-ui-accent); + transition-property: transform, ackground-color; + transition-duration: 200ms; + border-radius: inherit; + width: var(--switch-track-height); + height: var(--switch-track-height); + position: relative; + } + + .switch [data-scope="switch"][data-part="thumb"][data-state="checked"] { + transform: translateX(var(--switch-thumb-x)); + background: var(--color-ui-accent); + } + + .switch [data-scope="switch"][data-part="label"] { + @apply ui-label; + + cursor: pointer; + } + + .switch--square [data-scope="switch"][data-part="control"] { + border-radius: var(--radius-none); + } + + .switch [data-scope="switch"][data-part="error"] { + @apply ui-error; + } +} + +@utility switch--* { + [data-scope="switch"][data-part="root"] { + max-width: --value(--container-ui-*, [length]); + } + + [data-scope="switch"][data-part="control"][data-state="checked"] { + background-color: --value(--color-ui-*, [color]); + + [data-scope="switch"][data-part="thumb"] { + background-color: --value(--color-ui-* --text, [color]); + } + } + + [data-scope="switch"][data-part="control"] { + --switch-track-width: calc( + calc(--value(--spacing-micro-*, [length]) * 1.5) * 2.5 + ); + --switch-track-height: calc(--value(--spacing-micro-*, [length]) * 1.5); + --switch-track-diff: calc( + var(--switch-track-width) - var(--switch-track-height) + ); + --switch-thumb-x: var(--switch-track-diff); + + & [data-scope="switch"][data-part="thumb"] { + background: --value(--color-ui-*, [color]); + } + } +} diff --git a/design/components/tabs.css b/design/components/tabs.css new file mode 100644 index 0000000..86c9c3b --- /dev/null +++ b/design/components/tabs.css @@ -0,0 +1,78 @@ +@import "../main.css"; + +@layer components { + .tabs [data-scope="tabs"][data-part="root"] { + @apply ui-root; + + max-width: var(--container-ui); + } + + .tabs [data-scope="tabs"][data-part="root"][data-orientation="vertical"] { + flex-direction: row; + } + + .tabs [data-scope="tabs"][data-part="root"][data-orientation="horizontal"] { + flex-direction: column; + } + + .tabs [data-scope="tabs"][data-part="list"] { + display: inline-flex; + background-color: var(--color-ui--border); + gap: 1px; + width: fit-content; + border: 1px solid var(--color-ui--border); + border-radius: var(--radius-ui); + overflow: hidden; + } + + .tabs [data-scope="tabs"][data-part="list"][data-orientation="vertical"] { + flex-direction: column; + height: fit-content; + width: var(--container-micro); + } + + .tabs [data-scope="tabs"][data-part="list"][data-orientation="horizontal"] { + flex-direction: row; + } + + .tabs [data-scope="tabs"][data-part="content"] { + @apply ui-content; + + border: 1px solid var(--color-ui--border); + border-radius: var(--radius-ui); + padding: var(--spacing-ui-padding); + + & p { + margin-block: 0; + } + } + + .tabs [data-scope="tabs"][data-part="trigger"] { + @apply ui-item; + + width: auto; + } +} + +@utility tabs--* { + [data-scope="tabs"][data-part="root"] { + max-width: --value(--container-ui-*, [length]); + gap: --value(--spacing-ui-gap-*, [length]); + } + + [data-scope="tabs"][data-part="trigger"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + + &[data-selected] { + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + } + } + + [data-scope="tabs"][data-part="content"] > p { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + } +} diff --git a/design/components/toast.css b/design/components/toast.css new file mode 100644 index 0000000..c1d5bfa --- /dev/null +++ b/design/components/toast.css @@ -0,0 +1,164 @@ +@import "../main.css"; + +@layer components { + .toast [data-scope="toast"][data-part="group"] { + display: flex; + flex-direction: column; + gap: var(--spacing-ui-gap); + width: var(--container-mini); + } + + .toast [data-scope="toast"][data-part="root"] { + @apply ui-root; + max-width: var(--container-mini); + + translate: var(--x) var(--y); + scale: var(--scale); + z-index: var(--z-index); + height: var(--height); + opacity: var(--opacity); + will-change: translate, opacity, scale; + + transition: + translate 400ms cubic-bezier(0.21, 1.02, 0.73, 1), + scale 400ms cubic-bezier(0.21, 1.02, 0.73, 1), + opacity 400ms cubic-bezier(0.21, 1.02, 0.73, 1), + height 400ms cubic-bezier(0.21, 1.02, 0.73, 1), + box-shadow 200ms; + + &[data-state="closed"] { + transition: + translate 400ms cubic-bezier(0.06, 0.71, 0.55, 1), + scale 400ms cubic-bezier(0.06, 0.71, 0.55, 1), + opacity 200ms cubic-bezier(0.06, 0.71, 0.55, 1); + } + } + + .toast [data-scope="toast"][data-part="content"] { + @apply ui-content; + + padding: 0; + border-radius: var(--radius-ui); + overflow: hidden; + box-shadow: var(--shadow-ui); + border: 1px solid var(--color-ui--border); + } + + .toast [data-scope="toast"][data-part="title"] { + @apply ui-label; + + justify-content: start; + width: 100%; + background-color: var(--color-layer); + color: var(--color-layer--text); + padding-block: var(--spacing-ui-padding-sm); + border-block-end: 1px solid var(--color-ui--border); + padding-inline: var(--spacing-ui-lg); + } + + .toast [data-scope="toast"][data-part="description"] { + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + font-weight: var(--font-weight-ui); + padding: var(--spacing-ui-padding); + min-height: var(--spacing-ui-xl); + } + + .toast [data-scope="toast"][data-part="close-trigger"] { + @apply ui-trigger ui-trigger--sm ui-trigger--circle; + + position: absolute; + min-height: var(--spacing-mini-sm); + right: var(--spacing-mini-gap); + top: var(--spacing-ui-gap-sm); + } + + .toast [data-scope="toast"][data-part="root"][data-type="error"] [data-scope="toast"][data-part="title"] { + color: var(--color-ui-alert); + } + + .toast [data-scope="toast"][data-part="root"][data-type="error"] [data-scope="toast"][data-part="progressbar"] { + background: linear-gradient(to left, + --alpha(var(--color-ui-alert) / 80%), + var(--color-ui-alert)); + } + + .toast [data-scope="toast"][data-part="root"][data-type="info"] [data-scope="toast"][data-part="title"] { + color: var(--color-ui-info); + } + + .toast [data-scope="toast"][data-part="root"][data-type="info"] [data-scope="toast"][data-part="progressbar"] { + background: linear-gradient(to left, + --alpha(var(--color-ui-info) / 80%), + var(--color-ui-info)); + } + + .toast [data-scope="toast"][data-part="root"][data-type="success"] [data-scope="toast"][data-part="title"] { + color: var(--color-ui-success); + } + + .toast [data-scope="toast"][data-part="root"][data-type="success"] [data-scope="toast"][data-part="progressbar"] { + background: linear-gradient(to left, + --alpha(var(--color-ui-success) / 80%), + var(--color-ui-success)); + } + + .toast [data-scope="toast"][data-part="progressbar"] { + height: var(--spacing-micro-sm); + background: linear-gradient(to left, + --alpha(var(--color-ui-accent) / 80%), + var(--color-ui-accent)); + width: 100%; + position: absolute; + bottom: 0; + inset-inline: 0; + animation-name: shrink; + animation-duration: var(--duration); + animation-fill-mode: forwards; + animation-play-state: running; + transform-origin: left; + + [data-paused] & { + animation-play-state: paused; + } + + [dir="rtl"] & { + transform-origin: right; + } + } + + .toast [data-scope="toast"][data-part="loading-spinner"] { + @apply ui-icon ui-icon--sm; + + position: absolute; + min-height: var(--spacing-mini-sm); + left: var(--spacing-mini-gap); + top: var(--spacing-mini-gap-lg); + + animation: spin 1s linear infinite; + } + + .toast [data-scope="toast"][data-part="root"][data-duration-infinity="true"] [data-scope="toast"][data-part="progressbar"] { + display: none; + } + + @keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } + } + + @keyframes shrink { + from { + transform: scaleX(1); + } + + to { + transform: scaleX(0); + } + } +} \ No newline at end of file diff --git a/design/components/toggle-group.css b/design/components/toggle-group.css new file mode 100644 index 0000000..6f70e2a --- /dev/null +++ b/design/components/toggle-group.css @@ -0,0 +1,128 @@ +@import "../main.css"; + +@layer components { + .toggle-group [data-scope="toggle-group"][data-part="root"] { + @apply ui-root; + + justify-content: center; + flex-direction: row; + width: auto; + gap: 1px; + background-color: var(--color-ui--border); + border: 1px solid var(--color-ui--border); + border-radius: var(--radius-ui); + overflow: hidden; + } + + .toggle-group [data-scope="toggle-group"][data-part="item"] { + @apply ui-item; + + width: auto; + justify-content: center; + } + + .toggle-group.toggle-group--square [data-scope="toggle-group"][data-part="root"], + .toggle-group.toggle-group--circle [data-scope="toggle-group"][data-part="root"], + .toggle-group.toggle-group--square [data-scope="toggle-group"][data-part="item"], + .toggle-group.toggle-group--circle [data-scope="toggle-group"][data-part="item"] { + display: inline-flex; + aspect-ratio: 1 / 1; + justify-content: center; + align-items: center; + width: auto; + padding: 0; + } + + .toggle-group.toggle-group--circle [data-scope="toggle-group"][data-part="root"], + .toggle-group.toggle-group--circle [data-scope="toggle-group"][data-part="item"] { + border-radius: var(--radius-full); + } + + /* .toggle-group [data-scope="toggle-group"][data-part="item"] .icon.state-on, + .toggle-group [data-scope="toggle-group"][data-part="item"] .icon.state-off { + display: none; + } */ + + .toggle-group [data-scope="toggle-group"][data-part="item"][data-state="on"] .icon.state-on { + display: block !important; + } + + .toggle-group [data-scope="toggle-group"][data-part="item"][data-state="off"] .icon.state-on { + display: none !important; + } + .toggle-group [data-scope="toggle-group"][data-part="item"][data-state="on"] .icon.state-off { + display: none !important; + } + + .toggle-group [data-scope="toggle-group"][data-part="item"][data-state="off"] .icon.state-off { + display: block !important; + } + + .toggle-group.toggle-group--inverted [data-scope="toggle-group"][data-part="item"][data-state="on"] { + background-color: var(--color-ui); + color: var(--color-ui--text); + } + + .toggle-group.toggle-group--inverted [data-scope="toggle-group"][data-part="item"][data-state="on"]:hover { + background-color: var(--color-ui-accent); + color: var(--color-ui-accent--text); + } +} + +@utility toggle-group--* { + [data-scope="toggle-group"][data-part="root"] { + max-width: --value(--container-mini-*, [length]); + } + + [data-scope="toggle-group"][data-part="label"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + } + + [data-scope="toggle-group"][data-part="trigger"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + } + + [data-scope="toggle-group"][data-part="control"] { + width: --value(--container-mini-*, [length]); + gap: --value(--spacing-mini-gap-*, [length]); + } + + [data-scope="toggle-group"][data-part="item"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + } + + [data-scope="toggle-group"][data-part="item"][data-toggle-grouped], + [data-scope="toggle-group"][data-part="item"][data-state="checked"] { + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="toggle-group"][data-part="item"][data-toggle-grouped]:hover, + [data-scope="toggle-group"][data-part="item"][data-state="checked"]:hover { + background-color: --value(--color-ui-* -hover, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="toggle-group"][data-part="item"][data-toggle-grouped]:active { + background-color: --value(--color-ui-* -active, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="toggle-group"][data-part="item"][data-highlighted] { + outline: 2px solid --value(--color-ui-* --text, [color]); + background-color: --value(--color-ui-* -hover, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="toggle-group"][data-part="item"][data-toggle-grouped][data-highlighted], + [data-scope="toggle-group"][data-part="item"][data-state="checked"][data-highlighted] { + outline: 2px solid var(--color-ui-accent--text); + background-color: --value(--color-ui-* -hover, [color]); + color: --value(--color-ui-* --text, [color]); + } +} diff --git a/design/components/tree-view.css b/design/components/tree-view.css new file mode 100644 index 0000000..69869df --- /dev/null +++ b/design/components/tree-view.css @@ -0,0 +1,77 @@ +@import "../main.css"; + +@layer components { + .tree-view [data-scope="tree-view"][data-part="root"] { + @apply ui-root; + + max-width: var(--container-mini); + gap: var(--spacing-ui-gap); + } + + .tree-view [data-scope="tree-view"][data-part="tree"] { + display: flex; + flex-direction: column; + width: 100%; + gap: var(--spacing-ui-gap); + } + + .tree-view [data-scope="tree-view"][data-part="label"] { + @apply ui-label; + } + + .tree-view [data-scope="tree-view"][data-part="branch"] { + display: flex; + flex-direction: column; + width: 100%; + gap: var(--spacing-ui-gap-sm); + } + + .tree-view [data-scope="tree-view"][data-part="item"], + .tree-view [data-scope="tree-view"][data-part="branch-control"] { + @apply ui-item; + + border: 1px solid var(--color-ui--border); + border-radius: 0 var(--radius-ui) var(--radius-ui) 0; + } + + .tree-view [data-scope="tree-view"][data-part="item-text"], + .tree-view [data-scope="tree-view"][data-part="branch-text"] { + width: 100%; + text-align: start; + } + + .tree-view [data-scope="tree-view"][data-part="branch-content"] { + margin-inline-start: var(--spacing-ui-gap); + gap: var(--spacing-ui-gap-sm); + } + + .tree-view + [data-scope="tree-view"][data-part="branch-content"][data-state="open"] { + visibility: visible; + position: relative; + isolation: isolate; + display: flex; + flex-direction: column; + } + + .tree-view [data-scope="tree-view"][data-part="branch-label"] { + @apply ui-label; + } + + .tree-view + [data-part="branch-content"]:not([dir="rtl"]) + [data-part="branch-indent-guide"] { + position: absolute; + border-left: 2px solid var(--color-ui--border); + height: 100%; + left: 0; + pointer-events: none; + } + + .tree-view + [data-part="branch-content"][dir="rtl"] + [data-part="branch-indent-guide"] { + border-right: 2px solid var(--color-ui--border); + right: 0; + } +} diff --git a/design/components/typo.css b/design/components/typo.css new file mode 100644 index 0000000..2729c4b --- /dev/null +++ b/design/components/typo.css @@ -0,0 +1,301 @@ +@import "../main.css"; +@import "./scrollbar.css"; + +@layer components { + .typo { + font-family: var(--font-ui); + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + font-weight: var(--font-weight-ui); + color: var(--color-root--text); + text-wrap: wrap; + + &:focus-visible { + outline: none; + } + } + + .typo h1 { + font-size: var(--text-ui-xl); + line-height: var(--text-ui-xl--line-height); + font-weight: var(--font-weight-ui-xl); + color: var(--color-ui--text); + margin-block: var(--spacing-ui-padding-xl); + + &:focus-visible { + outline: none; + } + } + + .typo h2 { + font-size: var(--text-ui-lg); + line-height: var(--text-ui-lg--line-height); + font-weight: var(--font-weight-ui-lg); + color: var(--color-ui--text); + margin-block: var(--spacing-ui-padding-lg); + + &:focus-visible { + outline: none; + } + } + + .typo h3 { + font-size: var(--text-ui-lg); + line-height: var(--text-ui-lg--line-height); + font-weight: var(--font-weight-ui-lg); + color: var(--color-ui--text); + margin-block: var(--spacing-ui-padding); + + &:focus-visible { + outline: none; + } + } + + .typo h4 { + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + font-weight: var(--font-weight-ui); + color: var(--color-ui--text); + margin-block: var(--spacing-ui-padding-sm); + } + + .typo p { + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + font-weight: var(--font-weight-ui); + color: var(--color-ui--text); + margin-block: var(--spacing-ui-padding-sm); + } + + .typo small { + font-size: var(--text-ui-sm); + line-height: var(--text-ui-sm--line-height); + font-weight: var(--font-weight-ui-sm); + margin-block: var(--spacing-ui-padding-sm); + } + + .typo mark { + background: var(--color-ui-success); + color: var(--color-ui-success--text); + padding: 0.1em 0.25em; + border-radius: 0.25em; + } + + .typo abbr[title] { + border-bottom: 1px dotted var(--color-ui--border); + cursor: help; + text-decoration: none; + } + + .typo sup, + .typo sub { + font-size: 0.75em; + line-height: 1; + position: relative; + vertical-align: baseline; + } + + .typo sup { + top: -0.5em; + } + + .typo sub { + bottom: -0.25em; + } + + .typo kbd { + font-family: var(--font-mono); + background: var(--color-layer); + color: var(--color-layer--text); + border-radius: var(--radius-ui); + border: 1px solid var(--color-layer--border); + padding: 0.25em; + margin-inline: 0.25em; + } + + .typo del { + text-decoration: line-through; + color: var(--color-ui--alert); + } + + .typo ins { + text-decoration: underline; + color: var(--color-ui--success); + } + + .typo b, + .typo strong { + font-weight: var(--font-weight-ui-lg); + } + + .typo i, + .typo em { + font-style: italic; + } + + .typo blockquote { + background: var(--color-layer); + color: var(--color-layer--text); + box-shadow: var(--shadow-layer); + font-style: italic; + border-end-end-radius: var(--radius-ui); + border-start-end-radius: var(--radius-ui); + border: 1px solid var(--color-layer--border); + border-inline-start: 4px solid var(--color-layer--info); + margin-block: var(--spacing-ui); + padding: var(--spacing-ui-padding); + gap: var(--spacing-ui-gap); + width: fit-content; + + & p { + margin-block: 0; + } + } + + .typo hr { + border-top: 1px solid var(--color-ui--border); + margin-block: 5em; + margin-inline: auto; + width: 80%; + } + + .typo dt { + font-weight: var(--font-weight-ui-lg); + font-style: italic; + } + + .typo figcaption { + font-size: var(--text-ui-sm); + line-height: var(--text-ui-sm--line-height); + color: var(--color-ui--muted); + margin-top: 1em; + } + + .table-scroll { + margin-block: 2em; + width: 100%; + min-width: 0; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + .table-scroll table { + width: 100%; + min-width: max-content; + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + border-collapse: collapse; + table-layout: auto; + } + + .table-scroll table th, + .table-scroll table td { + min-width: min-content; + text-align: left; + padding: 1em; + border-bottom: 1px solid var(--color-ui--border); + } + + .table-scroll table th:not(:last-child), + .table-scroll table td:not(:last-child) { + padding-inline-end: 1.75em; + } + + .table-scroll table a { + white-space: nowrap; + } + + .table-scroll [data-clickable="row"] { + cursor: pointer; + } + + .table-scroll [data-cell="action"] { + width: 1%; + min-width: max-content; + white-space: nowrap; + text-align: right; + } + + .table-scroll [data-cell="action"] > div { + display: inline-flex; + gap: var(--spacing-ui-gap); + flex-shrink: 0; + justify-content: flex-end; + margin-inline-start: auto; + } + + .table-scroll [data-cell="action"] > div > * { + flex-shrink: 0; + min-width: min-content; + } + + .typo table { + display: block; + width: 100%; + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + margin-block: 2em; + border-collapse: collapse; + } + + .typo table th, + .typo table td { + text-align: left; + padding: 1em; + border-bottom: 1px solid var(--color-ui--border); + white-space: wrap; + } + + .typo table a { + white-space: wrap; + } + + .typo .list { + width: 100%; + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + margin-block: 2em; + border-collapse: collapse; + border: 1px solid var(--color-ui--border); + border-radius: var(--radius-ui); + overflow: hidden; + list-style: none; + padding: 0; + } + + .typo .list li { + text-align: left; + padding: var(--spacing-ui-padding); + border-bottom: 1px solid var(--color-ui--border); + white-space: normal; + color: var(--color-ui--text); + } + + .typo .list li:last-child { + border-bottom: none; + } + + .typo .list li:hover { + background: var(--color-layer); + } + + .typo form { + width: var(--container-mini); + display: flex; + flex-direction: column; + gap: var(--spacing-ui-gap-lg); + } + + .typo pre { + @apply scrollbar scrollbar--sm; + + font-family: var(--font-code); + font-size: var(--text-ui-sm); + line-height: var(--text-ui-sm--line-height); + padding: var(--spacing-ui-padding-sm); + background-color: var(--color-ui); + color: var(--color-ui--text); + border: 1px solid var(--color-ui--border); + overflow: auto; + } +} diff --git a/design/design/build.mjs b/design/design/build.mjs new file mode 100644 index 0000000..cc6a573 --- /dev/null +++ b/design/design/build.mjs @@ -0,0 +1,155 @@ +import StyleDictionary from 'style-dictionary'; +import { register } from '@tokens-studio/sd-transforms'; + +import path from 'node:path'; +import fs from 'node:fs'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const tokensBasePath = path.resolve(__dirname, "./"); +const buildPath = 'tokens/'; + +function cleanBuildPath() { + if (fs.existsSync(buildPath)) { + fs.rmSync(buildPath, { recursive: true, force: true }); + } + fs.mkdirSync(buildPath, { recursive: true }); +} + +function getTokenSets() { + const metadataPath = path.resolve(tokensBasePath, 'tokens/$metadata.json'); + return JSON.parse(fs.readFileSync(metadataPath, 'utf-8')).tokenSetOrder; +} + +function registerTransforms() { + register(StyleDictionary, { + excludeParentKeys: false, + platform: "css", + 'ts/color/modifiers': { format: 'hex' } + }); + + StyleDictionary.registerTransform({ + type: 'name', + transitive: true, + name: 'name/kebab-no-default', + transform: (token, config) => { + const transformedName = token.path + .map(part => part.replace(/([a-z0-9])([A-Z])/g, '$1-$2') + .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2').toLowerCase()) + .join('-') + .replace(/-default$/, ''); + return config.prefix ? `${config.prefix}--${transformedName}` : transformedName; + } + }); +} + +async function semantic() { + const sets = getTokenSets().filter(set => set.startsWith("semantic/")); + const promises = sets.map(set => { + const sd = new StyleDictionary({ + source: [ + `${tokensBasePath}/tokens/semantic/*.json`, + `${tokensBasePath}/tokens/themes/neo/light.json` + ], + preprocessors: ['tokens-studio'], + platforms: { + css: { + transforms: [ + 'attribute/cti', + 'name/kebab-no-default', + 'fontFamily/css', + 'shadow/css/shorthand', + 'cubicBezier/css', + 'ts/descriptionToComment', + 'ts/resolveMath', + 'ts/opacity', + 'ts/size/lineheight', + 'ts/typography/fontWeight', + 'ts/color/modifiers', + 'ts/size/css/letterspacing', + 'ts/shadow/innerShadow' + ], + buildPath, + files: [{ + destination: `${set}.css`, + format: 'css/variables', + options: { + selector: `@theme inline`, + outputReferences: true + }, + filter: token => token.filePath.endsWith(`${set}.json`) + }] + } + }, + log: { verbosity: 'default' } + }); + return sd.buildPlatform('css'); + }); + return Promise.all(promises); +} + +async function theme() { + const themes = ["neo", "uno", "duo", "leo"]; + const modes = ["light", "dark"]; + + const promises = themes.flatMap(theme => + modes.map(mode => { + const sd = new StyleDictionary({ + source: [`${tokensBasePath}/tokens/themes/${theme}/${mode}.json`], + preprocessors: ['tokens-studio'], + platforms: { + css: { + transforms: [ + 'attribute/cti', + 'name/kebab-no-default', + 'fontFamily/css', + 'shadow/css/shorthand', + 'cubicBezier/css', + 'ts/descriptionToComment', + 'ts/resolveMath', + 'ts/opacity', + 'ts/size/lineheight', + 'ts/typography/fontWeight', + 'ts/color/modifiers', + 'ts/size/css/letterspacing', + 'ts/shadow/innerShadow' + ], + buildPath, + files: [{ + destination: `themes/${theme}/${mode}.css`, + format: 'css/variables', + options: { + selector: `[data-theme="${theme}"][data-mode="${mode}"]`, + outputReferences: true + }, + filter: token => token.filePath.endsWith(`themes/${theme}/${mode}.json`) + }] + } + }, + log: { verbosity: 'default' } + }); + return sd.buildPlatform('css'); + }) + ); + + return Promise.all(promises); +} + +async function build() { + try { + cleanBuildPath(); + registerTransforms(); + + const promises = [ + semantic(), + theme(), + ]; + + await Promise.all(promises); + } catch (error) { + console.error('\n❌ Build failed:', error); + process.exit(1); + } +} + +build(); \ No newline at end of file diff --git a/design/design/tokens/$metadata.json b/design/design/tokens/$metadata.json new file mode 100644 index 0000000..4f9af2e --- /dev/null +++ b/design/design/tokens/$metadata.json @@ -0,0 +1,18 @@ +{ + "tokenSetOrder": [ + "semantic/color", + "semantic/dimension", + "semantic/border", + "semantic/text", + "semantic/font", + "semantic/effect", + "themes/neo/light", + "themes/neo/dark", + "themes/uno/light", + "themes/uno/dark", + "themes/duo/light", + "themes/duo/dark", + "themes/leo/light", + "themes/leo/dark" + ] +} \ No newline at end of file diff --git a/design/design/tokens/semantic/border.json b/design/design/tokens/semantic/border.json new file mode 100644 index 0000000..4d46e0c --- /dev/null +++ b/design/design/tokens/semantic/border.json @@ -0,0 +1,12 @@ +{ + "radius": { + "ui": { + "value": "0.375rem", + "type": "borderRadius" + }, + "full": { + "value": "9999px", + "type": "borderRadius" + } + } +} \ No newline at end of file diff --git a/design/design/tokens/semantic/color.json b/design/design/tokens/semantic/color.json new file mode 100644 index 0000000..83dfe25 --- /dev/null +++ b/design/design/tokens/semantic/color.json @@ -0,0 +1,304 @@ +{ + "color": { + "root": { + "default": { + "value": "{theme.color.root.default}", + "type": "color" + }, + "-border": { + "default": { + "value": "{theme.color.root.-border.default}", + "type": "color" + } + }, + "-text": { + "default": { + "value": "{theme.color.root.-text.default}", + "type": "color" + } + }, + "-muted": { + "default": { + "value": "{theme.color.root.-muted.default}", + "type": "color" + } + }, + "-accent": { + "default": { + "value": "{theme.color.root.-accent.default}", + "type": "color" + } + }, + "-brand": { + "default": { + "value": "{theme.color.root.-brand.default}", + "type": "color" + } + }, + "-alert": { + "default": { + "value": "{theme.color.root.-alert.default}", + "type": "color" + } + }, + "-info": { + "default": { + "value": "{theme.color.root.-info.default}", + "type": "color" + } + }, + "-success": { + "default": { + "value": "{theme.color.root.-success.default}", + "type": "color" + } + } + }, + "layer": { + "default": { + "value": "{theme.color.layer.default}", + "type": "color" + }, + "-border": { + "default": { + "value": "{theme.color.layer.-border.default}", + "type": "color" + } + }, + "-text": { + "default": { + "value": "{theme.color.layer.-text.default}", + "type": "color" + } + }, + "-muted": { + "default": { + "value": "{theme.color.layer.-muted.default}", + "type": "color" + } + }, + "-accent": { + "default": { + "value": "{theme.color.layer.-accent.default}", + "type": "color" + } + }, + "-brand": { + "default": { + "value": "{theme.color.layer.-brand.default}", + "type": "color" + } + }, + "-alert": { + "default": { + "value": "{theme.color.layer.-alert.default}", + "type": "color" + } + }, + "-info": { + "default": { + "value": "{theme.color.layer.-info.default}", + "type": "color" + } + }, + "-success": { + "default": { + "value": "{theme.color.layer.-success.default}", + "type": "color" + } + } + }, + "ui": { + "default": { + "value": "{theme.color.ui.default}", + "type": "color" + }, + "hover": { + "value": "{theme.color.ui.hover}", + "type": "color" + }, + "active": { + "value": "{theme.color.ui.active}", + "type": "color" + }, + "-border": { + "default": { + "value": "{theme.color.ui.-border.default}", + "type": "color" + } + }, + "-text": { + "default": { + "value": "{theme.color.ui.-text.default}", + "type": "color" + } + }, + "-muted": { + "default": { + "value": "{theme.color.ui.-muted.default}", + "type": "color" + } + } + }, + "ui-accent": { + "default": { + "value": "{theme.color.ui-accent.default}", + "type": "color" + }, + "hover": { + "value": "{theme.color.ui-accent.hover}", + "type": "color" + }, + "active": { + "value": "{theme.color.ui-accent.active}", + "type": "color" + }, + "-border": { + "default": { + "value": "{theme.color.ui-accent.-border.default}", + "type": "color" + } + }, + "-text": { + "default": { + "value": "{theme.color.ui-accent.-text.default}", + "type": "color" + } + }, + "-muted": { + "default": { + "value": "{theme.color.ui-accent.-muted.default}", + "type": "color" + } + } + }, + "ui-brand": { + "default": { + "value": "{theme.color.ui-brand.default}", + "type": "color" + }, + "hover": { + "value": "{theme.color.ui-brand.hover}", + "type": "color" + }, + "active": { + "value": "{theme.color.ui-brand.active}", + "type": "color" + }, + "-border": { + "default": { + "value": "{theme.color.ui-brand.-border.default}", + "type": "color" + } + }, + "-text": { + "default": { + "value": "{theme.color.ui-brand.-text.default}", + "type": "color" + } + }, + "-muted": { + "default": { + "value": "{theme.color.ui-brand.-muted.default}", + "type": "color" + } + } + }, + "ui-alert": { + "default": { + "value": "{theme.color.ui-alert.default}", + "type": "color" + }, + "hover": { + "value": "{theme.color.ui-alert.hover}", + "type": "color" + }, + "active": { + "value": "{theme.color.ui-alert.active}", + "type": "color" + }, + "-border": { + "default": { + "value": "{theme.color.ui-alert.-border.default}", + "type": "color" + } + }, + "-text": { + "default": { + "value": "{theme.color.ui-alert.-text.default}", + "type": "color" + } + }, + "-muted": { + "default": { + "value": "{theme.color.ui-alert.-muted.default}", + "type": "color" + } + } + }, + "ui-info": { + "default": { + "value": "{theme.color.ui-info.default}", + "type": "color" + }, + "hover": { + "value": "{theme.color.ui-info.hover}", + "type": "color" + }, + "active": { + "value": "{theme.color.ui-info.active}", + "type": "color" + }, + "-border": { + "default": { + "value": "{theme.color.ui-info.-border.default}", + "type": "color" + } + }, + "-text": { + "default": { + "value": "{theme.color.ui-info.-text.default}", + "type": "color" + } + }, + "-muted": { + "default": { + "value": "{theme.color.ui-info.-muted.default}", + "type": "color" + } + } + }, + "ui-success": { + "default": { + "value": "{theme.color.ui-success.default}", + "type": "color" + }, + "hover": { + "value": "{theme.color.ui-success.hover}", + "type": "color" + }, + "active": { + "value": "{theme.color.ui-success.active}", + "type": "color" + }, + "-border": { + "default": { + "value": "{theme.color.ui-success.-border.default}", + "type": "color" + } + }, + "-text": { + "default": { + "value": "{theme.color.ui-success.-text.default}", + "type": "color" + } + }, + "-muted": { + "default": { + "value": "{theme.color.ui-success.-muted.default}", + "type": "color" + } + } + } + } +} \ No newline at end of file diff --git a/design/design/tokens/semantic/dimension.json b/design/design/tokens/semantic/dimension.json new file mode 100644 index 0000000..ff101a8 --- /dev/null +++ b/design/design/tokens/semantic/dimension.json @@ -0,0 +1,496 @@ +{ + "spacing": { + "default": { + "value": "0.25rem", + "type": "dimension" + }, + "ui": { + "default": { + "value": "{spacing.ui.md}", + "type": "dimension" + }, + "sm": { + "value": "calc({spacing.default} * 8)", + "type": "dimension" + }, + "md": { + "value": "calc({spacing.default} * 10)", + "type": "dimension" + }, + "lg": { + "value": "calc({spacing.default} * 12)", + "type": "dimension" + }, + "xl": { + "value": "calc({spacing.default} * 14)", + "type": "dimension" + }, + "padding": { + "default": { + "value": "{spacing.ui.padding.md}", + "type": "dimension" + }, + "sm": { + "value": "calc({spacing.default} * 2)", + "type": "dimension" + }, + "md": { + "value": "calc({spacing.default} * 3)", + "type": "dimension" + }, + "lg": { + "value": "calc({spacing.default} * 4)", + "type": "dimension" + }, + "xl": { + "value": "calc({spacing.default} * 5)", + "type": "dimension" + } + }, + "gap": { + "default": { + "value": "{spacing.ui.gap.md}", + "type": "dimension" + }, + "sm": { + "value": "calc({spacing.default} * 2)", + "type": "dimension" + }, + "md": { + "value": "calc({spacing.default} * 4)", + "type": "dimension" + }, + "lg": { + "value": "calc({spacing.default} * 6)", + "type": "dimension" + }, + "xl": { + "value": "calc({spacing.default} * 8)", + "type": "dimension" + } + } + }, + "mini": { + "default": { + "value": "{spacing.mini.md}", + "type": "dimension" + }, + "sm": { + "value": "calc({spacing.default} * 5)", + "type": "dimension" + }, + "md": { + "value": "calc({spacing.default} * 6)", + "type": "dimension" + }, + "lg": { + "value": "calc({spacing.default} * 7)", + "type": "dimension" + }, + "xl": { + "value": "calc({spacing.default} * 8)", + "type": "dimension" + }, + "padding": { + "default": { + "value": "{spacing.mini.padding.md}", + "type": "dimension" + }, + "sm": { + "value": "calc({spacing.default} * 1)", + "type": "dimension" + }, + "md": { + "value": "calc({spacing.default} * 2)", + "type": "dimension" + }, + "lg": { + "value": "calc({spacing.default} * 3)", + "type": "dimension" + }, + "xl": { + "value": "calc({spacing.default} * 4)", + "type": "dimension" + } + }, + "gap": { + "default": { + "value": "{spacing.mini.gap.md}", + "type": "dimension" + }, + "sm": { + "value": "calc({spacing.default} * 1)", + "type": "dimension" + }, + "md": { + "value": "calc({spacing.default} * 2)", + "type": "dimension" + }, + "lg": { + "value": "calc({spacing.default} * 3)", + "type": "dimension" + }, + "xl": { + "value": "calc({spacing.default} * 4)", + "type": "dimension" + } + } + }, + "micro": { + "default": { + "value": "{spacing.micro.md}", + "type": "dimension" + }, + "sm": { + "value": "calc({spacing.default} * 1)", + "type": "dimension" + }, + "md": { + "value": "calc({spacing.default} * 2)", + "type": "dimension" + }, + "lg": { + "value": "calc({spacing.default} * 3)", + "type": "dimension" + }, + "xl": { + "value": "calc({spacing.default} * 4)", + "type": "dimension" + }, + "padding": { + "default": { + "value": "{spacing.micro.padding.md}", + "type": "dimension" + }, + "sm": { + "value": "calc({spacing.default} * 1)", + "type": "dimension" + }, + "md": { + "value": "calc({spacing.default} * 1)", + "type": "dimension" + }, + "lg": { + "value": "calc({spacing.default} * 2)", + "type": "dimension" + }, + "xl": { + "value": "calc({spacing.default} * 2)", + "type": "dimension" + } + }, + "gap": { + "default": { + "value": "{spacing.micro.gap.md}", + "type": "dimension" + }, + "sm": { + "value": "calc({spacing.default} * 1)", + "type": "dimension" + }, + "md": { + "value": "calc({spacing.default} * 1)", + "type": "dimension" + }, + "lg": { + "value": "calc({spacing.default} * 2)", + "type": "dimension" + }, + "xl": { + "value": "calc({spacing.default} * 2)", + "type": "dimension" + } + } + } + }, + "container": { + "ui": { + "default": { + "value": "{container.ui.md}", + "type": "dimension" + }, + "sm": { + "value": "20rem", + "type": "dimension" + }, + "md": { + "value": "24rem", + "type": "dimension" + }, + "lg": { + "value": "40rem", + "type": "dimension" + }, + "xl": { + "value": "60rem", + "type": "dimension" + } + }, + "mini": { + "default": { + "value": "{container.mini.md}", + "type": "dimension" + }, + "sm": { + "value": "12rem", + "type": "dimension" + }, + "md": { + "value": "16rem", + "type": "dimension" + }, + "lg": { + "value": "20rem", + "type": "dimension" + }, + "xl": { + "value": "24rem", + "type": "dimension" + } + }, + "micro": { + "default": { + "value": "{container.micro.md}", + "type": "dimension" + }, + "sm": { + "value": "6rem", + "type": "dimension" + }, + "md": { + "value": "8rem", + "type": "dimension" + }, + "lg": { + "value": "12rem", + "type": "dimension" + }, + "xl": { + "value": "16rem", + "type": "dimension" + } + } + }, + "width": { + "ui": { + "default": { + "value": "{container.ui.default}", + "type": "dimension" + }, + "sm": { + "value": "{container.ui.sm}", + "type": "dimension" + }, + "md": { + "value": "{container.ui.md}", + "type": "dimension" + }, + "lg": { + "value": "{container.ui.lg}", + "type": "dimension" + }, + "xl": { + "value": "{container.ui.xl}", + "type": "dimension" + } + }, + "mini": { + "default": { + "value": "{container.mini.default}", + "type": "dimension" + }, + "sm": { + "value": "{container.mini.sm}", + "type": "dimension" + }, + "md": { + "value": "{container.mini.md}", + "type": "dimension" + }, + "lg": { + "value": "{container.mini.lg}", + "type": "dimension" + }, + "xl": { + "value": "{container.mini.xl}", + "type": "dimension" + } + }, + "micro": { + "default": { + "value": "{container.micro.default}", + "type": "dimension" + }, + "sm": { + "value": "{container.micro.sm}", + "type": "dimension" + }, + "md": { + "value": "{container.micro.md}", + "type": "dimension" + }, + "lg": { + "value": "{container.micro.lg}", + "type": "dimension" + }, + "xl": { + "value": "{container.micro.xl}", + "type": "dimension" + } + } + }, + "min-width": { + "ui": { + "default": { + "value": "{container.ui.default}", + "type": "dimension" + }, + "sm": { + "value": "{container.ui.sm}", + "type": "dimension" + }, + "md": { + "value": "{container.ui.md}", + "type": "dimension" + }, + "lg": { + "value": "{container.ui.lg}", + "type": "dimension" + }, + "xl": { + "value": "{container.ui.xl}", + "type": "dimension" + } + }, + "mini": { + "default": { + "value": "{container.mini.default}", + "type": "dimension" + }, + "sm": { + "value": "{container.mini.sm}", + "type": "dimension" + }, + "md": { + "value": "{container.mini.md}", + "type": "dimension" + }, + "lg": { + "value": "{container.mini.lg}", + "type": "dimension" + }, + "xl": { + "value": "{container.mini.xl}", + "type": "dimension" + } + }, + "micro": { + "default": { + "value": "{container.micro.default}", + "type": "dimension" + }, + "sm": { + "value": "{container.micro.sm}", + "type": "dimension" + }, + "md": { + "value": "{container.micro.md}", + "type": "dimension" + }, + "lg": { + "value": "{container.micro.lg}", + "type": "dimension" + }, + "xl": { + "value": "{container.micro.xl}", + "type": "dimension" + } + } + }, + "max-width": { + "ui": { + "default": { + "value": "{container.ui.default}", + "type": "dimension" + }, + "sm": { + "value": "{container.ui.sm}", + "type": "dimension" + }, + "md": { + "value": "{container.ui.md}", + "type": "dimension" + }, + "lg": { + "value": "{container.ui.lg}", + "type": "dimension" + }, + "xl": { + "value": "{container.ui.xl}", + "type": "dimension" + } + }, + "mini": { + "default": { + "value": "{container.mini.default}", + "type": "dimension" + }, + "sm": { + "value": "{container.mini.sm}", + "type": "dimension" + }, + "md": { + "value": "{container.mini.md}", + "type": "dimension" + }, + "lg": { + "value": "{container.mini.lg}", + "type": "dimension" + }, + "xl": { + "value": "{container.mini.xl}", + "type": "dimension" + } + }, + "micro": { + "default": { + "value": "{container.micro.default}", + "type": "dimension" + }, + "sm": { + "value": "{container.micro.sm}", + "type": "dimension" + }, + "md": { + "value": "{container.micro.md}", + "type": "dimension" + }, + "lg": { + "value": "{container.micro.lg}", + "type": "dimension" + }, + "xl": { + "value": "{container.micro.xl}", + "type": "dimension" + } + } + }, + "breakpoint": { + "sm": { + "value": "20rem", + "type": "dimension" + }, + "md": { + "value": "48rem", + "type": "dimension" + }, + "lg": { + "value": "64rem", + "type": "dimension" + }, + "xl": { + "value": "106rem", + "type": "dimension" + } + } +} \ No newline at end of file diff --git a/design/design/tokens/semantic/effect.json b/design/design/tokens/semantic/effect.json new file mode 100644 index 0000000..1a28455 --- /dev/null +++ b/design/design/tokens/semantic/effect.json @@ -0,0 +1,84 @@ +{ + "shadow": { + "layer": { + "default": { + "type": "shadow", + "value": [ + { + "color": "{color.layer.-border.default}", + "offsetX": "0", + "offsetY": "1px", + "blur": "1px", + "spread": "0" + }, + { + "color": "{color.layer.-border.default}", + "offsetX": "0", + "offsetY": "1px", + "blur": "1px", + "spread": "0" + } + ] + } + }, + "ui": { + "default": { + "type": "shadow", + "value": [ + { + "color": "{color.ui.-border.default}", + "offsetX": "0", + "offsetY": "1px", + "blur": "2px", + "spread": "0" + }, + { + "color": "{color.ui.-border.default}", + "offsetX": "0", + "offsetY": "2px", + "blur": "4px", + "spread": "0" + } + ] + }, + "hover": { + "type": "shadow", + "value": [ + { + "color": "{color.ui.-border.default}", + "offsetX": "0", + "offsetY": "3px", + "blur": "4px", + "spread": "0" + }, + { + "color": "{color.ui.-border.default}", + "offsetX": "0", + "offsetY": "2px", + "blur": "4px", + "spread": "0" + } + ] + }, + "active": { + "type": "shadow", + "value": [ + { + "color": "{color.ui.-border.default}", + "offsetX": "0", + "offsetY": "1px", + "blur": "2px", + "spread": "0" + }, + { + "color": "{color.ui.-border.default}", + "offsetX": "0", + "offsetY": "1px", + "blur": "1px", + "spread": "0" + } + ] + } + } + } +} \ No newline at end of file diff --git a/design/design/tokens/semantic/font.json b/design/design/tokens/semantic/font.json new file mode 100644 index 0000000..242aa58 --- /dev/null +++ b/design/design/tokens/semantic/font.json @@ -0,0 +1,44 @@ +{ + "font": { + "ui": { + "default": { + "value": [ + "system-ui", + "sans-serif" + ], + "type": "fontFamily" + } + }, + "code": { + "value": [ + "ui-monospace", + "monospace" + ], + "type": "fontFamily" + } + }, + "font-weight": { + "ui": { + "sm": { + "value": 200, + "type": "fontWeights" + }, + "default": { + "value": "{font-weight.ui.md}", + "type": "fontWeights" + }, + "md": { + "value": 400, + "type": "fontWeights" + }, + "lg": { + "value": 600, + "type": "fontWeights" + }, + "xl": { + "value": 800, + "type": "fontWeights" + } + } + } +} \ No newline at end of file diff --git a/design/design/tokens/semantic/text.json b/design/design/tokens/semantic/text.json new file mode 100644 index 0000000..946842f --- /dev/null +++ b/design/design/tokens/semantic/text.json @@ -0,0 +1,46 @@ +{ + "text": { + "ui": { + "default": { + "value": "{text.ui.md}", + "type": "fontSizes" + }, + "-line-height": { + "value": "{text.ui.md--line-height}", + "type": "dimension" + }, + "sm": { + "value": "1rem", + "type": "fontSizes" + }, + "sm--line-height": { + "value": "1.7", + "type": "dimension" + }, + "md": { + "value": "1.125rem", + "type": "fontSizes" + }, + "md--line-height": { + "value": "1.8", + "type": "dimension" + }, + "lg": { + "value": "1.25rem", + "type": "fontSizes" + }, + "lg--line-height": { + "value": "1.9", + "type": "dimension" + }, + "xl": { + "value": "1.8rem", + "type": "fontSizes" + }, + "xl--line-height": { + "value": "2", + "type": "dimension" + } + } + } +} \ No newline at end of file diff --git a/design/design/tokens/themes/duo/dark.json b/design/design/tokens/themes/duo/dark.json new file mode 100644 index 0000000..a0b1699 --- /dev/null +++ b/design/design/tokens/themes/duo/dark.json @@ -0,0 +1,360 @@ +{ + "theme": { + "color": { + "root": { + "-border": { + "default": { + "value": "#382f24", + "type": "color", + "description": "1.4:1 contrast against root-default (#171411)" + } + }, + "-text": { + "default": { + "value": "#b9b5b1", + "type": "color", + "description": "9:1 contrast against root-default (#171411)" + } + }, + "-muted": { + "default": { + "value": "#9d918a", + "type": "color", + "description": "6:1 contrast against root-default (#171411)" + } + }, + "-accent": { + "default": { + "value": "#ae9d8f", + "type": "color", + "description": "7:1 contrast against root-default (#171411)" + } + }, + "-brand": { + "default": { + "value": "#bf9881", + "type": "color", + "description": "7:1 contrast against root-default (#171411)" + } + }, + "-alert": { + "default": { + "value": "#e68484", + "type": "color", + "description": "7:1 contrast against root-default (#171411)" + } + }, + "-info": { + "default": { + "value": "#7ca3d1", + "type": "color", + "description": "7:1 contrast against root-default (#171411)" + } + }, + "-success": { + "default": { + "value": "#65ae8e", + "type": "color", + "description": "7:1 contrast against root-default (#171411)" + } + }, + "default": { + "value": "#171411", + "type": "color", + "description": "1:1 contrast against root-default (#171411)" + } + }, + "layer": { + "-border": { + "default": { + "value": "#3e3328", + "type": "color", + "description": "1.4:1 contrast against layer-default (#1f1a16)" + } + }, + "-text": { + "default": { + "value": "#bebbb6", + "type": "color", + "description": "9:1 contrast against layer-default (#1f1a16)" + } + }, + "-muted": { + "default": { + "value": "#a2968f", + "type": "color", + "description": "6:1 contrast against layer-default (#1f1a16)" + } + }, + "-accent": { + "default": { + "value": "#b3a293", + "type": "color", + "description": "7:1 contrast against layer-default (#1f1a16)" + } + }, + "-brand": { + "default": { + "value": "#c39d88", + "type": "color", + "description": "7:1 contrast against layer-default (#1f1a16)" + } + }, + "-alert": { + "default": { + "value": "#e88b8b", + "type": "color", + "description": "7:1 contrast against layer-default (#1f1a16)" + } + }, + "-info": { + "default": { + "value": "#82a8d3", + "type": "color", + "description": "7:1 contrast against layer-default (#1f1a16)" + } + }, + "-success": { + "default": { + "value": "#68b392", + "type": "color", + "description": "7:1 contrast against layer-default (#1f1a16)" + } + }, + "default": { + "value": "#1f1a16", + "type": "color", + "description": "1:1 contrast against layer-default (#1f1a16)" + } + }, + "ui": { + "-border": { + "default": { + "value": "#41382d", + "type": "color", + "description": "1.4:1 contrast against ui-active (#26211b)" + } + }, + "-text": { + "default": { + "value": "#c4c1bc", + "type": "color", + "description": "9:1 contrast against ui-active (#26211b)" + } + }, + "-muted": { + "default": { + "value": "#a69c94", + "type": "color", + "description": "6:1 contrast against ui-active (#26211b)" + } + }, + "default": { + "value": "#15120f", + "type": "color", + "description": "-1.15:1 contrast against ui-active (#26211b)" + }, + "hover": { + "value": "#1d1915", + "type": "color", + "description": "-1.08:1 contrast against ui-active (#26211b)" + }, + "active": { + "value": "#26211b", + "type": "color", + "description": "1:1 contrast against ui-active (#26211b)" + } + }, + "ui-accent": { + "-border": { + "default": { + "value": "#a19387", + "type": "color", + "description": "1.1:1 contrast against ui-accent-active (#aa998c)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-accent-active (#aa998c)" + } + }, + "-muted": { + "default": { + "value": "#261e17", + "type": "color", + "description": "6:1 contrast against ui-accent-active (#aa998c)" + } + }, + "default": { + "value": "#b8a697", + "type": "color", + "description": "-1.15:1 contrast against ui-accent-active (#aa998c)" + }, + "hover": { + "value": "#b1a091", + "type": "color", + "description": "-1.08:1 contrast against ui-accent-active (#aa998c)" + }, + "active": { + "value": "#aa998c", + "type": "color", + "description": "1:1 contrast against ui-accent-active (#aa998c)" + } + }, + "ui-brand": { + "-border": { + "default": { + "value": "#a5816c", + "type": "color", + "description": "1.1:1 contrast against ui-brand-active (#b0866e)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-brand-active (#b0866e)" + } + }, + "-muted": { + "default": { + "value": "#120e0b", + "type": "color", + "description": "6:1 contrast against ui-brand-active (#b0866e)" + } + }, + "default": { + "value": "#bc927a", + "type": "color", + "description": "-1.15:1 contrast against ui-brand-active (#b0866e)" + }, + "hover": { + "value": "#b98d75", + "type": "color", + "description": "-1.08:1 contrast against ui-brand-active (#b0866e)" + }, + "active": { + "value": "#b0866e", + "type": "color", + "description": "1:1 contrast against ui-brand-active (#b0866e)" + } + }, + "ui-alert": { + "-border": { + "default": { + "value": "#ce6565", + "type": "color", + "description": "1.1:1 contrast against ui-alert-active (#df6666)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-alert-active (#df6666)" + } + }, + "-muted": { + "default": { + "value": "#100404", + "type": "color", + "description": "6:1 contrast against ui-alert-active (#df6666)" + } + }, + "default": { + "value": "#e47878", + "type": "color", + "description": "-1.15:1 contrast against ui-alert-active (#df6666)" + }, + "hover": { + "value": "#e26f6f", + "type": "color", + "description": "-1.08:1 contrast against ui-alert-active (#df6666)" + }, + "active": { + "value": "#df6666", + "type": "color", + "description": "1:1 contrast against ui-alert-active (#df6666)" + } + }, + "ui-info": { + "-border": { + "default": { + "value": "#6885a8", + "type": "color", + "description": "1.1:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "-muted": { + "default": { + "value": "#010206", + "type": "color", + "description": "6:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "default": { + "value": "#7399c4", + "type": "color", + "description": "-1.15:1 contrast against ui-info-active (#6a8cb3)" + }, + "hover": { + "value": "#6f93bc", + "type": "color", + "description": "-1.08:1 contrast against ui-info-active (#6a8cb3)" + }, + "active": { + "value": "#6a8cb3", + "type": "color", + "description": "1:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "ui-success": { + "-border": { + "default": { + "value": "#588e76", + "type": "color", + "description": "1.1:1 contrast against ui-success-active (#58977b)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-success-active (#58977b)" + } + }, + "-muted": { + "default": { + "value": "#000403", + "type": "color", + "description": "6:1 contrast against ui-success-active (#58977b)" + } + }, + "default": { + "value": "#5fa385", + "type": "color", + "description": "-1.15:1 contrast against ui-success-active (#58977b)" + }, + "hover": { + "value": "#5c9e81", + "type": "color", + "description": "-1.08:1 contrast against ui-success-active (#58977b)" + }, + "active": { + "value": "#58977b", + "type": "color", + "description": "1:1 contrast against ui-success-active (#58977b)" + } + } + } + } +} \ No newline at end of file diff --git a/design/design/tokens/themes/duo/light.json b/design/design/tokens/themes/duo/light.json new file mode 100644 index 0000000..625a8b6 --- /dev/null +++ b/design/design/tokens/themes/duo/light.json @@ -0,0 +1,360 @@ +{ + "theme": { + "color": { + "root": { + "-border": { + "default": { + "value": "#f3f2ef", + "type": "color", + "description": "1.05:1 contrast against root-default (#f8f6f3)" + } + }, + "-text": { + "default": { + "value": "#544f4b", + "type": "color", + "description": "7.5:1 contrast against root-default (#f8f6f3)" + } + }, + "-muted": { + "default": { + "value": "#766c61", + "type": "color", + "description": "4.8:1 contrast against root-default (#f8f6f3)" + } + }, + "-accent": { + "default": { + "value": "#5e5348", + "type": "color", + "description": "7:1 contrast against root-default (#f8f6f3)" + } + }, + "-brand": { + "default": { + "value": "#6f4d3c", + "type": "color", + "description": "7:1 contrast against root-default (#f8f6f3)" + } + }, + "-alert": { + "default": { + "value": "#8f3838", + "type": "color", + "description": "7:1 contrast against root-default (#f8f6f3)" + } + }, + "-info": { + "default": { + "value": "#3a5775", + "type": "color", + "description": "7:1 contrast against root-default (#f8f6f3)" + } + }, + "-success": { + "default": { + "value": "#2d5e47", + "type": "color", + "description": "7:1 contrast against root-default (#f8f6f3)" + } + }, + "default": { + "value": "#f8f6f3", + "type": "color", + "description": "1:1 contrast against root-default (#f8f6f3)" + } + }, + "layer": { + "-border": { + "default": { + "value": "#f7f5f3", + "type": "color", + "description": "1.05:1 contrast against layer-default (#fcfbfa)" + } + }, + "-text": { + "default": { + "value": "#57524e", + "type": "color", + "description": "7.5:1 contrast against layer-default (#fcfbfa)" + } + }, + "-muted": { + "default": { + "value": "#786f64", + "type": "color", + "description": "4.8:1 contrast against layer-default (#fcfbfa)" + } + }, + "-accent": { + "default": { + "value": "#61564a", + "type": "color", + "description": "7:1 contrast against layer-default (#fcfbfa)" + } + }, + "-brand": { + "default": { + "value": "#734f3f", + "type": "color", + "description": "7:1 contrast against layer-default (#fcfbfa)" + } + }, + "-alert": { + "default": { + "value": "#943a3a", + "type": "color", + "description": "7:1 contrast against layer-default (#fcfbfa)" + } + }, + "-info": { + "default": { + "value": "#3c5a78", + "type": "color", + "description": "7:1 contrast against layer-default (#fcfbfa)" + } + }, + "-success": { + "default": { + "value": "#2f614a", + "type": "color", + "description": "7:1 contrast against layer-default (#fcfbfa)" + } + }, + "default": { + "value": "#fcfbfa", + "type": "color", + "description": "1:1 contrast against layer-default (#fcfbfa)" + } + }, + "ui": { + "-border": { + "default": { + "value": "#ebe7e2", + "type": "color", + "description": "1.08:1 contrast against ui-active (#f3f0ec)" + } + }, + "-text": { + "default": { + "value": "#514c48", + "type": "color", + "description": "7.5:1 contrast against ui-active (#f3f0ec)" + } + }, + "-muted": { + "default": { + "value": "#72685e", + "type": "color", + "description": "4.8:1 contrast against ui-active (#f3f0ec)" + } + }, + "default": { + "value": "#fcfcfb", + "type": "color", + "description": "-1.1:1 contrast against ui-active (#f3f0ec)" + }, + "hover": { + "value": "#faf8f6", + "type": "color", + "description": "-1.05:1 contrast against ui-active (#f3f0ec)" + }, + "active": { + "value": "#f3f0ec", + "type": "color", + "description": "1:1 contrast against ui-active (#f3f0ec)" + } + }, + "ui-accent": { + "-border": { + "default": { + "value": "#837567", + "type": "color", + "description": "1.1:1 contrast against ui-accent-active (#7e6e60)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-accent-active (#7e6e60)" + } + }, + "-muted": { + "default": { + "value": "#fdfdfc", + "type": "color", + "description": "4.8:1 contrast against ui-accent-active (#7e6e60)" + } + }, + "default": { + "value": "#736558", + "type": "color", + "description": "-1.15:1 contrast against ui-accent-active (#7e6e60)" + }, + "hover": { + "value": "#77695b", + "type": "color", + "description": "-1.08:1 contrast against ui-accent-active (#7e6e60)" + }, + "active": { + "value": "#7e6e60", + "type": "color", + "description": "1:1 contrast against ui-accent-active (#7e6e60)" + } + }, + "ui-brand": { + "-border": { + "default": { + "value": "#996e59", + "type": "color", + "description": "1.1:1 contrast against ui-brand-active (#966751)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-brand-active (#966751)" + } + }, + "-muted": { + "default": { + "value": "#fffcfa", + "type": "color", + "description": "4.8:1 contrast against ui-brand-active (#966751)" + } + }, + "default": { + "value": "#875d49", + "type": "color", + "description": "-1.15:1 contrast against ui-brand-active (#966751)" + }, + "hover": { + "value": "#8d614c", + "type": "color", + "description": "-1.08:1 contrast against ui-brand-active (#966751)" + }, + "active": { + "value": "#966751", + "type": "color", + "description": "1:1 contrast against ui-brand-active (#966751)" + } + }, + "ui-alert": { + "-border": { + "default": { + "value": "#b15353", + "type": "color", + "description": "1.1:1 contrast against ui-alert-active (#b24646)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-alert-active (#b24646)" + } + }, + "-muted": { + "default": { + "value": "#feeded", + "type": "color", + "description": "4.8:1 contrast against ui-alert-active (#b24646)" + } + }, + "default": { + "value": "#a34040", + "type": "color", + "description": "-1.15:1 contrast against ui-alert-active (#b24646)" + }, + "hover": { + "value": "#a84242", + "type": "color", + "description": "-1.08:1 contrast against ui-alert-active (#b24646)" + }, + "active": { + "value": "#b24646", + "type": "color", + "description": "1:1 contrast against ui-alert-active (#b24646)" + } + }, + "ui-info": { + "-border": { + "default": { + "value": "#567ba0", + "type": "color", + "description": "1.1:1 contrast against ui-info-active (#4d759c)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-info-active (#4d759c)" + } + }, + "-muted": { + "default": { + "value": "#fafdff", + "type": "color", + "description": "4.8:1 contrast against ui-info-active (#4d759c)" + } + }, + "default": { + "value": "#466a8d", + "type": "color", + "description": "-1.15:1 contrast against ui-info-active (#4d759c)" + }, + "hover": { + "value": "#496f94", + "type": "color", + "description": "-1.08:1 contrast against ui-info-active (#4d759c)" + }, + "active": { + "value": "#4d759c", + "type": "color", + "description": "1:1 contrast against ui-info-active (#4d759c)" + } + }, + "ui-success": { + "-border": { + "default": { + "value": "#468467", + "type": "color", + "description": "1.1:1 contrast against ui-success-active (#3d7d5f)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-success-active (#3d7d5f)" + } + }, + "-muted": { + "default": { + "value": "#f9fefc", + "type": "color", + "description": "4.8:1 contrast against ui-success-active (#3d7d5f)" + } + }, + "default": { + "value": "#377257", + "type": "color", + "description": "-1.15:1 contrast against ui-success-active (#3d7d5f)" + }, + "hover": { + "value": "#3a775b", + "type": "color", + "description": "-1.08:1 contrast against ui-success-active (#3d7d5f)" + }, + "active": { + "value": "#3d7d5f", + "type": "color", + "description": "1:1 contrast against ui-success-active (#3d7d5f)" + } + } + } + } +} \ No newline at end of file diff --git a/design/design/tokens/themes/leo/dark.json b/design/design/tokens/themes/leo/dark.json new file mode 100644 index 0000000..5b58949 --- /dev/null +++ b/design/design/tokens/themes/leo/dark.json @@ -0,0 +1,360 @@ +{ + "theme": { + "color": { + "root": { + "-border": { + "default": { + "value": "#2b3236", + "type": "color", + "description": "1.4:1 contrast against root-default (#121517)" + } + }, + "-text": { + "default": { + "value": "#b3b6b8", + "type": "color", + "description": "9:1 contrast against root-default (#121517)" + } + }, + "-muted": { + "default": { + "value": "#88959c", + "type": "color", + "description": "6:1 contrast against root-default (#121517)" + } + }, + "-accent": { + "default": { + "value": "#94a2aa", + "type": "color", + "description": "7:1 contrast against root-default (#121517)" + } + }, + "-brand": { + "default": { + "value": "#7aa7b5", + "type": "color", + "description": "7:1 contrast against root-default (#121517)" + } + }, + "-alert": { + "default": { + "value": "#e68484", + "type": "color", + "description": "7:1 contrast against root-default (#121517)" + } + }, + "-info": { + "default": { + "value": "#7ca3d1", + "type": "color", + "description": "7:1 contrast against root-default (#121517)" + } + }, + "-success": { + "default": { + "value": "#65ae8e", + "type": "color", + "description": "7:1 contrast against root-default (#121517)" + } + }, + "default": { + "value": "#121517", + "type": "color", + "description": "1:1 contrast against root-default (#121517)" + } + }, + "layer": { + "-border": { + "default": { + "value": "#2f363b", + "type": "color", + "description": "1.4:1 contrast against layer-default (#181c20)" + } + }, + "-text": { + "default": { + "value": "#b8bbbd", + "type": "color", + "description": "9:1 contrast against layer-default (#181c20)" + } + }, + "-muted": { + "default": { + "value": "#8d9aa1", + "type": "color", + "description": "6:1 contrast against layer-default (#181c20)" + } + }, + "-accent": { + "default": { + "value": "#99a7af", + "type": "color", + "description": "7:1 contrast against layer-default (#181c20)" + } + }, + "-brand": { + "default": { + "value": "#81abb9", + "type": "color", + "description": "7:1 contrast against layer-default (#181c20)" + } + }, + "-alert": { + "default": { + "value": "#e88b8b", + "type": "color", + "description": "7:1 contrast against layer-default (#181c20)" + } + }, + "-info": { + "default": { + "value": "#82a8d3", + "type": "color", + "description": "7:1 contrast against layer-default (#181c20)" + } + }, + "-success": { + "default": { + "value": "#68b392", + "type": "color", + "description": "7:1 contrast against layer-default (#181c20)" + } + }, + "default": { + "value": "#181c20", + "type": "color", + "description": "1:1 contrast against layer-default (#181c20)" + } + }, + "ui": { + "-border": { + "default": { + "value": "#343b40", + "type": "color", + "description": "1.4:1 contrast against ui-active (#1e2327)" + } + }, + "-text": { + "default": { + "value": "#bfc2c4", + "type": "color", + "description": "9:1 contrast against ui-active (#1e2327)" + } + }, + "-muted": { + "default": { + "value": "#93a0a7", + "type": "color", + "description": "6:1 contrast against ui-active (#1e2327)" + } + }, + "default": { + "value": "#101315", + "type": "color", + "description": "-1.15:1 contrast against ui-active (#1e2327)" + }, + "hover": { + "value": "#171b1e", + "type": "color", + "description": "-1.08:1 contrast against ui-active (#1e2327)" + }, + "active": { + "value": "#1e2327", + "type": "color", + "description": "1:1 contrast against ui-active (#1e2327)" + } + }, + "ui-accent": { + "-border": { + "default": { + "value": "#8b979d", + "type": "color", + "description": "1.1:1 contrast against ui-accent-active (#909ea5)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-accent-active (#909ea5)" + } + }, + "-muted": { + "default": { + "value": "#1c2023", + "type": "color", + "description": "6:1 contrast against ui-accent-active (#909ea5)" + } + }, + "default": { + "value": "#9cabb3", + "type": "color", + "description": "-1.15:1 contrast against ui-accent-active (#909ea5)" + }, + "hover": { + "value": "#97a6ad", + "type": "color", + "description": "-1.08:1 contrast against ui-accent-active (#909ea5)" + }, + "active": { + "value": "#909ea5", + "type": "color", + "description": "1:1 contrast against ui-accent-active (#909ea5)" + } + }, + "ui-brand": { + "-border": { + "default": { + "value": "#618996", + "type": "color", + "description": "1.1:1 contrast against ui-brand-active (#62909f)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-brand-active (#62909f)" + } + }, + "-muted": { + "default": { + "value": "#010203", + "type": "color", + "description": "6:1 contrast against ui-brand-active (#62909f)" + } + }, + "default": { + "value": "#6b9dad", + "type": "color", + "description": "-1.15:1 contrast against ui-brand-active (#62909f)" + }, + "hover": { + "value": "#6797a7", + "type": "color", + "description": "-1.08:1 contrast against ui-brand-active (#62909f)" + }, + "active": { + "value": "#62909f", + "type": "color", + "description": "1:1 contrast against ui-brand-active (#62909f)" + } + }, + "ui-alert": { + "-border": { + "default": { + "value": "#ce6565", + "type": "color", + "description": "1.1:1 contrast against ui-alert-active (#df6666)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-alert-active (#df6666)" + } + }, + "-muted": { + "default": { + "value": "#100404", + "type": "color", + "description": "6:1 contrast against ui-alert-active (#df6666)" + } + }, + "default": { + "value": "#e47878", + "type": "color", + "description": "-1.15:1 contrast against ui-alert-active (#df6666)" + }, + "hover": { + "value": "#e26f6f", + "type": "color", + "description": "-1.08:1 contrast against ui-alert-active (#df6666)" + }, + "active": { + "value": "#df6666", + "type": "color", + "description": "1:1 contrast against ui-alert-active (#df6666)" + } + }, + "ui-info": { + "-border": { + "default": { + "value": "#6885a8", + "type": "color", + "description": "1.1:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "-muted": { + "default": { + "value": "#010206", + "type": "color", + "description": "6:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "default": { + "value": "#7399c4", + "type": "color", + "description": "-1.15:1 contrast against ui-info-active (#6a8cb3)" + }, + "hover": { + "value": "#6f93bc", + "type": "color", + "description": "-1.08:1 contrast against ui-info-active (#6a8cb3)" + }, + "active": { + "value": "#6a8cb3", + "type": "color", + "description": "1:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "ui-success": { + "-border": { + "default": { + "value": "#588e76", + "type": "color", + "description": "1.1:1 contrast against ui-success-active (#58977b)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-success-active (#58977b)" + } + }, + "-muted": { + "default": { + "value": "#000403", + "type": "color", + "description": "6:1 contrast against ui-success-active (#58977b)" + } + }, + "default": { + "value": "#5fa385", + "type": "color", + "description": "-1.15:1 contrast against ui-success-active (#58977b)" + }, + "hover": { + "value": "#5c9e81", + "type": "color", + "description": "-1.08:1 contrast against ui-success-active (#58977b)" + }, + "active": { + "value": "#58977b", + "type": "color", + "description": "1:1 contrast against ui-success-active (#58977b)" + } + } + } + } +} \ No newline at end of file diff --git a/design/design/tokens/themes/leo/light.json b/design/design/tokens/themes/leo/light.json new file mode 100644 index 0000000..af1a2ed --- /dev/null +++ b/design/design/tokens/themes/leo/light.json @@ -0,0 +1,360 @@ +{ + "theme": { + "color": { + "root": { + "-border": { + "default": { + "value": "#f0f2f3", + "type": "color", + "description": "1.05:1 contrast against root-default (#f5f7f8)" + } + }, + "-text": { + "default": { + "value": "#4c5154", + "type": "color", + "description": "7.5:1 contrast against root-default (#f5f7f8)" + } + }, + "-muted": { + "default": { + "value": "#627076", + "type": "color", + "description": "4.8:1 contrast against root-default (#f5f7f8)" + } + }, + "-accent": { + "default": { + "value": "#46575f", + "type": "color", + "description": "7:1 contrast against root-default (#f5f7f8)" + } + }, + "-brand": { + "default": { + "value": "#365a66", + "type": "color", + "description": "7:1 contrast against root-default (#f5f7f8)" + } + }, + "-alert": { + "default": { + "value": "#903838", + "type": "color", + "description": "7:1 contrast against root-default (#f5f7f8)" + } + }, + "-info": { + "default": { + "value": "#3a5875", + "type": "color", + "description": "7:1 contrast against root-default (#f5f7f8)" + } + }, + "-success": { + "default": { + "value": "#2e5e48", + "type": "color", + "description": "7:1 contrast against root-default (#f5f7f8)" + } + }, + "default": { + "value": "#f5f7f8", + "type": "color", + "description": "1:1 contrast against root-default (#f5f7f8)" + } + }, + "layer": { + "-border": { + "default": { + "value": "#f4f6f7", + "type": "color", + "description": "1.05:1 contrast against layer-default (#fbfcfd)" + } + }, + "-text": { + "default": { + "value": "#4e5456", + "type": "color", + "description": "7.5:1 contrast against layer-default (#fbfcfd)" + } + }, + "-muted": { + "default": { + "value": "#657278", + "type": "color", + "description": "4.8:1 contrast against layer-default (#fbfcfd)" + } + }, + "-accent": { + "default": { + "value": "#495a62", + "type": "color", + "description": "7:1 contrast against layer-default (#fbfcfd)" + } + }, + "-brand": { + "default": { + "value": "#375c68", + "type": "color", + "description": "7:1 contrast against layer-default (#fbfcfd)" + } + }, + "-alert": { + "default": { + "value": "#933a3a", + "type": "color", + "description": "7:1 contrast against layer-default (#fbfcfd)" + } + }, + "-info": { + "default": { + "value": "#3b5a78", + "type": "color", + "description": "7:1 contrast against layer-default (#fbfcfd)" + } + }, + "-success": { + "default": { + "value": "#2f6149", + "type": "color", + "description": "7:1 contrast against layer-default (#fbfcfd)" + } + }, + "default": { + "value": "#fbfcfd", + "type": "color", + "description": "1:1 contrast against layer-default (#fbfcfd)" + } + }, + "ui": { + "-border": { + "default": { + "value": "#e3e7e9", + "type": "color", + "description": "1.08:1 contrast against ui-active (#edf0f1)" + } + }, + "-text": { + "default": { + "value": "#474d4f", + "type": "color", + "description": "7.5:1 contrast against ui-active (#edf0f1)" + } + }, + "-muted": { + "default": { + "value": "#5e6b71", + "type": "color", + "description": "4.8:1 contrast against ui-active (#edf0f1)" + } + }, + "default": { + "value": "#fbfcfc", + "type": "color", + "description": "-1.1:1 contrast against ui-active (#edf0f1)" + }, + "hover": { + "value": "#f3f6f7", + "type": "color", + "description": "-1.05:1 contrast against ui-active (#edf0f1)" + }, + "active": { + "value": "#edf0f1", + "type": "color", + "description": "1:1 contrast against ui-active (#edf0f1)" + } + }, + "ui-accent": { + "-border": { + "default": { + "value": "#667b85", + "type": "color", + "description": "1.1:1 contrast against ui-accent-active (#60757f)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-accent-active (#60757f)" + } + }, + "-muted": { + "default": { + "value": "#fcfdfd", + "type": "color", + "description": "4.8:1 contrast against ui-accent-active (#60757f)" + } + }, + "default": { + "value": "#556a73", + "type": "color", + "description": "-1.15:1 contrast against ui-accent-active (#60757f)" + }, + "hover": { + "value": "#596f78", + "type": "color", + "description": "-1.08:1 contrast against ui-accent-active (#60757f)" + }, + "active": { + "value": "#60757f", + "type": "color", + "description": "1:1 contrast against ui-accent-active (#60757f)" + } + }, + "ui-brand": { + "-border": { + "default": { + "value": "#517e8d", + "type": "color", + "description": "1.1:1 contrast against ui-brand-active (#487887)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-brand-active (#487887)" + } + }, + "-muted": { + "default": { + "value": "#fbfeff", + "type": "color", + "description": "4.8:1 contrast against ui-brand-active (#487887)" + } + }, + "default": { + "value": "#416e7c", + "type": "color", + "description": "-1.15:1 contrast against ui-brand-active (#487887)" + }, + "hover": { + "value": "#447280", + "type": "color", + "description": "-1.08:1 contrast against ui-brand-active (#487887)" + }, + "active": { + "value": "#487887", + "type": "color", + "description": "1:1 contrast against ui-brand-active (#487887)" + } + }, + "ui-alert": { + "-border": { + "default": { + "value": "#b15353", + "type": "color", + "description": "1.1:1 contrast against ui-alert-active (#b24646)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-alert-active (#b24646)" + } + }, + "-muted": { + "default": { + "value": "#feeded", + "type": "color", + "description": "4.8:1 contrast against ui-alert-active (#b24646)" + } + }, + "default": { + "value": "#a34040", + "type": "color", + "description": "-1.15:1 contrast against ui-alert-active (#b24646)" + }, + "hover": { + "value": "#a84242", + "type": "color", + "description": "-1.08:1 contrast against ui-alert-active (#b24646)" + }, + "active": { + "value": "#b24646", + "type": "color", + "description": "1:1 contrast against ui-alert-active (#b24646)" + } + }, + "ui-info": { + "-border": { + "default": { + "value": "#567ba0", + "type": "color", + "description": "1.1:1 contrast against ui-info-active (#4d759c)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-info-active (#4d759c)" + } + }, + "-muted": { + "default": { + "value": "#fafdff", + "type": "color", + "description": "4.8:1 contrast against ui-info-active (#4d759c)" + } + }, + "default": { + "value": "#466a8d", + "type": "color", + "description": "-1.15:1 contrast against ui-info-active (#4d759c)" + }, + "hover": { + "value": "#496f94", + "type": "color", + "description": "-1.08:1 contrast against ui-info-active (#4d759c)" + }, + "active": { + "value": "#4d759c", + "type": "color", + "description": "1:1 contrast against ui-info-active (#4d759c)" + } + }, + "ui-success": { + "-border": { + "default": { + "value": "#468467", + "type": "color", + "description": "1.1:1 contrast against ui-success-active (#3d7d5f)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-success-active (#3d7d5f)" + } + }, + "-muted": { + "default": { + "value": "#f9fefc", + "type": "color", + "description": "4.8:1 contrast against ui-success-active (#3d7d5f)" + } + }, + "default": { + "value": "#377257", + "type": "color", + "description": "-1.15:1 contrast against ui-success-active (#3d7d5f)" + }, + "hover": { + "value": "#3a775b", + "type": "color", + "description": "-1.08:1 contrast against ui-success-active (#3d7d5f)" + }, + "active": { + "value": "#3d7d5f", + "type": "color", + "description": "1:1 contrast against ui-success-active (#3d7d5f)" + } + } + } + } +} \ No newline at end of file diff --git a/design/design/tokens/themes/neo/dark.json b/design/design/tokens/themes/neo/dark.json new file mode 100644 index 0000000..ab1e8b6 --- /dev/null +++ b/design/design/tokens/themes/neo/dark.json @@ -0,0 +1,360 @@ +{ + "theme": { + "color": { + "root": { + "-border": { + "default": { + "value": "#2e323a", + "type": "color", + "description": "1.4:1 contrast against root-default (#15171c)" + } + }, + "-text": { + "default": { + "value": "#b6b7bb", + "type": "color", + "description": "9:1 contrast against root-default (#15171c)" + } + }, + "-muted": { + "default": { + "value": "#8f95a0", + "type": "color", + "description": "6:1 contrast against root-default (#15171c)" + } + }, + "-accent": { + "default": { + "value": "#9ea2a6", + "type": "color", + "description": "7:1 contrast against root-default (#15171c)" + } + }, + "-brand": { + "default": { + "value": "#8a9eed", + "type": "color", + "description": "7:1 contrast against root-default (#15171c)" + } + }, + "-alert": { + "default": { + "value": "#e78686", + "type": "color", + "description": "7:1 contrast against root-default (#15171c)" + } + }, + "-info": { + "default": { + "value": "#7da5d2", + "type": "color", + "description": "7:1 contrast against root-default (#15171c)" + } + }, + "-success": { + "default": { + "value": "#66b08f", + "type": "color", + "description": "7:1 contrast against root-default (#15171c)" + } + }, + "default": { + "value": "#15171c", + "type": "color", + "description": "1:1 contrast against root-default (#15171c)" + } + }, + "layer": { + "-border": { + "default": { + "value": "#32353d", + "type": "color", + "description": "1.4:1 contrast against layer-default (#191c20)" + } + }, + "-text": { + "default": { + "value": "#babbbf", + "type": "color", + "description": "9:1 contrast against layer-default (#191c20)" + } + }, + "-muted": { + "default": { + "value": "#9298a4", + "type": "color", + "description": "6:1 contrast against layer-default (#191c20)" + } + }, + "-accent": { + "default": { + "value": "#a2a5aa", + "type": "color", + "description": "7:1 contrast against layer-default (#191c20)" + } + }, + "-brand": { + "default": { + "value": "#8ea1ed", + "type": "color", + "description": "7:1 contrast against layer-default (#191c20)" + } + }, + "-alert": { + "default": { + "value": "#e88b8b", + "type": "color", + "description": "7:1 contrast against layer-default (#191c20)" + } + }, + "-info": { + "default": { + "value": "#83a8d4", + "type": "color", + "description": "7:1 contrast against layer-default (#191c20)" + } + }, + "-success": { + "default": { + "value": "#68b392", + "type": "color", + "description": "7:1 contrast against layer-default (#191c20)" + } + }, + "default": { + "value": "#191c20", + "type": "color", + "description": "1:1 contrast against layer-default (#191c20)" + } + }, + "ui": { + "-border": { + "default": { + "value": "#373b42", + "type": "color", + "description": "1.4:1 contrast against ui-active (#20242a)" + } + }, + "-text": { + "default": { + "value": "#c2c4c7", + "type": "color", + "description": "9:1 contrast against ui-active (#20242a)" + } + }, + "-muted": { + "default": { + "value": "#99a0ac", + "type": "color", + "description": "6:1 contrast against ui-active (#20242a)" + } + }, + "default": { + "value": "#131519", + "type": "color", + "description": "-1.15:1 contrast against ui-active (#20242a)" + }, + "hover": { + "value": "#191c20", + "type": "color", + "description": "-1.08:1 contrast against ui-active (#20242a)" + }, + "active": { + "value": "#20242a", + "type": "color", + "description": "1:1 contrast against ui-active (#20242a)" + } + }, + "ui-accent": { + "-border": { + "default": { + "value": "#a0a1a4", + "type": "color", + "description": "1.1:1 contrast against ui-accent-active (#a6a9ae)" + } + }, + "-text": { + "default": { + "value": "#000001", + "type": "color", + "description": "9:1 contrast against ui-accent-active (#a6a9ae)" + } + }, + "-muted": { + "default": { + "value": "#262c37", + "type": "color", + "description": "6:1 contrast against ui-accent-active (#a6a9ae)" + } + }, + "default": { + "value": "#b4b7bd", + "type": "color", + "description": "-1.15:1 contrast against ui-accent-active (#a6a9ae)" + }, + "hover": { + "value": "#aeb1b6", + "type": "color", + "description": "-1.08:1 contrast against ui-accent-active (#a6a9ae)" + }, + "active": { + "value": "#a6a9ae", + "type": "color", + "description": "1:1 contrast against ui-accent-active (#a6a9ae)" + } + }, + "ui-brand": { + "-border": { + "default": { + "value": "#5f7ce8", + "type": "color", + "description": "1.1:1 contrast against ui-brand-active (#6a84e8)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-brand-active (#6a84e8)" + } + }, + "-muted": { + "default": { + "value": "#020308", + "type": "color", + "description": "6:1 contrast against ui-brand-active (#6a84e8)" + } + }, + "default": { + "value": "#7a91ea", + "type": "color", + "description": "-1.15:1 contrast against ui-brand-active (#6a84e8)" + }, + "hover": { + "value": "#738ce9", + "type": "color", + "description": "-1.08:1 contrast against ui-brand-active (#6a84e8)" + }, + "active": { + "value": "#6a84e8", + "type": "color", + "description": "1:1 contrast against ui-brand-active (#6a84e8)" + } + }, + "ui-alert": { + "-border": { + "default": { + "value": "#ce6565", + "type": "color", + "description": "1.1:1 contrast against ui-alert-active (#df6666)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-alert-active (#df6666)" + } + }, + "-muted": { + "default": { + "value": "#100404", + "type": "color", + "description": "6:1 contrast against ui-alert-active (#df6666)" + } + }, + "default": { + "value": "#e47878", + "type": "color", + "description": "-1.15:1 contrast against ui-alert-active (#df6666)" + }, + "hover": { + "value": "#e26f6f", + "type": "color", + "description": "-1.08:1 contrast against ui-alert-active (#df6666)" + }, + "active": { + "value": "#df6666", + "type": "color", + "description": "1:1 contrast against ui-alert-active (#df6666)" + } + }, + "ui-info": { + "-border": { + "default": { + "value": "#6885a8", + "type": "color", + "description": "1.1:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "-muted": { + "default": { + "value": "#010206", + "type": "color", + "description": "6:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "default": { + "value": "#7399c4", + "type": "color", + "description": "-1.15:1 contrast against ui-info-active (#6a8cb3)" + }, + "hover": { + "value": "#6f93bc", + "type": "color", + "description": "-1.08:1 contrast against ui-info-active (#6a8cb3)" + }, + "active": { + "value": "#6a8cb3", + "type": "color", + "description": "1:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "ui-success": { + "-border": { + "default": { + "value": "#588e76", + "type": "color", + "description": "1.1:1 contrast against ui-success-active (#58977b)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-success-active (#58977b)" + } + }, + "-muted": { + "default": { + "value": "#000403", + "type": "color", + "description": "6:1 contrast against ui-success-active (#58977b)" + } + }, + "default": { + "value": "#5fa385", + "type": "color", + "description": "-1.15:1 contrast against ui-success-active (#58977b)" + }, + "hover": { + "value": "#5c9e81", + "type": "color", + "description": "-1.08:1 contrast against ui-success-active (#58977b)" + }, + "active": { + "value": "#58977b", + "type": "color", + "description": "1:1 contrast against ui-success-active (#58977b)" + } + } + } + } +} \ No newline at end of file diff --git a/design/design/tokens/themes/neo/light.json b/design/design/tokens/themes/neo/light.json new file mode 100644 index 0000000..9f9768a --- /dev/null +++ b/design/design/tokens/themes/neo/light.json @@ -0,0 +1,360 @@ +{ + "theme": { + "color": { + "root": { + "-border": { + "default": { + "value": "#f1f2f4", + "type": "color", + "description": "1.05:1 contrast against root-default (#f5f7fa)" + } + }, + "-text": { + "default": { + "value": "#4e5155", + "type": "color", + "description": "7.5:1 contrast against root-default (#f5f7fa)" + } + }, + "-muted": { + "default": { + "value": "#686e7c", + "type": "color", + "description": "4.8:1 contrast against root-default (#f5f7fa)" + } + }, + "-accent": { + "default": { + "value": "#4c5663", + "type": "color", + "description": "7:1 contrast against root-default (#f5f7fa)" + } + }, + "-brand": { + "default": { + "value": "#314bb6", + "type": "color", + "description": "7:1 contrast against root-default (#f5f7fa)" + } + }, + "-alert": { + "default": { + "value": "#903838", + "type": "color", + "description": "7:1 contrast against root-default (#f5f7fa)" + } + }, + "-info": { + "default": { + "value": "#3a5875", + "type": "color", + "description": "7:1 contrast against root-default (#f5f7fa)" + } + }, + "-success": { + "default": { + "value": "#2e5e48", + "type": "color", + "description": "7:1 contrast against root-default (#f5f7fa)" + } + }, + "default": { + "value": "#f5f7fa", + "type": "color", + "description": "1:1 contrast against root-default (#f5f7fa)" + } + }, + "layer": { + "-border": { + "default": { + "value": "#ecedf0", + "type": "color", + "description": "1.05:1 contrast against layer-default (#f1f2f3)" + } + }, + "-text": { + "default": { + "value": "#4b4e53", + "type": "color", + "description": "7.5:1 contrast against layer-default (#f1f2f3)" + } + }, + "-muted": { + "default": { + "value": "#656b78", + "type": "color", + "description": "4.8:1 contrast against layer-default (#f1f2f3)" + } + }, + "-accent": { + "default": { + "value": "#4a5361", + "type": "color", + "description": "7:1 contrast against layer-default (#f1f2f3)" + } + }, + "-brand": { + "default": { + "value": "#2f49b0", + "type": "color", + "description": "7:1 contrast against layer-default (#f1f2f3)" + } + }, + "-alert": { + "default": { + "value": "#8b3737", + "type": "color", + "description": "7:1 contrast against layer-default (#f1f2f3)" + } + }, + "-info": { + "default": { + "value": "#385471", + "type": "color", + "description": "7:1 contrast against layer-default (#f1f2f3)" + } + }, + "-success": { + "default": { + "value": "#2c5b46", + "type": "color", + "description": "7:1 contrast against layer-default (#f1f2f3)" + } + }, + "default": { + "value": "#f1f2f3", + "type": "color", + "description": "1:1 contrast against layer-default (#f1f2f3)" + } + }, + "ui": { + "-border": { + "default": { + "value": "#dbdee3", + "type": "color", + "description": "1.08:1 contrast against ui-active (#e5e6e7)" + } + }, + "-text": { + "default": { + "value": "#45474c", + "type": "color", + "description": "7.5:1 contrast against ui-active (#e5e6e7)" + } + }, + "-muted": { + "default": { + "value": "#5e6470", + "type": "color", + "description": "4.8:1 contrast against ui-active (#e5e6e7)" + } + }, + "default": { + "value": "#f1f2f4", + "type": "color", + "description": "-1.1:1 contrast against ui-active (#e5e6e7)" + }, + "hover": { + "value": "#ebecee", + "type": "color", + "description": "-1.05:1 contrast against ui-active (#e5e6e7)" + }, + "active": { + "value": "#e5e6e7", + "type": "color", + "description": "1:1 contrast against ui-active (#e5e6e7)" + } + }, + "ui-accent": { + "-border": { + "default": { + "value": "#5e6470", + "type": "color", + "description": "1.1:1 contrast against ui-accent-active (#555f6c)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-accent-active (#555f6c)" + } + }, + "-muted": { + "default": { + "value": "#d9dde2", + "type": "color", + "description": "4.8:1 contrast against ui-accent-active (#555f6c)" + } + }, + "default": { + "value": "#4b5563", + "type": "color", + "description": "-1.15:1 contrast against ui-accent-active (#555f6c)" + }, + "hover": { + "value": "#4f5967", + "type": "color", + "description": "-1.08:1 contrast against ui-accent-active (#555f6c)" + }, + "active": { + "value": "#555f6c", + "type": "color", + "description": "1:1 contrast against ui-accent-active (#555f6c)" + } + }, + "ui-brand": { + "-border": { + "default": { + "value": "#4766e0", + "type": "color", + "description": "1.1:1 contrast against ui-brand-active (#3f5edc)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-brand-active (#3f5edc)" + } + }, + "-muted": { + "default": { + "value": "#ecf0ff", + "type": "color", + "description": "4.8:1 contrast against ui-brand-active (#3f5edc)" + } + }, + "default": { + "value": "#3755cd", + "type": "color", + "description": "-1.15:1 contrast against ui-brand-active (#3f5edc)" + }, + "hover": { + "value": "#3958d5", + "type": "color", + "description": "-1.08:1 contrast against ui-brand-active (#3f5edc)" + }, + "active": { + "value": "#3f5edc", + "type": "color", + "description": "1:1 contrast against ui-brand-active (#3f5edc)" + } + }, + "ui-alert": { + "-border": { + "default": { + "value": "#b15353", + "type": "color", + "description": "1.1:1 contrast against ui-alert-active (#b24646)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-alert-active (#b24646)" + } + }, + "-muted": { + "default": { + "value": "#feeded", + "type": "color", + "description": "4.8:1 contrast against ui-alert-active (#b24646)" + } + }, + "default": { + "value": "#a34040", + "type": "color", + "description": "-1.15:1 contrast against ui-alert-active (#b24646)" + }, + "hover": { + "value": "#a84242", + "type": "color", + "description": "-1.08:1 contrast against ui-alert-active (#b24646)" + }, + "active": { + "value": "#b24646", + "type": "color", + "description": "1:1 contrast against ui-alert-active (#b24646)" + } + }, + "ui-info": { + "-border": { + "default": { + "value": "#567ba0", + "type": "color", + "description": "1.1:1 contrast against ui-info-active (#4d759c)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-info-active (#4d759c)" + } + }, + "-muted": { + "default": { + "value": "#fafdff", + "type": "color", + "description": "4.8:1 contrast against ui-info-active (#4d759c)" + } + }, + "default": { + "value": "#466a8d", + "type": "color", + "description": "-1.15:1 contrast against ui-info-active (#4d759c)" + }, + "hover": { + "value": "#496f94", + "type": "color", + "description": "-1.08:1 contrast against ui-info-active (#4d759c)" + }, + "active": { + "value": "#4d759c", + "type": "color", + "description": "1:1 contrast against ui-info-active (#4d759c)" + } + }, + "ui-success": { + "-border": { + "default": { + "value": "#468467", + "type": "color", + "description": "1.1:1 contrast against ui-success-active (#3d7d5f)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-success-active (#3d7d5f)" + } + }, + "-muted": { + "default": { + "value": "#f9fefc", + "type": "color", + "description": "4.8:1 contrast against ui-success-active (#3d7d5f)" + } + }, + "default": { + "value": "#377257", + "type": "color", + "description": "-1.15:1 contrast against ui-success-active (#3d7d5f)" + }, + "hover": { + "value": "#3a775b", + "type": "color", + "description": "-1.08:1 contrast against ui-success-active (#3d7d5f)" + }, + "active": { + "value": "#3d7d5f", + "type": "color", + "description": "1:1 contrast against ui-success-active (#3d7d5f)" + } + } + } + } +} \ No newline at end of file diff --git a/design/design/tokens/themes/uno/dark.json b/design/design/tokens/themes/uno/dark.json new file mode 100644 index 0000000..05a40c3 --- /dev/null +++ b/design/design/tokens/themes/uno/dark.json @@ -0,0 +1,360 @@ +{ + "theme": { + "color": { + "root": { + "-border": { + "default": { + "value": "#332f3d", + "type": "color", + "description": "1.4:1 contrast against root-default (#17141c)" + } + }, + "-text": { + "default": { + "value": "#b7b5bc", + "type": "color", + "description": "9:1 contrast against root-default (#17141c)" + } + }, + "-muted": { + "default": { + "value": "#9791a4", + "type": "color", + "description": "6:1 contrast against root-default (#17141c)" + } + }, + "-accent": { + "default": { + "value": "#a59cb4", + "type": "color", + "description": "7:1 contrast against root-default (#17141c)" + } + }, + "-brand": { + "default": { + "value": "#a79bbd", + "type": "color", + "description": "7:1 contrast against root-default (#17141c)" + } + }, + "-alert": { + "default": { + "value": "#e68585", + "type": "color", + "description": "7:1 contrast against root-default (#17141c)" + } + }, + "-info": { + "default": { + "value": "#7ca3d1", + "type": "color", + "description": "7:1 contrast against root-default (#17141c)" + } + }, + "-success": { + "default": { + "value": "#65ae8e", + "type": "color", + "description": "7:1 contrast against root-default (#17141c)" + } + }, + "default": { + "value": "#17141c", + "type": "color", + "description": "1:1 contrast against root-default (#17141c)" + } + }, + "layer": { + "-border": { + "default": { + "value": "#373341", + "type": "color", + "description": "1.4:1 contrast against layer-default (#1c1a25)" + } + }, + "-text": { + "default": { + "value": "#bcbac1", + "type": "color", + "description": "9:1 contrast against layer-default (#1c1a25)" + } + }, + "-muted": { + "default": { + "value": "#9b95a8", + "type": "color", + "description": "6:1 contrast against layer-default (#1c1a25)" + } + }, + "-accent": { + "default": { + "value": "#aaa1b9", + "type": "color", + "description": "7:1 contrast against layer-default (#1c1a25)" + } + }, + "-brand": { + "default": { + "value": "#ab9fc0", + "type": "color", + "description": "7:1 contrast against layer-default (#1c1a25)" + } + }, + "-alert": { + "default": { + "value": "#e88b8b", + "type": "color", + "description": "7:1 contrast against layer-default (#1c1a25)" + } + }, + "-info": { + "default": { + "value": "#82a8d4", + "type": "color", + "description": "7:1 contrast against layer-default (#1c1a25)" + } + }, + "-success": { + "default": { + "value": "#68b392", + "type": "color", + "description": "7:1 contrast against layer-default (#1c1a25)" + } + }, + "default": { + "value": "#1c1a25", + "type": "color", + "description": "1:1 contrast against layer-default (#1c1a25)" + } + }, + "ui": { + "-border": { + "default": { + "value": "#3c3747", + "type": "color", + "description": "1.4:1 contrast against ui-active (#23202d)" + } + }, + "-text": { + "default": { + "value": "#c3c0c8", + "type": "color", + "description": "9:1 contrast against ui-active (#23202d)" + } + }, + "-muted": { + "default": { + "value": "#a19bad", + "type": "color", + "description": "6:1 contrast against ui-active (#23202d)" + } + }, + "default": { + "value": "#131118", + "type": "color", + "description": "-1.15:1 contrast against ui-active (#23202d)" + }, + "hover": { + "value": "#1a1721", + "type": "color", + "description": "-1.08:1 contrast against ui-active (#23202d)" + }, + "active": { + "value": "#23202d", + "type": "color", + "description": "1:1 contrast against ui-active (#23202d)" + } + }, + "ui-accent": { + "-border": { + "default": { + "value": "#9891a5", + "type": "color", + "description": "1.1:1 contrast against ui-accent-active (#a199af)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-accent-active (#a199af)" + } + }, + "-muted": { + "default": { + "value": "#211e27", + "type": "color", + "description": "6:1 contrast against ui-accent-active (#a199af)" + } + }, + "default": { + "value": "#aea4bd", + "type": "color", + "description": "-1.15:1 contrast against ui-accent-active (#a199af)" + }, + "hover": { + "value": "#a9a0b8", + "type": "color", + "description": "-1.08:1 contrast against ui-accent-active (#a199af)" + }, + "active": { + "value": "#a199af", + "type": "color", + "description": "1:1 contrast against ui-accent-active (#a199af)" + } + }, + "ui-brand": { + "-border": { + "default": { + "value": "#9081aa", + "type": "color", + "description": "1.1:1 contrast against ui-brand-active (#9788b1)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-brand-active (#9788b1)" + } + }, + "-muted": { + "default": { + "value": "#0f0d13", + "type": "color", + "description": "6:1 contrast against ui-brand-active (#9788b1)" + } + }, + "default": { + "value": "#a295b9", + "type": "color", + "description": "-1.15:1 contrast against ui-brand-active (#9788b1)" + }, + "hover": { + "value": "#9d90b6", + "type": "color", + "description": "-1.08:1 contrast against ui-brand-active (#9788b1)" + }, + "active": { + "value": "#9788b1", + "type": "color", + "description": "1:1 contrast against ui-brand-active (#9788b1)" + } + }, + "ui-alert": { + "-border": { + "default": { + "value": "#ce6565", + "type": "color", + "description": "1.1:1 contrast against ui-alert-active (#df6666)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-alert-active (#df6666)" + } + }, + "-muted": { + "default": { + "value": "#100404", + "type": "color", + "description": "6:1 contrast against ui-alert-active (#df6666)" + } + }, + "default": { + "value": "#e47878", + "type": "color", + "description": "-1.15:1 contrast against ui-alert-active (#df6666)" + }, + "hover": { + "value": "#e26f6f", + "type": "color", + "description": "-1.08:1 contrast against ui-alert-active (#df6666)" + }, + "active": { + "value": "#df6666", + "type": "color", + "description": "1:1 contrast against ui-alert-active (#df6666)" + } + }, + "ui-info": { + "-border": { + "default": { + "value": "#6885a8", + "type": "color", + "description": "1.1:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "-muted": { + "default": { + "value": "#010206", + "type": "color", + "description": "6:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "default": { + "value": "#7399c4", + "type": "color", + "description": "-1.15:1 contrast against ui-info-active (#6a8cb3)" + }, + "hover": { + "value": "#6f93bc", + "type": "color", + "description": "-1.08:1 contrast against ui-info-active (#6a8cb3)" + }, + "active": { + "value": "#6a8cb3", + "type": "color", + "description": "1:1 contrast against ui-info-active (#6a8cb3)" + } + }, + "ui-success": { + "-border": { + "default": { + "value": "#588e76", + "type": "color", + "description": "1.1:1 contrast against ui-success-active (#58977b)" + } + }, + "-text": { + "default": { + "value": "#000000", + "type": "color", + "description": "9:1 contrast against ui-success-active (#58977b)" + } + }, + "-muted": { + "default": { + "value": "#000403", + "type": "color", + "description": "6:1 contrast against ui-success-active (#58977b)" + } + }, + "default": { + "value": "#5fa385", + "type": "color", + "description": "-1.15:1 contrast against ui-success-active (#58977b)" + }, + "hover": { + "value": "#5c9e81", + "type": "color", + "description": "-1.08:1 contrast against ui-success-active (#58977b)" + }, + "active": { + "value": "#58977b", + "type": "color", + "description": "1:1 contrast against ui-success-active (#58977b)" + } + } + } + } +} \ No newline at end of file diff --git a/design/design/tokens/themes/uno/light.json b/design/design/tokens/themes/uno/light.json new file mode 100644 index 0000000..4c58128 --- /dev/null +++ b/design/design/tokens/themes/uno/light.json @@ -0,0 +1,360 @@ +{ + "theme": { + "color": { + "root": { + "-border": { + "default": { + "value": "#ebe9ee", + "type": "color", + "description": "1.08:1 contrast against root-default (#f3f2f5)" + } + }, + "-text": { + "default": { + "value": "#42404f", + "type": "color", + "description": "9:1 contrast against root-default (#f3f2f5)" + } + }, + "-muted": { + "default": { + "value": "#505d6f", + "type": "color", + "description": "6:1 contrast against root-default (#f3f2f5)" + } + }, + "-accent": { + "default": { + "value": "#594d67", + "type": "color", + "description": "7:1 contrast against root-default (#f3f2f5)" + } + }, + "-brand": { + "default": { + "value": "#594b75", + "type": "color", + "description": "7:1 contrast against root-default (#f3f2f5)" + } + }, + "-alert": { + "default": { + "value": "#8b3737", + "type": "color", + "description": "7:1 contrast against root-default (#f3f2f5)" + } + }, + "-info": { + "default": { + "value": "#385471", + "type": "color", + "description": "7:1 contrast against root-default (#f3f2f5)" + } + }, + "-success": { + "default": { + "value": "#2c5b45", + "type": "color", + "description": "7:1 contrast against root-default (#f3f2f5)" + } + }, + "default": { + "value": "#f3f2f5", + "type": "color", + "description": "1:1 contrast against root-default (#f3f2f5)" + } + }, + "layer": { + "-border": { + "default": { + "value": "#e0dee4", + "type": "color", + "description": "1.08:1 contrast against layer-default (#e5e6e7)" + } + }, + "-text": { + "default": { + "value": "#3b3948", + "type": "color", + "description": "9:1 contrast against layer-default (#e5e6e7)" + } + }, + "-muted": { + "default": { + "value": "#4a5667", + "type": "color", + "description": "6:1 contrast against layer-default (#e5e6e7)" + } + }, + "-accent": { + "default": { + "value": "#52475e", + "type": "color", + "description": "7:1 contrast against layer-default (#e5e6e7)" + } + }, + "-brand": { + "default": { + "value": "#52456b", + "type": "color", + "description": "7:1 contrast against layer-default (#e5e6e7)" + } + }, + "-alert": { + "default": { + "value": "#803232", + "type": "color", + "description": "7:1 contrast against layer-default (#e5e6e7)" + } + }, + "-info": { + "default": { + "value": "#334e67", + "type": "color", + "description": "7:1 contrast against layer-default (#e5e6e7)" + } + }, + "-success": { + "default": { + "value": "#285340", + "type": "color", + "description": "7:1 contrast against layer-default (#e5e6e7)" + } + }, + "default": { + "value": "#e5e6e7", + "type": "color", + "description": "1:1 contrast against layer-default (#e5e6e7)" + } + }, + "ui": { + "-border": { + "default": { + "value": "#d3d0d8", + "type": "color", + "description": "1.1:1 contrast against ui-active (#dbdade)" + } + }, + "-text": { + "default": { + "value": "#403e4d", + "type": "color", + "description": "7.5:1 contrast against ui-active (#dbdade)" + } + }, + "-muted": { + "default": { + "value": "#515d70", + "type": "color", + "description": "4.8:1 contrast against ui-active (#dbdade)" + } + }, + "default": { + "value": "#e6e5e9", + "type": "color", + "description": "-1.1:1 contrast against ui-active (#dbdade)" + }, + "hover": { + "value": "#e2e1e5", + "type": "color", + "description": "-1.05:1 contrast against ui-active (#dbdade)" + }, + "active": { + "value": "#dbdade", + "type": "color", + "description": "1:1 contrast against ui-active (#dbdade)" + } + }, + "ui-accent": { + "-border": { + "default": { + "value": "#bdb3c8", + "type": "color", + "description": "1.1:1 contrast against ui-accent-active (#c4bccc)" + } + }, + "-text": { + "default": { + "value": "#2d2d2d", + "type": "color", + "description": "7.5:1 contrast against ui-accent-active (#c4bccc)" + } + }, + "-muted": { + "default": { + "value": "#4b4a4c", + "type": "color", + "description": "4.8:1 contrast against ui-accent-active (#c4bccc)" + } + }, + "default": { + "value": "#d1cbd8", + "type": "color", + "description": "-1.15:1 contrast against ui-accent-active (#c4bccc)" + }, + "hover": { + "value": "#ccc5d3", + "type": "color", + "description": "-1.08:1 contrast against ui-accent-active (#c4bccc)" + }, + "active": { + "value": "#c4bccc", + "type": "color", + "description": "1:1 contrast against ui-accent-active (#c4bccc)" + } + }, + "ui-brand": { + "-border": { + "default": { + "value": "#78699a", + "type": "color", + "description": "1.1:1 contrast against ui-brand-active (#716292)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-brand-active (#716292)" + } + }, + "-muted": { + "default": { + "value": "#f2effe", + "type": "color", + "description": "4.8:1 contrast against ui-brand-active (#716292)" + } + }, + "default": { + "value": "#685889", + "type": "color", + "description": "-1.15:1 contrast against ui-brand-active (#716292)" + }, + "hover": { + "value": "#6c5c8e", + "type": "color", + "description": "-1.08:1 contrast against ui-brand-active (#716292)" + }, + "active": { + "value": "#716292", + "type": "color", + "description": "1:1 contrast against ui-brand-active (#716292)" + } + }, + "ui-alert": { + "-border": { + "default": { + "value": "#b15353", + "type": "color", + "description": "1.1:1 contrast against ui-alert-active (#b24646)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-alert-active (#b24646)" + } + }, + "-muted": { + "default": { + "value": "#feeded", + "type": "color", + "description": "4.8:1 contrast against ui-alert-active (#b24646)" + } + }, + "default": { + "value": "#a34040", + "type": "color", + "description": "-1.15:1 contrast against ui-alert-active (#b24646)" + }, + "hover": { + "value": "#a84242", + "type": "color", + "description": "-1.08:1 contrast against ui-alert-active (#b24646)" + }, + "active": { + "value": "#b24646", + "type": "color", + "description": "1:1 contrast against ui-alert-active (#b24646)" + } + }, + "ui-info": { + "-border": { + "default": { + "value": "#567ba0", + "type": "color", + "description": "1.1:1 contrast against ui-info-active (#4d759c)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-info-active (#4d759c)" + } + }, + "-muted": { + "default": { + "value": "#fafdff", + "type": "color", + "description": "4.8:1 contrast against ui-info-active (#4d759c)" + } + }, + "default": { + "value": "#466a8d", + "type": "color", + "description": "-1.15:1 contrast against ui-info-active (#4d759c)" + }, + "hover": { + "value": "#496f94", + "type": "color", + "description": "-1.08:1 contrast against ui-info-active (#4d759c)" + }, + "active": { + "value": "#4d759c", + "type": "color", + "description": "1:1 contrast against ui-info-active (#4d759c)" + } + }, + "ui-success": { + "-border": { + "default": { + "value": "#468467", + "type": "color", + "description": "1.1:1 contrast against ui-success-active (#3d7d5f)" + } + }, + "-text": { + "default": { + "value": "#ffffff", + "type": "color", + "description": "7.5:1 contrast against ui-success-active (#3d7d5f)" + } + }, + "-muted": { + "default": { + "value": "#f9fefc", + "type": "color", + "description": "4.8:1 contrast against ui-success-active (#3d7d5f)" + } + }, + "default": { + "value": "#377257", + "type": "color", + "description": "-1.15:1 contrast against ui-success-active (#3d7d5f)" + }, + "hover": { + "value": "#3a775b", + "type": "color", + "description": "-1.08:1 contrast against ui-success-active (#3d7d5f)" + }, + "active": { + "value": "#3d7d5f", + "type": "color", + "description": "1:1 contrast against ui-success-active (#3d7d5f)" + } + } + } + } +} \ No newline at end of file diff --git a/design/design/transform.mjs b/design/design/transform.mjs new file mode 100644 index 0000000..1e140ed --- /dev/null +++ b/design/design/transform.mjs @@ -0,0 +1,45 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'url'; + +export const SingleToMulti = async () => { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const tokensBasePath = path.resolve(__dirname, "./"); + const single_token = path.resolve(tokensBasePath, 'tokens/tokens.json'); + const multi_token_path = path.resolve(tokensBasePath, 'tokens/multi'); + + try { + const tokens = JSON.parse(await fs.promises.readFile(single_token, 'utf8')); + const metadata = tokens.$metadata || {}; + const themes = tokens.$themes || {}; + delete tokens.$metadata; + delete tokens.$themes; + + const writeTokensToFile = async (filePath, tokenData) => { + const dirName = path.dirname(filePath); + await fs.promises.mkdir(dirName, { recursive: true }); + await fs.promises.writeFile(filePath, JSON.stringify(tokenData, null, 2), 'utf8'); + console.log(`✔︎ ${path.relative(process.cwd(), filePath)}`); + }; + + await writeTokensToFile(path.join(multi_token_path, '$metadata.json'), metadata); + await writeTokensToFile(path.join(multi_token_path, '$themes.json'), themes); + + if (metadata.tokenSetOrder) { + for (const tokenSet of metadata.tokenSetOrder) { + if (tokens[tokenSet]) { + const targetPath = path.join(multi_token_path, ...tokenSet.split('/')) + '.json'; + await writeTokensToFile(targetPath, tokens[tokenSet]); + } + } + } + + console.log('✅ Tokens transformed successfully'); + return { success: true }; + } catch (error) { + console.error('❌ Token transformation failed:', error.message); + return { success: false, error }; + } +}; + diff --git a/design/main.css b/design/main.css new file mode 100644 index 0000000..6325335 --- /dev/null +++ b/design/main.css @@ -0,0 +1,3 @@ +@import "tailwindcss/utilities.css"; +@import "./tokens.css"; +@import "./utilities.css"; diff --git a/design/tokens.css b/design/tokens.css new file mode 100644 index 0000000..f8114b4 --- /dev/null +++ b/design/tokens.css @@ -0,0 +1,6 @@ +@import "./tokens/semantic/color.css"; +@import "./tokens/semantic/dimension.css"; +@import "./tokens/semantic/border.css"; +@import "./tokens/semantic/text.css"; +@import "./tokens/semantic/font.css"; +@import "./tokens/semantic/effect.css"; diff --git a/design/tokens/semantic/border.css b/design/tokens/semantic/border.css new file mode 100644 index 0000000..9045c61 --- /dev/null +++ b/design/tokens/semantic/border.css @@ -0,0 +1,8 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +@theme inline { + --radius-ui: 0.375rem; + --radius-full: 9999px; +} diff --git a/design/tokens/semantic/color.css b/design/tokens/semantic/color.css new file mode 100644 index 0000000..59586a2 --- /dev/null +++ b/design/tokens/semantic/color.css @@ -0,0 +1,60 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +@theme inline { + --color-root: var(--theme-color-root); + --color-root--border: var(--theme-color-root--border); + --color-root--text: var(--theme-color-root--text); + --color-root--muted: var(--theme-color-root--muted); + --color-root--accent: var(--theme-color-root--accent); + --color-root--brand: var(--theme-color-root--brand); + --color-root--alert: var(--theme-color-root--alert); + --color-root--info: var(--theme-color-root--info); + --color-root--success: var(--theme-color-root--success); + --color-layer: var(--theme-color-layer); + --color-layer--border: var(--theme-color-layer--border); + --color-layer--text: var(--theme-color-layer--text); + --color-layer--muted: var(--theme-color-layer--muted); + --color-layer--accent: var(--theme-color-layer--accent); + --color-layer--brand: var(--theme-color-layer--brand); + --color-layer--alert: var(--theme-color-layer--alert); + --color-layer--info: var(--theme-color-layer--info); + --color-layer--success: var(--theme-color-layer--success); + --color-ui: var(--theme-color-ui); + --color-ui-hover: var(--theme-color-ui-hover); + --color-ui-active: var(--theme-color-ui-active); + --color-ui--border: var(--theme-color-ui--border); + --color-ui--text: var(--theme-color-ui--text); + --color-ui--muted: var(--theme-color-ui--muted); + --color-ui-accent: var(--theme-color-ui-accent); + --color-ui-accent-hover: var(--theme-color-ui-accent-hover); + --color-ui-accent-active: var(--theme-color-ui-accent-active); + --color-ui-accent--border: var(--theme-color-ui-accent--border); + --color-ui-accent--text: var(--theme-color-ui-accent--text); + --color-ui-accent--muted: var(--theme-color-ui-accent--muted); + --color-ui-brand: var(--theme-color-ui-brand); + --color-ui-brand-hover: var(--theme-color-ui-brand-hover); + --color-ui-brand-active: var(--theme-color-ui-brand-active); + --color-ui-brand--border: var(--theme-color-ui-brand--border); + --color-ui-brand--text: var(--theme-color-ui-brand--text); + --color-ui-brand--muted: var(--theme-color-ui-brand--muted); + --color-ui-alert: var(--theme-color-ui-alert); + --color-ui-alert-hover: var(--theme-color-ui-alert-hover); + --color-ui-alert-active: var(--theme-color-ui-alert-active); + --color-ui-alert--border: var(--theme-color-ui-alert--border); + --color-ui-alert--text: var(--theme-color-ui-alert--text); + --color-ui-alert--muted: var(--theme-color-ui-alert--muted); + --color-ui-info: var(--theme-color-ui-info); + --color-ui-info-hover: var(--theme-color-ui-info-hover); + --color-ui-info-active: var(--theme-color-ui-info-active); + --color-ui-info--border: var(--theme-color-ui-info--border); + --color-ui-info--text: var(--theme-color-ui-info--text); + --color-ui-info--muted: var(--theme-color-ui-info--muted); + --color-ui-success: var(--theme-color-ui-success); + --color-ui-success-hover: var(--theme-color-ui-success-hover); + --color-ui-success-active: var(--theme-color-ui-success-active); + --color-ui-success--border: var(--theme-color-ui-success--border); + --color-ui-success--text: var(--theme-color-ui-success--text); + --color-ui-success--muted: var(--theme-color-ui-success--muted); +} diff --git a/design/tokens/semantic/dimension.css b/design/tokens/semantic/dimension.css new file mode 100644 index 0000000..feaa064 --- /dev/null +++ b/design/tokens/semantic/dimension.css @@ -0,0 +1,116 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +@theme inline { + --spacing: 0.25rem; + --container-ui-sm: 20rem; + --container-ui-md: 24rem; + --container-ui-lg: 40rem; + --container-ui-xl: 60rem; + --container-mini-sm: 12rem; + --container-mini-md: 16rem; + --container-mini-lg: 20rem; + --container-mini-xl: 24rem; + --container-micro-sm: 6rem; + --container-micro-md: 8rem; + --container-micro-lg: 12rem; + --container-micro-xl: 16rem; + --breakpoint-sm: 20rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 106rem; + --spacing-ui-sm: calc(var(--spacing) * 8); + --spacing-ui-md: calc(var(--spacing) * 10); + --spacing-ui-lg: calc(var(--spacing) * 12); + --spacing-ui-xl: calc(var(--spacing) * 14); + --spacing-ui-padding-sm: calc(var(--spacing) * 2); + --spacing-ui-padding-md: calc(var(--spacing) * 3); + --spacing-ui-padding-lg: calc(var(--spacing) * 4); + --spacing-ui-padding-xl: calc(var(--spacing) * 5); + --spacing-ui-gap-sm: calc(var(--spacing) * 2); + --spacing-ui-gap-md: calc(var(--spacing) * 4); + --spacing-ui-gap-lg: calc(var(--spacing) * 6); + --spacing-ui-gap-xl: calc(var(--spacing) * 8); + --spacing-mini-sm: calc(var(--spacing) * 5); + --spacing-mini-md: calc(var(--spacing) * 6); + --spacing-mini-lg: calc(var(--spacing) * 7); + --spacing-mini-xl: calc(var(--spacing) * 8); + --spacing-mini-padding-sm: calc(var(--spacing) * 1); + --spacing-mini-padding-md: calc(var(--spacing) * 2); + --spacing-mini-padding-lg: calc(var(--spacing) * 3); + --spacing-mini-padding-xl: calc(var(--spacing) * 4); + --spacing-mini-gap-sm: calc(var(--spacing) * 1); + --spacing-mini-gap-md: calc(var(--spacing) * 2); + --spacing-mini-gap-lg: calc(var(--spacing) * 3); + --spacing-mini-gap-xl: calc(var(--spacing) * 4); + --spacing-micro-sm: calc(var(--spacing) * 1); + --spacing-micro-md: calc(var(--spacing) * 2); + --spacing-micro-lg: calc(var(--spacing) * 3); + --spacing-micro-xl: calc(var(--spacing) * 4); + --spacing-micro-padding-sm: calc(var(--spacing) * 1); + --spacing-micro-padding-md: calc(var(--spacing) * 1); + --spacing-micro-padding-lg: calc(var(--spacing) * 2); + --spacing-micro-padding-xl: calc(var(--spacing) * 2); + --spacing-micro-gap-sm: calc(var(--spacing) * 1); + --spacing-micro-gap-md: calc(var(--spacing) * 1); + --spacing-micro-gap-lg: calc(var(--spacing) * 2); + --spacing-micro-gap-xl: calc(var(--spacing) * 2); + --container-ui: var(--container-ui-md); + --container-mini: var(--container-mini-md); + --container-micro: var(--container-micro-md); + --width-ui-sm: var(--container-ui-sm); + --width-ui-md: var(--container-ui-md); + --width-ui-lg: var(--container-ui-lg); + --width-ui-xl: var(--container-ui-xl); + --width-mini-sm: var(--container-mini-sm); + --width-mini-md: var(--container-mini-md); + --width-mini-lg: var(--container-mini-lg); + --width-mini-xl: var(--container-mini-xl); + --width-micro-sm: var(--container-micro-sm); + --width-micro-md: var(--container-micro-md); + --width-micro-lg: var(--container-micro-lg); + --width-micro-xl: var(--container-micro-xl); + --min-width-ui-sm: var(--container-ui-sm); + --min-width-ui-md: var(--container-ui-md); + --min-width-ui-lg: var(--container-ui-lg); + --min-width-ui-xl: var(--container-ui-xl); + --min-width-mini-sm: var(--container-mini-sm); + --min-width-mini-md: var(--container-mini-md); + --min-width-mini-lg: var(--container-mini-lg); + --min-width-mini-xl: var(--container-mini-xl); + --min-width-micro-sm: var(--container-micro-sm); + --min-width-micro-md: var(--container-micro-md); + --min-width-micro-lg: var(--container-micro-lg); + --min-width-micro-xl: var(--container-micro-xl); + --max-width-ui-sm: var(--container-ui-sm); + --max-width-ui-md: var(--container-ui-md); + --max-width-ui-lg: var(--container-ui-lg); + --max-width-ui-xl: var(--container-ui-xl); + --max-width-mini-sm: var(--container-mini-sm); + --max-width-mini-md: var(--container-mini-md); + --max-width-mini-lg: var(--container-mini-lg); + --max-width-mini-xl: var(--container-mini-xl); + --max-width-micro-sm: var(--container-micro-sm); + --max-width-micro-md: var(--container-micro-md); + --max-width-micro-lg: var(--container-micro-lg); + --max-width-micro-xl: var(--container-micro-xl); + --spacing-ui: var(--spacing-ui-md); + --spacing-ui-padding: var(--spacing-ui-padding-md); + --spacing-ui-gap: var(--spacing-ui-gap-md); + --spacing-mini: var(--spacing-mini-md); + --spacing-mini-padding: var(--spacing-mini-padding-md); + --spacing-mini-gap: var(--spacing-mini-gap-md); + --spacing-micro: var(--spacing-micro-md); + --spacing-micro-padding: var(--spacing-micro-padding-md); + --spacing-micro-gap: var(--spacing-micro-gap-md); + --width-ui: var(--container-ui); + --width-mini: var(--container-mini); + --width-micro: var(--container-micro); + --min-width-ui: var(--container-ui); + --min-width-mini: var(--container-mini); + --min-width-micro: var(--container-micro); + --max-width-ui: var(--container-ui); + --max-width-mini: var(--container-mini); + --max-width-micro: var(--container-micro); +} diff --git a/design/tokens/semantic/effect.css b/design/tokens/semantic/effect.css new file mode 100644 index 0000000..eb9ae85 --- /dev/null +++ b/design/tokens/semantic/effect.css @@ -0,0 +1,15 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +@theme inline { + --shadow-layer: + 0 1px 1px 0 var(--color-layer--border), + 0 1px 1px 0 var(--color-layer--border); + --shadow-ui: + 0 1px 2px 0 var(--color-ui--border), 0 2px 4px 0 var(--color-ui--border); + --shadow-ui-hover: + 0 3px 4px 0 var(--color-ui--border), 0 2px 4px 0 var(--color-ui--border); + --shadow-ui-active: + 0 1px 2px 0 var(--color-ui--border), 0 1px 1px 0 var(--color-ui--border); +} diff --git a/design/tokens/semantic/font.css b/design/tokens/semantic/font.css new file mode 100644 index 0000000..e85d396 --- /dev/null +++ b/design/tokens/semantic/font.css @@ -0,0 +1,13 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +@theme inline { + --font-ui: system-ui, sans-serif; + --font-code: ui-monospace, monospace; + --font-weight-ui-sm: 200; + --font-weight-ui-md: 400; + --font-weight-ui-lg: 600; + --font-weight-ui-xl: 800; + --font-weight-ui: var(--font-weight-ui-md); +} diff --git a/design/tokens/semantic/text.css b/design/tokens/semantic/text.css new file mode 100644 index 0000000..fd189d3 --- /dev/null +++ b/design/tokens/semantic/text.css @@ -0,0 +1,16 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +@theme inline { + --text-ui-sm: 1rem; + --text-ui-sm--line-height: 1.7; + --text-ui-md: 1.125rem; + --text-ui-md--line-height: 1.8; + --text-ui-lg: 1.25rem; + --text-ui-lg--line-height: 1.9; + --text-ui-xl: 1.8rem; + --text-ui-xl--line-height: 2; + --text-ui: var(--text-ui-md); + --text-ui--line-height: var(--text-ui-md--line-height); +} diff --git a/design/tokens/themes/duo/dark.css b/design/tokens/themes/duo/dark.css new file mode 100644 index 0000000..8e7cf02 --- /dev/null +++ b/design/tokens/themes/duo/dark.css @@ -0,0 +1,60 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +[data-theme="duo"][data-mode="dark"] { + --theme-color-root--border: #382f24; /** 1.4:1 contrast against root-default (#171411) */ + --theme-color-root--text: #b9b5b1; /** 9:1 contrast against root-default (#171411) */ + --theme-color-root--muted: #9d918a; /** 6:1 contrast against root-default (#171411) */ + --theme-color-root--accent: #ae9d8f; /** 7:1 contrast against root-default (#171411) */ + --theme-color-root--brand: #bf9881; /** 7:1 contrast against root-default (#171411) */ + --theme-color-root--alert: #e68484; /** 7:1 contrast against root-default (#171411) */ + --theme-color-root--info: #7ca3d1; /** 7:1 contrast against root-default (#171411) */ + --theme-color-root--success: #65ae8e; /** 7:1 contrast against root-default (#171411) */ + --theme-color-root: #171411; /** 1:1 contrast against root-default (#171411) */ + --theme-color-layer--border: #3e3328; /** 1.4:1 contrast against layer-default (#1f1a16) */ + --theme-color-layer--text: #bebbb6; /** 9:1 contrast against layer-default (#1f1a16) */ + --theme-color-layer--muted: #a2968f; /** 6:1 contrast against layer-default (#1f1a16) */ + --theme-color-layer--accent: #b3a293; /** 7:1 contrast against layer-default (#1f1a16) */ + --theme-color-layer--brand: #c39d88; /** 7:1 contrast against layer-default (#1f1a16) */ + --theme-color-layer--alert: #e88b8b; /** 7:1 contrast against layer-default (#1f1a16) */ + --theme-color-layer--info: #82a8d3; /** 7:1 contrast against layer-default (#1f1a16) */ + --theme-color-layer--success: #68b392; /** 7:1 contrast against layer-default (#1f1a16) */ + --theme-color-layer: #1f1a16; /** 1:1 contrast against layer-default (#1f1a16) */ + --theme-color-ui--border: #41382d; /** 1.4:1 contrast against ui-active (#26211b) */ + --theme-color-ui--text: #c4c1bc; /** 9:1 contrast against ui-active (#26211b) */ + --theme-color-ui--muted: #a69c94; /** 6:1 contrast against ui-active (#26211b) */ + --theme-color-ui: #15120f; /** -1.15:1 contrast against ui-active (#26211b) */ + --theme-color-ui-hover: #1d1915; /** -1.08:1 contrast against ui-active (#26211b) */ + --theme-color-ui-active: #26211b; /** 1:1 contrast against ui-active (#26211b) */ + --theme-color-ui-accent--border: #a19387; /** 1.1:1 contrast against ui-accent-active (#aa998c) */ + --theme-color-ui-accent--text: #000000; /** 9:1 contrast against ui-accent-active (#aa998c) */ + --theme-color-ui-accent--muted: #261e17; /** 6:1 contrast against ui-accent-active (#aa998c) */ + --theme-color-ui-accent: #b8a697; /** -1.15:1 contrast against ui-accent-active (#aa998c) */ + --theme-color-ui-accent-hover: #b1a091; /** -1.08:1 contrast against ui-accent-active (#aa998c) */ + --theme-color-ui-accent-active: #aa998c; /** 1:1 contrast against ui-accent-active (#aa998c) */ + --theme-color-ui-brand--border: #a5816c; /** 1.1:1 contrast against ui-brand-active (#b0866e) */ + --theme-color-ui-brand--text: #000000; /** 9:1 contrast against ui-brand-active (#b0866e) */ + --theme-color-ui-brand--muted: #120e0b; /** 6:1 contrast against ui-brand-active (#b0866e) */ + --theme-color-ui-brand: #bc927a; /** -1.15:1 contrast against ui-brand-active (#b0866e) */ + --theme-color-ui-brand-hover: #b98d75; /** -1.08:1 contrast against ui-brand-active (#b0866e) */ + --theme-color-ui-brand-active: #b0866e; /** 1:1 contrast against ui-brand-active (#b0866e) */ + --theme-color-ui-alert--border: #ce6565; /** 1.1:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert--text: #000000; /** 9:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert--muted: #100404; /** 6:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert: #e47878; /** -1.15:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert-hover: #e26f6f; /** -1.08:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert-active: #df6666; /** 1:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-info--border: #6885a8; /** 1.1:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info--text: #000000; /** 9:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info--muted: #010206; /** 6:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info: #7399c4; /** -1.15:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info-hover: #6f93bc; /** -1.08:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info-active: #6a8cb3; /** 1:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-success--border: #588e76; /** 1.1:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success--text: #000000; /** 9:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success--muted: #000403; /** 6:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success: #5fa385; /** -1.15:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success-hover: #5c9e81; /** -1.08:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success-active: #58977b; /** 1:1 contrast against ui-success-active (#58977b) */ +} diff --git a/design/tokens/themes/duo/light.css b/design/tokens/themes/duo/light.css new file mode 100644 index 0000000..cd86325 --- /dev/null +++ b/design/tokens/themes/duo/light.css @@ -0,0 +1,60 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +[data-theme="duo"][data-mode="light"] { + --theme-color-root--border: #f3f2ef; /** 1.05:1 contrast against root-default (#f8f6f3) */ + --theme-color-root--text: #544f4b; /** 7.5:1 contrast against root-default (#f8f6f3) */ + --theme-color-root--muted: #766c61; /** 4.8:1 contrast against root-default (#f8f6f3) */ + --theme-color-root--accent: #5e5348; /** 7:1 contrast against root-default (#f8f6f3) */ + --theme-color-root--brand: #6f4d3c; /** 7:1 contrast against root-default (#f8f6f3) */ + --theme-color-root--alert: #8f3838; /** 7:1 contrast against root-default (#f8f6f3) */ + --theme-color-root--info: #3a5775; /** 7:1 contrast against root-default (#f8f6f3) */ + --theme-color-root--success: #2d5e47; /** 7:1 contrast against root-default (#f8f6f3) */ + --theme-color-root: #f8f6f3; /** 1:1 contrast against root-default (#f8f6f3) */ + --theme-color-layer--border: #f7f5f3; /** 1.05:1 contrast against layer-default (#fcfbfa) */ + --theme-color-layer--text: #57524e; /** 7.5:1 contrast against layer-default (#fcfbfa) */ + --theme-color-layer--muted: #786f64; /** 4.8:1 contrast against layer-default (#fcfbfa) */ + --theme-color-layer--accent: #61564a; /** 7:1 contrast against layer-default (#fcfbfa) */ + --theme-color-layer--brand: #734f3f; /** 7:1 contrast against layer-default (#fcfbfa) */ + --theme-color-layer--alert: #943a3a; /** 7:1 contrast against layer-default (#fcfbfa) */ + --theme-color-layer--info: #3c5a78; /** 7:1 contrast against layer-default (#fcfbfa) */ + --theme-color-layer--success: #2f614a; /** 7:1 contrast against layer-default (#fcfbfa) */ + --theme-color-layer: #fcfbfa; /** 1:1 contrast against layer-default (#fcfbfa) */ + --theme-color-ui--border: #ebe7e2; /** 1.08:1 contrast against ui-active (#f3f0ec) */ + --theme-color-ui--text: #514c48; /** 7.5:1 contrast against ui-active (#f3f0ec) */ + --theme-color-ui--muted: #72685e; /** 4.8:1 contrast against ui-active (#f3f0ec) */ + --theme-color-ui: #fcfcfb; /** -1.1:1 contrast against ui-active (#f3f0ec) */ + --theme-color-ui-hover: #faf8f6; /** -1.05:1 contrast against ui-active (#f3f0ec) */ + --theme-color-ui-active: #f3f0ec; /** 1:1 contrast against ui-active (#f3f0ec) */ + --theme-color-ui-accent--border: #837567; /** 1.1:1 contrast against ui-accent-active (#7e6e60) */ + --theme-color-ui-accent--text: #ffffff; /** 7.5:1 contrast against ui-accent-active (#7e6e60) */ + --theme-color-ui-accent--muted: #fdfdfc; /** 4.8:1 contrast against ui-accent-active (#7e6e60) */ + --theme-color-ui-accent: #736558; /** -1.15:1 contrast against ui-accent-active (#7e6e60) */ + --theme-color-ui-accent-hover: #77695b; /** -1.08:1 contrast against ui-accent-active (#7e6e60) */ + --theme-color-ui-accent-active: #7e6e60; /** 1:1 contrast against ui-accent-active (#7e6e60) */ + --theme-color-ui-brand--border: #996e59; /** 1.1:1 contrast against ui-brand-active (#966751) */ + --theme-color-ui-brand--text: #ffffff; /** 7.5:1 contrast against ui-brand-active (#966751) */ + --theme-color-ui-brand--muted: #fffcfa; /** 4.8:1 contrast against ui-brand-active (#966751) */ + --theme-color-ui-brand: #875d49; /** -1.15:1 contrast against ui-brand-active (#966751) */ + --theme-color-ui-brand-hover: #8d614c; /** -1.08:1 contrast against ui-brand-active (#966751) */ + --theme-color-ui-brand-active: #966751; /** 1:1 contrast against ui-brand-active (#966751) */ + --theme-color-ui-alert--border: #b15353; /** 1.1:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert--text: #ffffff; /** 7.5:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert--muted: #feeded; /** 4.8:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert: #a34040; /** -1.15:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert-hover: #a84242; /** -1.08:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert-active: #b24646; /** 1:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-info--border: #567ba0; /** 1.1:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info--text: #ffffff; /** 7.5:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info--muted: #fafdff; /** 4.8:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info: #466a8d; /** -1.15:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info-hover: #496f94; /** -1.08:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info-active: #4d759c; /** 1:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-success--border: #468467; /** 1.1:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success--text: #ffffff; /** 7.5:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success--muted: #f9fefc; /** 4.8:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success: #377257; /** -1.15:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success-hover: #3a775b; /** -1.08:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success-active: #3d7d5f; /** 1:1 contrast against ui-success-active (#3d7d5f) */ +} diff --git a/design/tokens/themes/leo/dark.css b/design/tokens/themes/leo/dark.css new file mode 100644 index 0000000..67d5a75 --- /dev/null +++ b/design/tokens/themes/leo/dark.css @@ -0,0 +1,60 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +[data-theme="leo"][data-mode="dark"] { + --theme-color-root--border: #2b3236; /** 1.4:1 contrast against root-default (#121517) */ + --theme-color-root--text: #b3b6b8; /** 9:1 contrast against root-default (#121517) */ + --theme-color-root--muted: #88959c; /** 6:1 contrast against root-default (#121517) */ + --theme-color-root--accent: #94a2aa; /** 7:1 contrast against root-default (#121517) */ + --theme-color-root--brand: #7aa7b5; /** 7:1 contrast against root-default (#121517) */ + --theme-color-root--alert: #e68484; /** 7:1 contrast against root-default (#121517) */ + --theme-color-root--info: #7ca3d1; /** 7:1 contrast against root-default (#121517) */ + --theme-color-root--success: #65ae8e; /** 7:1 contrast against root-default (#121517) */ + --theme-color-root: #121517; /** 1:1 contrast against root-default (#121517) */ + --theme-color-layer--border: #2f363b; /** 1.4:1 contrast against layer-default (#181c20) */ + --theme-color-layer--text: #b8bbbd; /** 9:1 contrast against layer-default (#181c20) */ + --theme-color-layer--muted: #8d9aa1; /** 6:1 contrast against layer-default (#181c20) */ + --theme-color-layer--accent: #99a7af; /** 7:1 contrast against layer-default (#181c20) */ + --theme-color-layer--brand: #81abb9; /** 7:1 contrast against layer-default (#181c20) */ + --theme-color-layer--alert: #e88b8b; /** 7:1 contrast against layer-default (#181c20) */ + --theme-color-layer--info: #82a8d3; /** 7:1 contrast against layer-default (#181c20) */ + --theme-color-layer--success: #68b392; /** 7:1 contrast against layer-default (#181c20) */ + --theme-color-layer: #181c20; /** 1:1 contrast against layer-default (#181c20) */ + --theme-color-ui--border: #343b40; /** 1.4:1 contrast against ui-active (#1e2327) */ + --theme-color-ui--text: #bfc2c4; /** 9:1 contrast against ui-active (#1e2327) */ + --theme-color-ui--muted: #93a0a7; /** 6:1 contrast against ui-active (#1e2327) */ + --theme-color-ui: #101315; /** -1.15:1 contrast against ui-active (#1e2327) */ + --theme-color-ui-hover: #171b1e; /** -1.08:1 contrast against ui-active (#1e2327) */ + --theme-color-ui-active: #1e2327; /** 1:1 contrast against ui-active (#1e2327) */ + --theme-color-ui-accent--border: #8b979d; /** 1.1:1 contrast against ui-accent-active (#909ea5) */ + --theme-color-ui-accent--text: #000000; /** 9:1 contrast against ui-accent-active (#909ea5) */ + --theme-color-ui-accent--muted: #1c2023; /** 6:1 contrast against ui-accent-active (#909ea5) */ + --theme-color-ui-accent: #9cabb3; /** -1.15:1 contrast against ui-accent-active (#909ea5) */ + --theme-color-ui-accent-hover: #97a6ad; /** -1.08:1 contrast against ui-accent-active (#909ea5) */ + --theme-color-ui-accent-active: #909ea5; /** 1:1 contrast against ui-accent-active (#909ea5) */ + --theme-color-ui-brand--border: #618996; /** 1.1:1 contrast against ui-brand-active (#62909f) */ + --theme-color-ui-brand--text: #000000; /** 9:1 contrast against ui-brand-active (#62909f) */ + --theme-color-ui-brand--muted: #010203; /** 6:1 contrast against ui-brand-active (#62909f) */ + --theme-color-ui-brand: #6b9dad; /** -1.15:1 contrast against ui-brand-active (#62909f) */ + --theme-color-ui-brand-hover: #6797a7; /** -1.08:1 contrast against ui-brand-active (#62909f) */ + --theme-color-ui-brand-active: #62909f; /** 1:1 contrast against ui-brand-active (#62909f) */ + --theme-color-ui-alert--border: #ce6565; /** 1.1:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert--text: #000000; /** 9:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert--muted: #100404; /** 6:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert: #e47878; /** -1.15:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert-hover: #e26f6f; /** -1.08:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert-active: #df6666; /** 1:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-info--border: #6885a8; /** 1.1:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info--text: #000000; /** 9:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info--muted: #010206; /** 6:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info: #7399c4; /** -1.15:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info-hover: #6f93bc; /** -1.08:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info-active: #6a8cb3; /** 1:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-success--border: #588e76; /** 1.1:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success--text: #000000; /** 9:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success--muted: #000403; /** 6:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success: #5fa385; /** -1.15:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success-hover: #5c9e81; /** -1.08:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success-active: #58977b; /** 1:1 contrast against ui-success-active (#58977b) */ +} diff --git a/design/tokens/themes/leo/light.css b/design/tokens/themes/leo/light.css new file mode 100644 index 0000000..0c9445e --- /dev/null +++ b/design/tokens/themes/leo/light.css @@ -0,0 +1,60 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +[data-theme="leo"][data-mode="light"] { + --theme-color-root--border: #f0f2f3; /** 1.05:1 contrast against root-default (#f5f7f8) */ + --theme-color-root--text: #4c5154; /** 7.5:1 contrast against root-default (#f5f7f8) */ + --theme-color-root--muted: #627076; /** 4.8:1 contrast against root-default (#f5f7f8) */ + --theme-color-root--accent: #46575f; /** 7:1 contrast against root-default (#f5f7f8) */ + --theme-color-root--brand: #365a66; /** 7:1 contrast against root-default (#f5f7f8) */ + --theme-color-root--alert: #903838; /** 7:1 contrast against root-default (#f5f7f8) */ + --theme-color-root--info: #3a5875; /** 7:1 contrast against root-default (#f5f7f8) */ + --theme-color-root--success: #2e5e48; /** 7:1 contrast against root-default (#f5f7f8) */ + --theme-color-root: #f5f7f8; /** 1:1 contrast against root-default (#f5f7f8) */ + --theme-color-layer--border: #f4f6f7; /** 1.05:1 contrast against layer-default (#fbfcfd) */ + --theme-color-layer--text: #4e5456; /** 7.5:1 contrast against layer-default (#fbfcfd) */ + --theme-color-layer--muted: #657278; /** 4.8:1 contrast against layer-default (#fbfcfd) */ + --theme-color-layer--accent: #495a62; /** 7:1 contrast against layer-default (#fbfcfd) */ + --theme-color-layer--brand: #375c68; /** 7:1 contrast against layer-default (#fbfcfd) */ + --theme-color-layer--alert: #933a3a; /** 7:1 contrast against layer-default (#fbfcfd) */ + --theme-color-layer--info: #3b5a78; /** 7:1 contrast against layer-default (#fbfcfd) */ + --theme-color-layer--success: #2f6149; /** 7:1 contrast against layer-default (#fbfcfd) */ + --theme-color-layer: #fbfcfd; /** 1:1 contrast against layer-default (#fbfcfd) */ + --theme-color-ui--border: #e3e7e9; /** 1.08:1 contrast against ui-active (#edf0f1) */ + --theme-color-ui--text: #474d4f; /** 7.5:1 contrast against ui-active (#edf0f1) */ + --theme-color-ui--muted: #5e6b71; /** 4.8:1 contrast against ui-active (#edf0f1) */ + --theme-color-ui: #fbfcfc; /** -1.1:1 contrast against ui-active (#edf0f1) */ + --theme-color-ui-hover: #f3f6f7; /** -1.05:1 contrast against ui-active (#edf0f1) */ + --theme-color-ui-active: #edf0f1; /** 1:1 contrast against ui-active (#edf0f1) */ + --theme-color-ui-accent--border: #667b85; /** 1.1:1 contrast against ui-accent-active (#60757f) */ + --theme-color-ui-accent--text: #ffffff; /** 7.5:1 contrast against ui-accent-active (#60757f) */ + --theme-color-ui-accent--muted: #fcfdfd; /** 4.8:1 contrast against ui-accent-active (#60757f) */ + --theme-color-ui-accent: #556a73; /** -1.15:1 contrast against ui-accent-active (#60757f) */ + --theme-color-ui-accent-hover: #596f78; /** -1.08:1 contrast against ui-accent-active (#60757f) */ + --theme-color-ui-accent-active: #60757f; /** 1:1 contrast against ui-accent-active (#60757f) */ + --theme-color-ui-brand--border: #517e8d; /** 1.1:1 contrast against ui-brand-active (#487887) */ + --theme-color-ui-brand--text: #ffffff; /** 7.5:1 contrast against ui-brand-active (#487887) */ + --theme-color-ui-brand--muted: #fbfeff; /** 4.8:1 contrast against ui-brand-active (#487887) */ + --theme-color-ui-brand: #416e7c; /** -1.15:1 contrast against ui-brand-active (#487887) */ + --theme-color-ui-brand-hover: #447280; /** -1.08:1 contrast against ui-brand-active (#487887) */ + --theme-color-ui-brand-active: #487887; /** 1:1 contrast against ui-brand-active (#487887) */ + --theme-color-ui-alert--border: #b15353; /** 1.1:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert--text: #ffffff; /** 7.5:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert--muted: #feeded; /** 4.8:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert: #a34040; /** -1.15:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert-hover: #a84242; /** -1.08:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert-active: #b24646; /** 1:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-info--border: #567ba0; /** 1.1:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info--text: #ffffff; /** 7.5:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info--muted: #fafdff; /** 4.8:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info: #466a8d; /** -1.15:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info-hover: #496f94; /** -1.08:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info-active: #4d759c; /** 1:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-success--border: #468467; /** 1.1:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success--text: #ffffff; /** 7.5:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success--muted: #f9fefc; /** 4.8:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success: #377257; /** -1.15:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success-hover: #3a775b; /** -1.08:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success-active: #3d7d5f; /** 1:1 contrast against ui-success-active (#3d7d5f) */ +} diff --git a/design/tokens/themes/neo/dark.css b/design/tokens/themes/neo/dark.css new file mode 100644 index 0000000..44aa654 --- /dev/null +++ b/design/tokens/themes/neo/dark.css @@ -0,0 +1,60 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +[data-theme="neo"][data-mode="dark"] { + --theme-color-root--border: #2e323a; /** 1.4:1 contrast against root-default (#15171c) */ + --theme-color-root--text: #b6b7bb; /** 9:1 contrast against root-default (#15171c) */ + --theme-color-root--muted: #8f95a0; /** 6:1 contrast against root-default (#15171c) */ + --theme-color-root--accent: #9ea2a6; /** 7:1 contrast against root-default (#15171c) */ + --theme-color-root--brand: #8a9eed; /** 7:1 contrast against root-default (#15171c) */ + --theme-color-root--alert: #e78686; /** 7:1 contrast against root-default (#15171c) */ + --theme-color-root--info: #7da5d2; /** 7:1 contrast against root-default (#15171c) */ + --theme-color-root--success: #66b08f; /** 7:1 contrast against root-default (#15171c) */ + --theme-color-root: #15171c; /** 1:1 contrast against root-default (#15171c) */ + --theme-color-layer--border: #32353d; /** 1.4:1 contrast against layer-default (#191c20) */ + --theme-color-layer--text: #babbbf; /** 9:1 contrast against layer-default (#191c20) */ + --theme-color-layer--muted: #9298a4; /** 6:1 contrast against layer-default (#191c20) */ + --theme-color-layer--accent: #a2a5aa; /** 7:1 contrast against layer-default (#191c20) */ + --theme-color-layer--brand: #8ea1ed; /** 7:1 contrast against layer-default (#191c20) */ + --theme-color-layer--alert: #e88b8b; /** 7:1 contrast against layer-default (#191c20) */ + --theme-color-layer--info: #83a8d4; /** 7:1 contrast against layer-default (#191c20) */ + --theme-color-layer--success: #68b392; /** 7:1 contrast against layer-default (#191c20) */ + --theme-color-layer: #191c20; /** 1:1 contrast against layer-default (#191c20) */ + --theme-color-ui--border: #373b42; /** 1.4:1 contrast against ui-active (#20242a) */ + --theme-color-ui--text: #c2c4c7; /** 9:1 contrast against ui-active (#20242a) */ + --theme-color-ui--muted: #99a0ac; /** 6:1 contrast against ui-active (#20242a) */ + --theme-color-ui: #131519; /** -1.15:1 contrast against ui-active (#20242a) */ + --theme-color-ui-hover: #191c20; /** -1.08:1 contrast against ui-active (#20242a) */ + --theme-color-ui-active: #20242a; /** 1:1 contrast against ui-active (#20242a) */ + --theme-color-ui-accent--border: #a0a1a4; /** 1.1:1 contrast against ui-accent-active (#a6a9ae) */ + --theme-color-ui-accent--text: #000001; /** 9:1 contrast against ui-accent-active (#a6a9ae) */ + --theme-color-ui-accent--muted: #262c37; /** 6:1 contrast against ui-accent-active (#a6a9ae) */ + --theme-color-ui-accent: #b4b7bd; /** -1.15:1 contrast against ui-accent-active (#a6a9ae) */ + --theme-color-ui-accent-hover: #aeb1b6; /** -1.08:1 contrast against ui-accent-active (#a6a9ae) */ + --theme-color-ui-accent-active: #a6a9ae; /** 1:1 contrast against ui-accent-active (#a6a9ae) */ + --theme-color-ui-brand--border: #5f7ce8; /** 1.1:1 contrast against ui-brand-active (#6a84e8) */ + --theme-color-ui-brand--text: #000000; /** 9:1 contrast against ui-brand-active (#6a84e8) */ + --theme-color-ui-brand--muted: #020308; /** 6:1 contrast against ui-brand-active (#6a84e8) */ + --theme-color-ui-brand: #7a91ea; /** -1.15:1 contrast against ui-brand-active (#6a84e8) */ + --theme-color-ui-brand-hover: #738ce9; /** -1.08:1 contrast against ui-brand-active (#6a84e8) */ + --theme-color-ui-brand-active: #6a84e8; /** 1:1 contrast against ui-brand-active (#6a84e8) */ + --theme-color-ui-alert--border: #ce6565; /** 1.1:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert--text: #000000; /** 9:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert--muted: #100404; /** 6:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert: #e47878; /** -1.15:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert-hover: #e26f6f; /** -1.08:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert-active: #df6666; /** 1:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-info--border: #6885a8; /** 1.1:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info--text: #000000; /** 9:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info--muted: #010206; /** 6:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info: #7399c4; /** -1.15:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info-hover: #6f93bc; /** -1.08:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info-active: #6a8cb3; /** 1:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-success--border: #588e76; /** 1.1:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success--text: #000000; /** 9:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success--muted: #000403; /** 6:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success: #5fa385; /** -1.15:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success-hover: #5c9e81; /** -1.08:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success-active: #58977b; /** 1:1 contrast against ui-success-active (#58977b) */ +} diff --git a/design/tokens/themes/neo/light.css b/design/tokens/themes/neo/light.css new file mode 100644 index 0000000..ce04dbb --- /dev/null +++ b/design/tokens/themes/neo/light.css @@ -0,0 +1,60 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +[data-theme="neo"][data-mode="light"] { + --theme-color-root--border: #f1f2f4; /** 1.05:1 contrast against root-default (#f5f7fa) */ + --theme-color-root--text: #4e5155; /** 7.5:1 contrast against root-default (#f5f7fa) */ + --theme-color-root--muted: #686e7c; /** 4.8:1 contrast against root-default (#f5f7fa) */ + --theme-color-root--accent: #4c5663; /** 7:1 contrast against root-default (#f5f7fa) */ + --theme-color-root--brand: #314bb6; /** 7:1 contrast against root-default (#f5f7fa) */ + --theme-color-root--alert: #903838; /** 7:1 contrast against root-default (#f5f7fa) */ + --theme-color-root--info: #3a5875; /** 7:1 contrast against root-default (#f5f7fa) */ + --theme-color-root--success: #2e5e48; /** 7:1 contrast against root-default (#f5f7fa) */ + --theme-color-root: #f5f7fa; /** 1:1 contrast against root-default (#f5f7fa) */ + --theme-color-layer--border: #ecedf0; /** 1.05:1 contrast against layer-default (#f1f2f3) */ + --theme-color-layer--text: #4b4e53; /** 7.5:1 contrast against layer-default (#f1f2f3) */ + --theme-color-layer--muted: #656b78; /** 4.8:1 contrast against layer-default (#f1f2f3) */ + --theme-color-layer--accent: #4a5361; /** 7:1 contrast against layer-default (#f1f2f3) */ + --theme-color-layer--brand: #2f49b0; /** 7:1 contrast against layer-default (#f1f2f3) */ + --theme-color-layer--alert: #8b3737; /** 7:1 contrast against layer-default (#f1f2f3) */ + --theme-color-layer--info: #385471; /** 7:1 contrast against layer-default (#f1f2f3) */ + --theme-color-layer--success: #2c5b46; /** 7:1 contrast against layer-default (#f1f2f3) */ + --theme-color-layer: #f1f2f3; /** 1:1 contrast against layer-default (#f1f2f3) */ + --theme-color-ui--border: #dbdee3; /** 1.08:1 contrast against ui-active (#e5e6e7) */ + --theme-color-ui--text: #45474c; /** 7.5:1 contrast against ui-active (#e5e6e7) */ + --theme-color-ui--muted: #5e6470; /** 4.8:1 contrast against ui-active (#e5e6e7) */ + --theme-color-ui: #f1f2f4; /** -1.1:1 contrast against ui-active (#e5e6e7) */ + --theme-color-ui-hover: #ebecee; /** -1.05:1 contrast against ui-active (#e5e6e7) */ + --theme-color-ui-active: #e5e6e7; /** 1:1 contrast against ui-active (#e5e6e7) */ + --theme-color-ui-accent--border: #5e6470; /** 1.1:1 contrast against ui-accent-active (#555f6c) */ + --theme-color-ui-accent--text: #ffffff; /** 7.5:1 contrast against ui-accent-active (#555f6c) */ + --theme-color-ui-accent--muted: #d9dde2; /** 4.8:1 contrast against ui-accent-active (#555f6c) */ + --theme-color-ui-accent: #4b5563; /** -1.15:1 contrast against ui-accent-active (#555f6c) */ + --theme-color-ui-accent-hover: #4f5967; /** -1.08:1 contrast against ui-accent-active (#555f6c) */ + --theme-color-ui-accent-active: #555f6c; /** 1:1 contrast against ui-accent-active (#555f6c) */ + --theme-color-ui-brand--border: #4766e0; /** 1.1:1 contrast against ui-brand-active (#3f5edc) */ + --theme-color-ui-brand--text: #ffffff; /** 7.5:1 contrast against ui-brand-active (#3f5edc) */ + --theme-color-ui-brand--muted: #ecf0ff; /** 4.8:1 contrast against ui-brand-active (#3f5edc) */ + --theme-color-ui-brand: #3755cd; /** -1.15:1 contrast against ui-brand-active (#3f5edc) */ + --theme-color-ui-brand-hover: #3958d5; /** -1.08:1 contrast against ui-brand-active (#3f5edc) */ + --theme-color-ui-brand-active: #3f5edc; /** 1:1 contrast against ui-brand-active (#3f5edc) */ + --theme-color-ui-alert--border: #b15353; /** 1.1:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert--text: #ffffff; /** 7.5:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert--muted: #feeded; /** 4.8:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert: #a34040; /** -1.15:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert-hover: #a84242; /** -1.08:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert-active: #b24646; /** 1:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-info--border: #567ba0; /** 1.1:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info--text: #ffffff; /** 7.5:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info--muted: #fafdff; /** 4.8:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info: #466a8d; /** -1.15:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info-hover: #496f94; /** -1.08:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info-active: #4d759c; /** 1:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-success--border: #468467; /** 1.1:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success--text: #ffffff; /** 7.5:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success--muted: #f9fefc; /** 4.8:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success: #377257; /** -1.15:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success-hover: #3a775b; /** -1.08:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success-active: #3d7d5f; /** 1:1 contrast against ui-success-active (#3d7d5f) */ +} diff --git a/design/tokens/themes/uno/dark.css b/design/tokens/themes/uno/dark.css new file mode 100644 index 0000000..a97c03b --- /dev/null +++ b/design/tokens/themes/uno/dark.css @@ -0,0 +1,60 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +[data-theme="uno"][data-mode="dark"] { + --theme-color-root--border: #332f3d; /** 1.4:1 contrast against root-default (#17141c) */ + --theme-color-root--text: #b7b5bc; /** 9:1 contrast against root-default (#17141c) */ + --theme-color-root--muted: #9791a4; /** 6:1 contrast against root-default (#17141c) */ + --theme-color-root--accent: #a59cb4; /** 7:1 contrast against root-default (#17141c) */ + --theme-color-root--brand: #a79bbd; /** 7:1 contrast against root-default (#17141c) */ + --theme-color-root--alert: #e68585; /** 7:1 contrast against root-default (#17141c) */ + --theme-color-root--info: #7ca3d1; /** 7:1 contrast against root-default (#17141c) */ + --theme-color-root--success: #65ae8e; /** 7:1 contrast against root-default (#17141c) */ + --theme-color-root: #17141c; /** 1:1 contrast against root-default (#17141c) */ + --theme-color-layer--border: #373341; /** 1.4:1 contrast against layer-default (#1c1a25) */ + --theme-color-layer--text: #bcbac1; /** 9:1 contrast against layer-default (#1c1a25) */ + --theme-color-layer--muted: #9b95a8; /** 6:1 contrast against layer-default (#1c1a25) */ + --theme-color-layer--accent: #aaa1b9; /** 7:1 contrast against layer-default (#1c1a25) */ + --theme-color-layer--brand: #ab9fc0; /** 7:1 contrast against layer-default (#1c1a25) */ + --theme-color-layer--alert: #e88b8b; /** 7:1 contrast against layer-default (#1c1a25) */ + --theme-color-layer--info: #82a8d4; /** 7:1 contrast against layer-default (#1c1a25) */ + --theme-color-layer--success: #68b392; /** 7:1 contrast against layer-default (#1c1a25) */ + --theme-color-layer: #1c1a25; /** 1:1 contrast against layer-default (#1c1a25) */ + --theme-color-ui--border: #3c3747; /** 1.4:1 contrast against ui-active (#23202d) */ + --theme-color-ui--text: #c3c0c8; /** 9:1 contrast against ui-active (#23202d) */ + --theme-color-ui--muted: #a19bad; /** 6:1 contrast against ui-active (#23202d) */ + --theme-color-ui: #131118; /** -1.15:1 contrast against ui-active (#23202d) */ + --theme-color-ui-hover: #1a1721; /** -1.08:1 contrast against ui-active (#23202d) */ + --theme-color-ui-active: #23202d; /** 1:1 contrast against ui-active (#23202d) */ + --theme-color-ui-accent--border: #9891a5; /** 1.1:1 contrast against ui-accent-active (#a199af) */ + --theme-color-ui-accent--text: #000000; /** 9:1 contrast against ui-accent-active (#a199af) */ + --theme-color-ui-accent--muted: #211e27; /** 6:1 contrast against ui-accent-active (#a199af) */ + --theme-color-ui-accent: #aea4bd; /** -1.15:1 contrast against ui-accent-active (#a199af) */ + --theme-color-ui-accent-hover: #a9a0b8; /** -1.08:1 contrast against ui-accent-active (#a199af) */ + --theme-color-ui-accent-active: #a199af; /** 1:1 contrast against ui-accent-active (#a199af) */ + --theme-color-ui-brand--border: #9081aa; /** 1.1:1 contrast against ui-brand-active (#9788b1) */ + --theme-color-ui-brand--text: #000000; /** 9:1 contrast against ui-brand-active (#9788b1) */ + --theme-color-ui-brand--muted: #0f0d13; /** 6:1 contrast against ui-brand-active (#9788b1) */ + --theme-color-ui-brand: #a295b9; /** -1.15:1 contrast against ui-brand-active (#9788b1) */ + --theme-color-ui-brand-hover: #9d90b6; /** -1.08:1 contrast against ui-brand-active (#9788b1) */ + --theme-color-ui-brand-active: #9788b1; /** 1:1 contrast against ui-brand-active (#9788b1) */ + --theme-color-ui-alert--border: #ce6565; /** 1.1:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert--text: #000000; /** 9:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert--muted: #100404; /** 6:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert: #e47878; /** -1.15:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert-hover: #e26f6f; /** -1.08:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-alert-active: #df6666; /** 1:1 contrast against ui-alert-active (#df6666) */ + --theme-color-ui-info--border: #6885a8; /** 1.1:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info--text: #000000; /** 9:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info--muted: #010206; /** 6:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info: #7399c4; /** -1.15:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info-hover: #6f93bc; /** -1.08:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-info-active: #6a8cb3; /** 1:1 contrast against ui-info-active (#6a8cb3) */ + --theme-color-ui-success--border: #588e76; /** 1.1:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success--text: #000000; /** 9:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success--muted: #000403; /** 6:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success: #5fa385; /** -1.15:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success-hover: #5c9e81; /** -1.08:1 contrast against ui-success-active (#58977b) */ + --theme-color-ui-success-active: #58977b; /** 1:1 contrast against ui-success-active (#58977b) */ +} diff --git a/design/tokens/themes/uno/light.css b/design/tokens/themes/uno/light.css new file mode 100644 index 0000000..fd1b14f --- /dev/null +++ b/design/tokens/themes/uno/light.css @@ -0,0 +1,60 @@ +/** + * Do not edit directly, this file was auto-generated. + */ + +[data-theme="uno"][data-mode="light"] { + --theme-color-root--border: #ebe9ee; /** 1.08:1 contrast against root-default (#f3f2f5) */ + --theme-color-root--text: #42404f; /** 9:1 contrast against root-default (#f3f2f5) */ + --theme-color-root--muted: #505d6f; /** 6:1 contrast against root-default (#f3f2f5) */ + --theme-color-root--accent: #594d67; /** 7:1 contrast against root-default (#f3f2f5) */ + --theme-color-root--brand: #594b75; /** 7:1 contrast against root-default (#f3f2f5) */ + --theme-color-root--alert: #8b3737; /** 7:1 contrast against root-default (#f3f2f5) */ + --theme-color-root--info: #385471; /** 7:1 contrast against root-default (#f3f2f5) */ + --theme-color-root--success: #2c5b45; /** 7:1 contrast against root-default (#f3f2f5) */ + --theme-color-root: #f3f2f5; /** 1:1 contrast against root-default (#f3f2f5) */ + --theme-color-layer--border: #e0dee4; /** 1.08:1 contrast against layer-default (#e5e6e7) */ + --theme-color-layer--text: #3b3948; /** 9:1 contrast against layer-default (#e5e6e7) */ + --theme-color-layer--muted: #4a5667; /** 6:1 contrast against layer-default (#e5e6e7) */ + --theme-color-layer--accent: #52475e; /** 7:1 contrast against layer-default (#e5e6e7) */ + --theme-color-layer--brand: #52456b; /** 7:1 contrast against layer-default (#e5e6e7) */ + --theme-color-layer--alert: #803232; /** 7:1 contrast against layer-default (#e5e6e7) */ + --theme-color-layer--info: #334e67; /** 7:1 contrast against layer-default (#e5e6e7) */ + --theme-color-layer--success: #285340; /** 7:1 contrast against layer-default (#e5e6e7) */ + --theme-color-layer: #e5e6e7; /** 1:1 contrast against layer-default (#e5e6e7) */ + --theme-color-ui--border: #d3d0d8; /** 1.1:1 contrast against ui-active (#dbdade) */ + --theme-color-ui--text: #403e4d; /** 7.5:1 contrast against ui-active (#dbdade) */ + --theme-color-ui--muted: #515d70; /** 4.8:1 contrast against ui-active (#dbdade) */ + --theme-color-ui: #e6e5e9; /** -1.1:1 contrast against ui-active (#dbdade) */ + --theme-color-ui-hover: #e2e1e5; /** -1.05:1 contrast against ui-active (#dbdade) */ + --theme-color-ui-active: #dbdade; /** 1:1 contrast against ui-active (#dbdade) */ + --theme-color-ui-accent--border: #bdb3c8; /** 1.1:1 contrast against ui-accent-active (#c4bccc) */ + --theme-color-ui-accent--text: #2d2d2d; /** 7.5:1 contrast against ui-accent-active (#c4bccc) */ + --theme-color-ui-accent--muted: #4b4a4c; /** 4.8:1 contrast against ui-accent-active (#c4bccc) */ + --theme-color-ui-accent: #d1cbd8; /** -1.15:1 contrast against ui-accent-active (#c4bccc) */ + --theme-color-ui-accent-hover: #ccc5d3; /** -1.08:1 contrast against ui-accent-active (#c4bccc) */ + --theme-color-ui-accent-active: #c4bccc; /** 1:1 contrast against ui-accent-active (#c4bccc) */ + --theme-color-ui-brand--border: #78699a; /** 1.1:1 contrast against ui-brand-active (#716292) */ + --theme-color-ui-brand--text: #ffffff; /** 7.5:1 contrast against ui-brand-active (#716292) */ + --theme-color-ui-brand--muted: #f2effe; /** 4.8:1 contrast against ui-brand-active (#716292) */ + --theme-color-ui-brand: #685889; /** -1.15:1 contrast against ui-brand-active (#716292) */ + --theme-color-ui-brand-hover: #6c5c8e; /** -1.08:1 contrast against ui-brand-active (#716292) */ + --theme-color-ui-brand-active: #716292; /** 1:1 contrast against ui-brand-active (#716292) */ + --theme-color-ui-alert--border: #b15353; /** 1.1:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert--text: #ffffff; /** 7.5:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert--muted: #feeded; /** 4.8:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert: #a34040; /** -1.15:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert-hover: #a84242; /** -1.08:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-alert-active: #b24646; /** 1:1 contrast against ui-alert-active (#b24646) */ + --theme-color-ui-info--border: #567ba0; /** 1.1:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info--text: #ffffff; /** 7.5:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info--muted: #fafdff; /** 4.8:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info: #466a8d; /** -1.15:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info-hover: #496f94; /** -1.08:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-info-active: #4d759c; /** 1:1 contrast against ui-info-active (#4d759c) */ + --theme-color-ui-success--border: #468467; /** 1.1:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success--text: #ffffff; /** 7.5:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success--muted: #f9fefc; /** 4.8:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success: #377257; /** -1.15:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success-hover: #3a775b; /** -1.08:1 contrast against ui-success-active (#3d7d5f) */ + --theme-color-ui-success-active: #3d7d5f; /** 1:1 contrast against ui-success-active (#3d7d5f) */ +} diff --git a/design/utilities.css b/design/utilities.css new file mode 100644 index 0000000..dcb46f3 --- /dev/null +++ b/design/utilities.css @@ -0,0 +1,379 @@ +@utility ui-root { + display: flex; + flex-direction: column; + width: 100%; + max-width: var(--container-ui); + gap: var(--spacing-ui-gap); + + &[data-orientation="vertical"] { + flex-direction: column; + } + + &[data-orientation="horizontal"] { + flex-flow: row nowrap; + width: fit-content; + } +} + +@utility ui-trigger { + display: inline-flex; + align-items: center; + justify-content: center; + text-align: center; + cursor: pointer; + width: auto; + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + font-weight: var(--font-weight-ui); + border-radius: var(--radius-ui); + border: 1px solid var(--color-ui--border); + padding-inline: var(--spacing-ui-padding); + gap: var(--spacing-ui-gap); + min-height: var(--spacing-ui); + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; + color: var(--color-ui--text); + background-color: var(--color-ui); + appearance: none; + + + &:hover { + background-color: var(--color-ui-hover); + } + + &:active { + background-color: var(--color-ui-active); + } + + &:focus-visible { + outline: 2px solid var(--color-ui--text); + outline-offset: -4px; + } + + &:disabled, + &[data-disabled], + &[data-disabled="true"], + &[disabled="true"] { + color: var(--color-ui--muted); + background-color: --alpha(var(--color-ui) / 60%); + cursor: not-allowed; + } + + & .icon, + & svg, + & img { + @apply ui-icon; + } +} + +@utility ui-trigger--* { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + padding-inline: --value(--spacing-ui-padding-*, [length]); + gap: --value(--spacing-ui-gap-*, [length]); + + &:hover { + background-color: --value(--color-ui-* -hover, [color]); + } + + &:active { + background-color: --value(--color-ui-* -active, [color]); + } + + &:focus-visible { + outline-color: --value(--color-ui-* --text, [color]); + } + + &:disabled, + &[data-disabled], + &[data-disabled="true"], + &[disabled="true"] { + color: --value(--color-ui-* --muted, [color]); + background-color: --alpha(--value(--color-ui-*, [color]) / 60%); + } +} + +@utility ui-trigger--square { + display: inline-flex; + aspect-ratio: 1 / 1; + justify-content: center; + align-items: center; + width: auto; + padding: 0; +} + +@utility ui-trigger--circle { + display: inline-flex; + aspect-ratio: 1 / 1; + justify-content: center; + align-items: center; + width: auto; + padding: 0; + border-radius: var(--radius-full) !important; +} + +@utility ui-icon { + display: inline-block; + align-items: center; + justify-content: center; + height: 0.9em !important; + width: 0.9em !important; + color: currentcolor; + + [dir="rtl"] & { + transform: scaleX(-1); + } +} + +@utility ui-icon--* { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + height: --value(--spacing-ui-*, [length])!important; + width: --value(--spacing-ui-*, [length])!important; +} + +@utility ui-content { + display: flex; + flex-direction: column; + width: auto; + padding: var(--spacing-ui-padding); + border-radius: var(--radius-ui); + border: 1px solid var(--color-layer--border); + background-color: var(--color-layer); + color: var(--color-layer--text); +} + +@utility ui-label { + display: inline-flex; + align-items: start; + justify-content: start; + text-align: start; + width: auto; + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + font-weight: var(--font-weight-ui); + gap: var(--spacing-ui-gap); + color: var(--color-ui--text); +} + +@utility ui-input { + display: inline-flex; + align-items: center; + justify-content: center; + text-align: start; + width: auto; + max-width: var(--container-mini); + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + font-weight: var(--font-weight-ui); + border-radius: var(--radius-ui); + border: 1px solid var(--color-ui--border); + padding-inline: var(--spacing-ui-padding); + gap: var(--spacing-ui-gap); + min-height: var(--spacing-ui); + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; + color: var(--color-ui--text); + background-color: var(--color-ui); + + &::placeholder { + color: var(--color-ui--muted); + } + + &:focus, + &:hover { + background-color: var(--color-root); + color: var(--color-root--text); + } + + &:focus-visible { + outline: 2px solid var(--color-ui--text); + outline-offset: -4px; + } + + &:focus-within { + outline: none; + border: 1px solid var(--color-ui--muted); + } + + &:disabled, + &[data-disabled], + &[data-disabled="true"], + &[disabled="true"] { + color: var(--color-ui--muted); + background-color: --alpha(var(--color-ui) / 60%); + cursor: not-allowed; + } + + & .icon, + & svg, + & img { + @apply ui-icon; + } +} + +@utility ui-item { + width: 100%; + display: inline-flex; + align-items: center; + text-align: start; + cursor: pointer; + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + font-weight: var(--font-weight-ui); + padding-inline: var(--spacing-ui-padding); + gap: var(--spacing-ui-gap); + min-height: var(--spacing-ui); + background-color: var(--color-ui); + color: var(--color-ui--text); + border-radius: var(--radius-none); + outline-offset: calc(var(--spacing-micro-sm) * -1); + + &:disabled, + &[data-disabled], + &[data-disabled="true"], + &[disabled="true"] { + color: var(--color-ui--muted); + background-color: --alpha(var(--color-ui) / 60%); + cursor: not-allowed; + } + + &[data-selected], + &[data-in-range], + &[data-highlighted], + &[data-state="checked"], + &[data-state="on"], + &[data-state="open"], + &:hover, + &:active { + background-color: var(--color-ui-accent); + color: var(--color-ui-accent--text); + } + &[disabled][data-selected], + &[disabled][data-in-range], + &[disabled][data-highlighted], + &[disabled][data-state="checked"], + &[disabled][data-state="on"], + &[disabled][data-state="open"] { + background-color: --alpha(var(--color-ui-accent) / 60%); + color: var(--color-ui-accent--muted); + } + + &[data-highlighted]:not(:hover) { + outline: 2px solid var(--color-ui--text); + } + + & [data-part="branch-indicator"][data-state="open"], + & [data-part="item-indicator"][data-state="open"] { + transform: rotate(90deg); + } + &[dir="rtl"] [data-part="branch-indicator"], + &[dir="rtl"] [data-part="item-indicator"] { + transform: scaleX(-1); + } + + &[dir="rtl"] [data-part="branch-indicator"][data-state="open"], + &[dir="rtl"] [data-part="item-indicator"][data-state="open"] { + transform: rotate(90deg); + } + + & [data-part="item-indicator"] svg, + & [data-part="branch-indicator"] svg, + & svg, + & .indicator, + & [data-part="item-indicator"] .indicator, + & [data-part="branch-indicator"] .indicator, + & .icon, + & img { + @apply ui-icon; + } + + & [data-part="item-text"] { + display: flex; + gap: var(--spacing-ui-gap); + width: 100%; + text-align: start; + } +} + +@utility ui-item--* { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + padding-inline: --value(--spacing-ui-padding-*, [length]); + gap: --value(--spacing-ui-gap-*, [length]); + + &:disabled, + &[data-disabled], + &[data-disabled="true"], + &[disabled="true"] { + background-color: --alpha(--value(--color-ui-*, [color]) / 60%); + color: --value(--color-ui-* --muted, [color]); + } + + &[data-selected], + &[data-in-range], + &[data-highlighted], + &[data-state="checked"], + &[data-state="on"], + &[data-state="open"], + &:hover, + &:active { + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + &:disabled, + &[data-disabled], + &[data-disabled="true"], + &[disabled="true"] { + background-color: --alpha(--value(--color-ui-*, [color]) / 60%); + color: --value(--color-ui-* --muted, [color]); + } + } + + &[data-highlighted]:not(:hover) { + outline: 2px solid --value(--color-ui-* --border, [color]); + } +} + +@utility ui-link { + display: inline-flex; + justify-content: start; + align-items: center; + cursor: pointer; + position: relative; + width: auto; + color: var(--color-layer--text); + height: inherit; + font-size: inherit; + line-height: inherit; + gap: var(--spacing-mini-gap); + padding-inline: var(--spacing-mini-padding); + border-radius: var(--radius-ui); + text-decoration: none; + text-wrap-mode: nowrap; + + &:hover { + color: var(--color-layer--brand); + } + +} +@utility ui-error { + display: inline-flex; + align-items: center; + justify-content: start; + text-align: start; + width: auto; + font-size: var(--text-ui-sm); + line-height: var(--text-ui-sm--line-height); + font-weight: var(--font-weight-ui); + padding-inline: var(--spacing-ui-padding-sm); + gap: var(--spacing-ui-gap-sm); + color: var(--color-root--alert); +} diff --git a/e2e/assets/corex/components/angle-slider.css b/e2e/assets/corex/components/angle-slider.css new file mode 100644 index 0000000..deb2124 --- /dev/null +++ b/e2e/assets/corex/components/angle-slider.css @@ -0,0 +1,214 @@ +@import "../main.css"; + +@layer components { + .angle-slider [data-scope="angle-slider"][data-part="root"] { + @apply ui-root; + + max-width: var(--container-micro); + gap: var(--spacing-ui-gap); + padding: var(--spacing-ui-padding); + background-color: var(--color-root); + color: var(--color-root--text); + border-radius: var(--radius-ui); + border: 1px solid var(--color-root--border); + } + + .angle-slider [data-scope="angle-slider"][data-part="label"] { + @apply ui-label; + } + + .angle-slider [data-scope="angle-slider"][data-part="control"] { + --size: calc(var(--spacing-ui) * 2); + --thumb-size: var(--spacing-ui); + --thumb-indicator-size: min(var(--thumb-size), calc(var(--size) / 2)); + + width: var(--size); + height: var(--size); + border-radius: var(--radius-full); + border: 1px solid var(--color-ui--border); + background-color: var(--color-ui); + margin-inline: auto; + display: flex; + align-items: center; + justify-content: center; + user-select: none; + position: relative; + } + + .angle-slider [data-scope="angle-slider"][data-part="thumb"] { + position: absolute; + inset: 0 0 0 calc(50% - 1px); + pointer-events: none; + width: var(--spacing-micro-sm); + color: var(--color-ui--text); + } + + .angle-slider [data-scope="angle-slider"][data-part="thumb"]:focus-visible { + outline: none; + } + + .angle-slider [data-scope="angle-slider"][data-part="thumb"]::before { + content: ""; + position: absolute; + right: 0; + top: 0; + height: var(--thumb-indicator-size); + width: var(--spacing-micro-sm); + background-color: var(--color-ui--text); + border-radius: var(--radius-ui); + } + + .angle-slider + [data-scope="angle-slider"][data-part="thumb"]:focus-visible::before { + outline: 1px solid var(--color-ui--text); + outline-offset: 2px; + } + + .angle-slider [data-scope="angle-slider"][data-part="marker-group"] { + position: absolute; + inset: 1px; + border-radius: var(--radius-full); + pointer-events: none; + } + + .angle-slider [data-scope="angle-slider"][data-part="marker"] { + width: 2px; + position: absolute; + top: 0; + bottom: 0; + left: calc(50% - 1px); + } + + .angle-slider [data-scope="angle-slider"][data-part="marker"]::before { + content: ""; + position: absolute; + top: calc(var(--thumb-size) / 4); + left: 0.5px; + width: 1px; + height: calc(var(--thumb-size) / 2); + transform: translate(-50%, -50%); + background-color: var(--marker-color); + border-radius: 1px; + } + + .angle-slider + [data-scope="angle-slider"][data-part="marker"][data-state="under-value"] { + --marker-color: var(--color-ui--text); + } + + .angle-slider + [data-scope="angle-slider"][data-part="marker"][data-state="under-value"]::before { + width: 3px; + } + + .angle-slider + [data-scope="angle-slider"][data-part="marker"][data-state="at-value"] { + display: none; + } + + .angle-slider + [data-scope="angle-slider"][data-part="marker"][data-state="over-value"] { + --marker-color: var(--color-ui--muted); + + width: var(--spacing-micro-sm); + } + + .angle-slider [data-scope="angle-slider"][data-part="value-text"] { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: var(--spacing-ui-gap-sm); + width: auto; + } + + .angle-slider + [data-scope="angle-slider"][data-part="value-text"] + [data-part="text"], + .angle-slider + [data-scope="angle-slider"][data-part="value-text"] + [data-part="value"] { + display: flex; + width: auto; + align-items: baseline; + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + font-weight: var(--font-weight-ui); + color: var(--color-ui--text); + } + + .angle-slider + [data-scope="angle-slider"][data-part="root"][data-invalid] + [data-scope="angle-slider"][data-part="value-text"] + [data-part="text"] { + color: var(--color-root--alert); + } + + .angle-slider + [data-scope="angle-slider"][data-part="root"][data-invalid] + [data-scope="angle-slider"][data-part="value-text"] + [data-part="value"] { + color: var(--color-root--alert); + } + + .angle-slider + [data-scope="angle-slider"][data-part="root"][data-invalid] + [data-scope="angle-slider"][data-part="thumb"]::before { + color: var(--color-root--alert); + } + + .angle-slider [data-scope="angle-slider"][data-part="hidden-input"] { + display: none; + } +} + +@utility angle-slider--* { + [data-scope="angle-slider"][data-part="root"] { + max-width: --value(--container-micro-*, [length]); + gap: --value(--spacing-micro-gap-*, [length]); + padding: --value(--spacing-micro-padding-*, [length]); + } + + [data-scope="angle-slider"][data-part="label"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + } + + [data-scope="angle-slider"][data-part="control"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + border-color: --value(--color-ui-* --border, [color]); + + --size: calc(--value(--spacing-ui-*, [length]) * 2); + --thumb-size: --value(--spacing-ui-*, [length]); + } + + [data-scope="angle-slider"][data-part="thumb"]::before { + background-color: --value(--color-ui-* --text, [color]); + } + + [data-scope="angle-slider"][data-part="marker"] { + --marker-color: --value(--color-ui-* --muted, [color]); + } + + [data-scope="angle-slider"][data-part="marker"][data-state="under-value"] { + --marker-color: --value(--color-ui-* --text, [color]); + } + + [data-scope="angle-slider"][data-part="marker"][data-state="over-value"] { + --marker-color: --value(--color-ui-* --muted, [color]); + } + + [data-scope="angle-slider"][data-part="value-text"] [data-part="value"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + } + + [data-scope="angle-slider"][data-part="value-text"] [data-part="text"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + } +} diff --git a/e2e/assets/corex/components/avatar.css b/e2e/assets/corex/components/avatar.css new file mode 100644 index 0000000..ff89902 --- /dev/null +++ b/e2e/assets/corex/components/avatar.css @@ -0,0 +1,90 @@ +@import "../main.css"; + +@layer components { + .avatar [data-scope="avatar"][data-part="root"] { + @apply ui-root; + + max-width: var(--spacing-ui); + } + + .avatar [data-scope="avatar"][data-part="fallback"] { + display: inline-flex; + align-items: center; + justify-content: center; + text-align: start; + width: var(--spacing-ui); + aspect-ratio: 1 / 1; + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + font-weight: var(--font-weight-ui); + border-radius: var(--radius-full); + border: 1px solid var(--color-ui--border); + gap: var(--spacing-ui-gap); + height: var(--spacing-ui); + color: var(--color-ui--text); + background-color: var(--color-ui); + } + + .avatar [data-scope="avatar"][data-part="image"] { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + } + + .avatar [data-scope="avatar"][data-part="skeleton"] { + display: block; + width: var(--spacing-ui); + aspect-ratio: 1 / 1; + border-radius: var(--radius-full); + background: linear-gradient( + 90deg, + var(--color-ui--muted) 0%, + var(--color-ui--border) 50%, + var(--color-ui--muted) 100% + ); + background-size: 200% 100%; + animation: avatar-skeleton-loading 1.2s ease-in-out infinite; + } + + .avatar [data-scope="avatar"][data-part="skeleton"][data-state="hidden"] { + display: none; + } + + .avatar.avatar--square [data-scope="avatar"][data-part="skeleton"] { + border-radius: var(--radius-ui); + } + + .avatar.avatar--square [data-scope="avatar"][data-part="root"] { + border-radius: var(--radius-ui); + } + + .avatar.avatar--square [data-scope="avatar"][data-part="fallback"] { + border-radius: var(--radius-ui); + } +} + +@keyframes avatar-skeleton-loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +@utility avatar--* { + [data-scope="avatar"][data-part="root"] { + max-width: --value(--spacing-ui-*, [length]); + } + + [data-scope="avatar"][data-part="fallback"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + width: --value(--spacing-ui-*, [length]); + height: --value(--spacing-ui-*, [length]); + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + border-color: --value(--color-ui-* --border, [color]); + } +} diff --git a/e2e/assets/corex/components/carousel.css b/e2e/assets/corex/components/carousel.css new file mode 100644 index 0000000..e6ad805 --- /dev/null +++ b/e2e/assets/corex/components/carousel.css @@ -0,0 +1,200 @@ +@import "../main.css"; + +@layer components { + .carousel [data-scope="carousel"][data-part="root"] { + @apply ui-root; + + max-width: var(--container-ui); + max-height: var(--container-ui); + gap: var(--spacing-ui-gap); + justify-content: center; + position: relative; + } + + .carousel [data-scope="carousel"][data-part="control"] { + display: flex; + position: absolute; + flex-direction: column; + width: auto; + padding: var(--spacing-mini-padding-sm); + max-width: var(--container-mini); + overflow: hidden; + gap: var(--spacing-mini-gap); + justify-content: center; + border-radius: var(--radius-ui); + + &[data-orientation="horizontal"] { + flex-direction: row; + bottom: var(--spacing-mini-padding); + } + + &[data-orientation="vertical"] { + flex-flow: column nowrap; + max-height: var(--container-mini); + right: var(--spacing-mini-padding); + + & [data-scope="carousel"][data-part="prev-trigger"], + & [data-scope="carousel"][data-part="next-trigger"], + & [data-scope="carousel"][data-part="autoplay-trigger"] { + transform: rotate(90deg); + } + } + } + + .carousel [data-scope="carousel"][data-part="prev-trigger"], + .carousel [data-scope="carousel"][data-part="next-trigger"], + .carousel [data-scope="carousel"][data-part="autoplay-trigger"] { + @apply ui-trigger; + + padding: 0; + aspect-ratio: 1/1; + gap: var(--spacing-min-gap); + min-height: var(--spacing-mini); + border: 0; + + &[disabled="true"] { + visibility: hidden; + } + } + + .carousel [data-scope="carousel"][data-part="item-group"] { + overflow: hidden; + align-self: stretch; + border-radius: var(--radius-ui); + scrollbar-width: none; + -webkit-scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } + } + + .carousel [data-scope="carousel"][data-part="item"] { + display: flex; + justify-content: center; + align-items: center; + font-size: 24px; + border: 1px solid var(--color-ui--border); + border-radius: var(--radius-ui); + } + + .carousel [data-scope="carousel"][data-part="item"] img { + margin: 0; + object-fit: cover; + border-radius: var(--radius-ui); + } + + .carousel + [data-scope="carousel"][data-part="item"][data-orientation="horizontal"] + img { + height: 100%; + width: 100%; + } + + .carousel + [data-scope="carousel"][data-part="item"][data-orientation="vertical"] + img { + height: 100%; + width: 100%; + } + + .carousel [data-scope="carousel"][data-part="indicator-group"] { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: var(--spacing-mini-gap); + + &[data-orientation="horizontal"] { + flex-direction: row; + height: auto; + } + + &[data-orientation="vertical"] { + flex-direction: column; + height: auto; + } + } + + .carousel [data-scope="carousel"][data-part="indicator"] { + @apply ui-trigger; + + min-height: var(--spacing-micro-xl); + min-width: var(--spacing-micro-xl); + aspect-ratio: 1/1; + border-radius: var(--radius-full); + background-color: var(--color-ui); + padding: 0; + transition: background-color 0.2s ease-in-out; + } + + .carousel [data-scope="carousel"][data-part="indicator"][data-current] { + background-color: var(--color-ui-accent); + } + + .carousel + [data-scope="carousel"][data-part="autoplay-trigger"][data-pressed] + [data-play] { + display: none; + } + + .carousel + [data-scope="carousel"][data-part="autoplay-trigger"]:not([data-pressed]) + [data-pause] { + display: none; + } +} + +@utility carousel--* { + [data-scope="carousel"][data-part="root"] { + max-width: --value(--container-ui-*, [length]); + max-height: --value(--container-ui-*, [length]); + gap: --value(--spacing-ui-gap-*, [length]); + } + + [data-scope="carousel"][data-part="control"] { + max-width: --value(--container-mini-*, [length]); + padding: --value(--spacing-mini-padding-*, [length]); + gap: --value(--spacing-mini-gap-*, [length]); + } + + [data-scope="carousel"][data-part="next-trigger"], + [data-scope="carousel"][data-part="prev-trigger"], + [data-scope="carousel"][data-part="autoplay-trigger"] { + gap: --value(--spacing-mini-gap-*, [length]); + min-height: --value(--spacing-mini-*, [length]); + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + border-color: --value(--color-ui-* --border, [color]); + } + + [data-scope="carousel"][data-part="next-trigger"]:hover, + [data-scope="carousel"][data-part="prev-trigger"]:hover, + [data-scope="carousel"][data-part="autoplay-trigger"]:hover { + background-color: --value(--color-ui-* -hover, [color]); + } + + [data-scope="carousel"][data-part="next-trigger"]:active, + [data-scope="carousel"][data-part="prev-trigger"]:active, + [data-scope="carousel"][data-part="autoplay-trigger"]:active { + background-color: --value(--color-ui-* -active, [color]); + } + + [data-scope="carousel"][data-part="next-trigger"]:focus-visible, + [data-scope="carousel"][data-part="prev-trigger"]:focus-visible, + [data-scope="carousel"][data-part="autoplay-trigger"]:focus-visible { + outline-color: --value(--color-ui-* --text, [color]); + } + + [data-scope="carousel"][data-part="indicator"] { + min-height: --value(--spacing-micro-*, [length]); + min-width: --value(--spacing-micro-*, [length]); + } + + [data-scope="carousel"][data-part="indicator"][data-current] { + background-color: --value(--color-ui-*, [color]); + } +} diff --git a/e2e/assets/corex/components/editable.css b/e2e/assets/corex/components/editable.css new file mode 100644 index 0000000..e569ed1 --- /dev/null +++ b/e2e/assets/corex/components/editable.css @@ -0,0 +1,111 @@ +@import "../main.css"; + +@layer components { + .editable [data-scope="editable"][data-part="root"] { + @apply ui-root; + + width: 100%; + max-width: var(--container-ui); + } + .editable [data-scope="editable"][data-part="control"] { + display: flex; + flex-direction: row; + gap: var(--spacing-mini-gap); + min-height: var(--spacing-ui); + width: 100%; + align-items: center; + justify-content: start; + max-width: var(--container-mini); + + } + + .editable [data-scope="editable"][data-part="area"] { + display: flex; + min-height: var(--spacing-ui); + flex: 1 1 auto; + align-items: center; + justify-content: start; + max-width: var(--container-mini); + gap: var(--spacing-mini-gap); + } + + .editable [data-scope="editable"][data-part="input"] { + @apply ui-input; + + flex: 1 1 auto; + width: 100%; + max-width: none; + } + + .editable [data-scope="editable"][data-part="preview"] { + @apply ui-input; + + flex: 1 1 auto; + width: 100%; + min-width: 0; + max-width: none; + text-wrap: nowrap; + } + + .editable [data-part="triggers"] { + display: inline-flex; + align-items: center; + min-height: var(--spacing-ui); + flex: 1 1 auto; + gap: var(--spacing-mini-gap); + } + + .editable [data-scope="editable"][data-part="edit-trigger"], + .editable [data-scope="editable"][data-part="cancel-trigger"], + .editable [data-scope="editable"][data-part="submit-trigger"] { + @apply ui-trigger ui-trigger--square; + } + + .editable [data-scope="editable"][data-part="cancel-trigger"] { + @apply ui-trigger--alert; + } + + .editable [data-scope="editable"][data-part="submit-trigger"] { + @apply ui-trigger--success; + } +} + +@utility editable--* { + [data-scope="editable"][data-part="root"] { + max-width: --value(--container-mini-*, [length]); + gap: --value(--spacing-mini-gap-*, [length]); + } + + [data-scope="editable"][data-part="edit-trigger"], + [data-scope="editable"][data-part="cancel-trigger"], + [data-scope="editable"][data-part="submit-trigger"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + padding-inline: --value(--spacing-ui-padding-*, [length]); + gap: --value(--spacing-ui-gap-*, [length]); + } + + [data-scope="editable"][data-part="edit-trigger"]::placeholder, + [data-scope="editable"][data-part="cancel-trigger"]::placeholder, + [data-scope="editable"][data-part="submit-trigger"]::placeholder { + color: --value(--color-root--*, [color]); + } + + /* [data-scope="editable"][data-part="area"] { + max-height: --value(--spacing-ui-*, [length]); + } */ + + [data-scope="editable"][data-part="preview"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + max-width: --value(--container-mini-*, [length]); + } + + [data-scope="editable"][data-part="input"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + } +} diff --git a/e2e/assets/corex/components/floating-panel.css b/e2e/assets/corex/components/floating-panel.css new file mode 100644 index 0000000..d0cd582 --- /dev/null +++ b/e2e/assets/corex/components/floating-panel.css @@ -0,0 +1,139 @@ +@import "../main.css"; +@import "./scrollbar.css"; + +@layer components { + .floating-panel [data-scope="floating-panel"][data-part="trigger"] { + @apply ui-trigger; + } + + .floating-panel + [data-scope="floating-panel"][data-part="trigger"][data-state="open"] + [data-closed] { + display: none; + } + + .floating-panel + [data-scope="floating-panel"][data-part="trigger"][data-state="closed"] + [data-open] { + display: none; + } + + .floating-panel [data-part="title"] { + @apply ui-label; + } + + .floating-panel [data-scope="floating-panel"][data-part="positioner"] { + z-index: 50; + } + + .floating-panel + [data-scope="floating-panel"][data-part="positioner"]:has( + [data-scope="floating-panel"][data-part="content"][data-state="closed"] + ) { + display: none; + } + + .floating-panel + [data-scope="floating-panel"][data-part="positioner"]:has( + [data-scope="floating-panel"][data-part="content"][data-topmost] + ) { + z-index: 999999; + } + + .floating-panel [data-scope="floating-panel"][data-part="content"] { + display: flex; + flex-direction: column; + z-index: var(--z-index); + position: relative; + min-height: var(--spacing-ui); + box-shadow: var(--shadow-ui); + + &[data-topmost] { + z-index: 999999; + } + + &[data-behind] { + opacity: 0.8; + } + + &:focus-visible { + outline: 2px solid var(--color-ui--text); + outline-offset: 0; + border-radius: var(--radius-ui); + } + } + + .floating-panel [data-scope="floating-panel"][data-part="stage-trigger"], + .floating-panel [data-scope="floating-panel"][data-part="close-trigger"] { + @apply ui-trigger ui-trigger--square ui-trigger--sm; + + min-height: var(--spacing-mini); + } + + [data-scope="floating-panel"][data-part="body"] { + @apply scrollbar scrollbar--sm; + + position: relative; + overflow: auto; + flex: 1 1 auto; + min-height: fit-content; + height: var(--height); + padding: var(--spacing-ui-padding); + background-color: var(--color-root); + border-radius: 0 0 var(--radius-ui) var(--radius-ui); + border: 1px solid var(--color-ui--border); + border-block-start: 0; + + & p { + margin-block: 0; + } + } + + [data-scope="floating-panel"][data-part="header"] { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + padding: var(--spacing-micro-padding); + gap: var(--spacing-mini-gap); + } + + [data-scope="floating-panel"][data-part="drag-trigger"] { + background-color: var(--color-layer); + border: 1px solid var(--color-ui--border); + border-radius: var(--radius-ui) var(--radius-ui) 0 0; + + &:focus-visible { + outline: none; + } + } + + [data-scope="floating-panel"][data-part="control"] { + display: flex; + flex-flow: row nowrap; + align-items: center; + gap: var(--spacing-mini-gap); + } + + [data-scope="floating-panel"][data-part="resize-trigger"] { + &[data-axis="n"], + &[data-axis="s"] { + height: 6px; + max-width: 90%; + } + + &[data-axis="e"], + &[data-axis="w"] { + width: 6px; + max-height: 90%; + } + + &[data-axis="ne"], + &[data-axis="nw"], + &[data-axis="se"], + &[data-axis="sw"] { + width: 10px; + height: 10px; + } + } +} diff --git a/e2e/assets/corex/components/layout.css b/e2e/assets/corex/components/layout.css index 0c2d88b..e4fc0f0 100644 --- a/e2e/assets/corex/components/layout.css +++ b/e2e/assets/corex/components/layout.css @@ -27,6 +27,11 @@ align-items: center; } + .layout__header--compact { + height: auto; + min-height: var(--spacing-ui); + } + .layout__header__content, .layout__footer__content, .layout__side__content { @@ -91,6 +96,7 @@ flex: 1; min-width: 0; width: 100%; + height: 100%; margin-inline: auto; margin-block-end: var(--container-micro); position: relative; @@ -114,6 +120,8 @@ align-items: center; padding: var(--spacing-ui-padding); gap: var(--spacing-ui-gap); + margin-block-end: var(--container-micro); + } .layout__article { diff --git a/e2e/assets/corex/components/listbox.css b/e2e/assets/corex/components/listbox.css new file mode 100644 index 0000000..5c046b8 --- /dev/null +++ b/e2e/assets/corex/components/listbox.css @@ -0,0 +1,117 @@ +@import "../main.css"; + +@layer components { + .listbox [data-scope="listbox"][data-part="root"] { + @apply ui-root; + + width: var(--container-mini); + gap: var(--spacing-mini-gap); + } + + .listbox [data-scope="listbox"][data-part="label"] { + @apply ui-label; + } + + .listbox [data-scope="listbox"][data-part="label"][data-disabled] { + color: var(--color-ui--muted); + } + + .listbox [data-scope="listbox"][data-part="item-group-label"] { + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + text-align: start; + padding-inline: var(--spacing-ui-padding); + padding-block: var(--spacing-mini-padding-sm); + background-color: var(--color-root); + color: var(--color-root--text); + border-bottom: 1px solid var(--color-ui--border); + } + + .listbox + [data-scope="listbox"][data-part="content"][data-layout="list"][data-orientation="vertical"] { + display: flex; + flex-direction: column; + } + + .listbox + [data-scope="listbox"][data-part="content"][data-layout="grid"]:not( + :has([data-part="item-group"]) + ) { + display: grid; + grid-template-columns: repeat(var(--column-count), 1fr); + } + + .listbox + [data-scope="listbox"][data-part="content"][data-layout="grid"] + [data-part="item-group"] { + display: grid; + grid-template-columns: repeat(var(--column-count), 1fr); + } + + .listbox [data-scope="listbox"][data-part="content"]:focus-visible { + outline: none; + } + + .listbox [data-scope="listbox"][data-part="item-group"] { + display: flex; + flex-direction: column; + margin-block: var(--spacing-micro); + } + + .listbox + [data-scope="listbox"][data-part="content"]:not( + :has([data-part="item-group"]) + ), + .listbox [data-scope="listbox"][data-part="item-group"] { + background-color: var(--color-ui--border); + border: 1px solid var(--color-ui--border); + border-radius: var(--radius-ui); + overflow: hidden; + gap: 1px; + } + + .listbox [data-scope="listbox"][data-part="item"] { + @apply ui-item; + } + + .listbox + [data-scope="listbox"][data-part="content"][dir="rtl"] + [data-scope="listbox"][data-part="item"] { + border-radius: 0; + } + + [dir="rtl"] .listbox [data-scope="listbox"][data-part="item-indicator"] { + transform: scaleX(-1); + } +} + +@utility listbox--* { + [data-scope="listbox"][data-part="root"] { + width: --value(--container-mini-*, [length]); + gap: --value(--spacing-mini-gap-*, [length]); + } + + [data-scope="listbox"][data-part="label"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + } + + [data-scope="listbox"][data-part="item"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + } + + [data-scope="listbox"][data-part="item"][data-selected] { + background-color: --value(--color-ui-*, [color]); + color: --value(--color-ui-* --text, [color]); + } + + [data-scope="listbox"][data-part="item"][data-selected]:hover { + background-color: --value(--color-ui-* -hover, [color]); + } + + [data-scope="listbox"][data-part="item"][data-selected]:active { + background-color: --value(--color-ui-* -active, [color]); + } +} diff --git a/e2e/assets/corex/components/number-input.css b/e2e/assets/corex/components/number-input.css new file mode 100644 index 0000000..f12f2f7 --- /dev/null +++ b/e2e/assets/corex/components/number-input.css @@ -0,0 +1,68 @@ +@import "../main.css"; + +@layer components { + .number-input [data-scope="number-input"][data-part="root"] { + @apply ui-root; + + gap: var(--spacing-ui-gap); + max-width: var(--container-ui); + } + + .number-input [data-scope="number-input"][data-part="label"] { + @apply ui-label; + } + + .number-input [data-scope="number-input"][data-part="control"] { + display: flex; + flex-flow: row nowrap; + align-items: stretch; + position: relative; + margin-inline: auto; + max-width: var(--container-micro); + } + + .number-input [data-scope="number-input"][data-part="scrubber"] svg { + transform: rotate(90deg); + } + + .number-input [data-scope="number-input"][data-part="input"] { + @apply ui-input; + + max-width: var(--container-micro); + flex: 1; + border-inline-end: 0; + border-end-end-radius: 0; + border-start-end-radius: 0; + } + + .number-input [data-part="trigger-group"] { + display: flex; + flex-direction: column; + gap: 0; + } + + .number-input [data-scope="number-input"][data-part="increment-trigger"], + .number-input [data-scope="number-input"][data-part="decrement-trigger"], + .number-input [data-scope="number-input"][data-part="scrubber"] { + @apply ui-trigger ui-trigger--square ui-trigger--sm; + + flex: 1; + min-height: 0; + padding: 0.25rem; + font-size: 0.625rem; + line-height: 1; + border-start-start-radius: 0; + border-end-start-radius: 0; + margin: 0; + } + + .number-input [data-scope="number-input"][data-part="increment-trigger"] { + border-end-end-radius: 0; + border-block-end: 0; + } + + .number-input [data-scope="number-input"][data-part="decrement-trigger"] { + border-start-end-radius: 0; + border-block-start: 0; + } +} diff --git a/e2e/assets/corex/components/password-input.css b/e2e/assets/corex/components/password-input.css new file mode 100644 index 0000000..d60554a --- /dev/null +++ b/e2e/assets/corex/components/password-input.css @@ -0,0 +1,54 @@ +@import "../main.css"; + +@layer components { + .password-input [data-scope="password-input"][data-part="root"] { + @apply ui-root; + + gap: var(--spacing-mini-gap); + max-width: var(--container-ui); + } + + .password-input [data-scope="password-input"][data-part="label"] { + @apply ui-label; + } + + .password-input [data-scope="password-input"][data-part="control"] { + display: flex; + flex-flow: row nowrap; + align-items: stretch; + position: relative; + margin-inline: auto; + max-width: var(--container-mini); + } + + .password-input [data-scope="password-input"][data-part="input"] { + @apply ui-input; + + max-width: var(--container-mini); + flex: 1; + border-inline-end: 0; + border-end-end-radius: 0; + border-start-end-radius: 0; + } + + .password-input + [data-scope="password-input"][data-part="indicator"][data-state="visible"] + [data-hidden] { + display: none; + } + + .password-input + [data-scope="password-input"][data-part="indicator"][data-state="hidden"] + [data-visible] { + display: none; + } + + .password-input + [data-scope="password-input"][data-part="visibility-trigger"] { + @apply ui-trigger ui-trigger--square; + + min-width: var(--spacing-ui); + border-start-start-radius: 0; + border-end-start-radius: 0; + } +} diff --git a/e2e/assets/corex/components/pin-input.css b/e2e/assets/corex/components/pin-input.css new file mode 100644 index 0000000..fada473 --- /dev/null +++ b/e2e/assets/corex/components/pin-input.css @@ -0,0 +1,41 @@ +@import "../main.css"; + +@layer components { + .pin-input [data-scope="pin-input"][data-part="root"] { + @apply ui-root; + + gap: var(--spacing-ui-gap); + max-width: var(--container-ui); + } + + .pin-input [data-scope="pin-input"][data-part="label"] { + @apply ui-label; + } + + .pin-input [data-scope="pin-input"][data-part="control"] { + display: flex; + flex-flow: row nowrap; + gap: var(--spacing-ui-gap); + align-items: center; + margin-inline: auto; + } + + .pin-input [data-scope="pin-input"][data-part="input"] { + @apply ui-input; + + height: var(--spacing-ui); + width: var(--spacing-ui); + flex-shrink: 0; + text-align: center; + + &[data-complete] { + color: var(--color-root--success); + border-color: var(--color-root--success); + } + + &[data-invalid] { + color: var(--color-root--alert); + border-color: var(--color-root--alert); + } + } +} diff --git a/e2e/assets/corex/components/radio-group.css b/e2e/assets/corex/components/radio-group.css new file mode 100644 index 0000000..bedc465 --- /dev/null +++ b/e2e/assets/corex/components/radio-group.css @@ -0,0 +1,107 @@ +@import "../main.css"; + +@layer components { + .radio-group [data-scope="radio-group"][data-part="root"] { + @apply ui-root; + + gap: var(--spacing-ui-gap); + max-width: var(--container-ui); + } + + .radio-group [data-scope="radio-group"][data-part="label"] { + @apply ui-label; + } + + [data-scope="radio-group"][data-part="item"] { + display: inline-flex; + align-items: center; + position: relative; + max-width: var(--container-ui); + width: auto; + gap: var(--spacing-ui-gap); + margin-inline: auto; + cursor: pointer; + } + + [data-scope="radio-group"][data-part="item"][data-disabled] { + cursor: not-allowed; + } + + [data-scope="radio-group"][data-part="item-control"] { + height: var(--spacing-mini); + width: var(--spacing-mini); + background: var(--color-ui); + color: var(--color-ui--text); + font-weight: var(--font-weight-ui); + border-radius: var(--radius-full); + border: 1px solid var(--color-ui--border); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + padding: var(--spacing-micro-padding-sm); + + &[data-state="checked"] { + background: var(--color-ui--text); + color: var(--color-ui); + border-color: var(--color-ui--text); + } + + &[data-state="unchecked"] { + background: var(--color-ui); + color: var(--color-ui--text); + border-color: var(--color-ui--border); + } + + &:hover { + background-color: var(--color-ui-hover); + } + + &:active { + background-color: var(--color-ui-active); + } + + &[data-state="checked"]:hover { + background-color: var(--color-ui--text); + opacity: 0.9; + } + + &:focus-visible, + &[data-focus] { + outline: 2px solid var(--color-ui--text); + outline-offset: -3px; + } + + &:disabled, + &[data-disabled], + &[data-disabled="true"], + &[disabled="true"] { + color: var(--color-ui--muted); + background-color: --alpha(var(--color-ui) / 60%); + cursor: not-allowed; + } + + &[data-invalid] { + color: var(--color-ui-alert); + border-color: var(--color-ui-alert); + } + + &[data-required] { + color: var(--color-ui-alert); + border-color: var(--color-ui-alert); + } + } + + [data-scope="radio-group"][data-part="item-control"] .data-checked { + display: none !important; + } + + [data-scope="radio-group"][data-part="item-control"][data-state="checked"] + .data-checked { + display: block !important; + @apply ui-icon; + color: var(--color-ui-accent--text); + width: 100%; + height: 100%; + } +} diff --git a/e2e/assets/corex/components/timer.css b/e2e/assets/corex/components/timer.css new file mode 100644 index 0000000..af829e8 --- /dev/null +++ b/e2e/assets/corex/components/timer.css @@ -0,0 +1,88 @@ +@import "../main.css"; + +@layer components { + .timer [data-scope="timer"][data-part="root"] { + @apply ui-root; + + max-width: var(--container-ui); + } + + .timer [data-scope="timer"][data-part="area"] { + display: flex; + width: 100%; + justify-content: center; + align-items: center; + flex-wrap: wrap; + } + + .timer [data-scope="timer"][data-part="item"] { + display: flex; + align-items: center; + justify-content: center; + background: var(--color-ui); + border: 1px solid var(--color-ui--border); + border-radius: var(--radius-ui); + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + font-weight: var(--font-weight-ui); + color: var(--color-ui--text); + height: 2em; + aspect-ratio: 1/1; + overflow: hidden; + + /* stylelint-disable-next-line declaration-property-value-no-unknown */ + font-variation-settings: "tnum"; + text-align: center; + position: relative; + } + + .timer [data-scope="timer"][data-part="item"]::before { + position: absolute; + left: 0; + right: 0; + content: "00\a 01\a 02\a 03\a 04\a 05\a 06\a 07\a 08\a 09\a 10\a 11\a 12\a 13\a 14\a 15\a 16\a 17\a 18\a 19\a 20\a 21\a 22\a 23\a 24\a 25\a 26\a 27\a 28\a 29\a 30\a 31\a 32\a 33\a 34\a 35\a 36\a 37\a 38\a 39\a 40\a 41\a 42\a 43\a 44\a 45\a 46\a 47\a 48\a 49\a 50\a 51\a 52\a 53\a 54\a 55\a 56\a 57\a 58\a 59\a 60\a 61\a 62\a 63\a 64\a 65\a 66\a 67\a 68\a 69\a 70\a 71\a 72\a 73\a 74\a 75\a 76\a 77\a 78\a 79\a 80\a 81\a 82\a 83\a 84\a 85\a 86\a 87\a 88\a 89\a 90\a 91\a 92\a 93\a 94\a 95\a 96\a 97\a 98\a 99\a"; + white-space: pre; + top: calc(var(--value) * -2em); + font-size: inherit; + line-height: 2; + text-align: center; + transition: top 1s cubic-bezier(1, 0, 0, 1); + pointer-events: none; + z-index: 1; + } + + .timer [data-scope="timer"][data-part="separator"] { + font-size: var(--text-ui); + line-height: var(--text-ui--line-height); + font-weight: var(--font-weight-ui); + padding: var(--spacing-micro-padding); + } + + .timer [data-scope="timer"][data-part="control"] { + display: inline-flex; + width: 100%; + justify-content: center; + gap: var(--spacing-micro-gap); + } + + .timer [data-scope="timer"][data-part="action-trigger"] { + @apply ui-trigger ui-trigger--square ui-trigger--sm; + } +} + +@utility timer--* { + [data-scope="timer"][data-part="root"] { + max-width: --value(--container-ui-*, [length]); + } + + [data-scope="timer"][data-part="item"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + } + + [data-scope="timer"][data-part="action-trigger"] { + font-size: --value(--text-ui-*, [length]); + line-height: --value(--text-ui-* --line-height, [length]); + min-height: --value(--spacing-ui-*, [length]); + } +} diff --git a/e2e/assets/corex/utilities.css b/e2e/assets/corex/utilities.css index dcb46f3..5f23a5a 100644 --- a/e2e/assets/corex/utilities.css +++ b/e2e/assets/corex/utilities.css @@ -123,6 +123,7 @@ height: 0.9em !important; width: 0.9em !important; color: currentcolor; + flex-shrink: 0; [dir="rtl"] & { transform: scaleX(-1); diff --git a/e2e/assets/css/app.css b/e2e/assets/css/app.css index 240e214..518c01b 100644 --- a/e2e/assets/css/app.css +++ b/e2e/assets/css/app.css @@ -1,5 +1,9 @@ @import "tailwindcss" source(none); +@plugin "../vendor/heroicons"; + @source "../css"; +@source "../corex"; + @source "../js"; @source "../../lib/e2e_web"; @@ -28,6 +32,17 @@ @import "../corex/components/toast.css"; @import "../corex/components/button.css"; @import "../corex/components/badge.css"; +@import "../corex/components/angle-slider.css"; +@import "../corex/components/avatar.css"; +@import "../corex/components/carousel.css"; +@import "../corex/components/editable.css"; +@import "../corex/components/floating-panel.css"; +@import "../corex/components/listbox.css"; +@import "../corex/components/number-input.css"; +@import "../corex/components/password-input.css"; +@import "../corex/components/pin-input.css"; +@import "../corex/components/radio-group.css"; +@import "../corex/components/timer.css"; @import "./navigation.css"; @@ -39,7 +54,6 @@ width: auto; } -@plugin "../vendor/heroicons"; @custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &); @custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &); diff --git a/e2e/config/config.exs b/e2e/config/config.exs index 56d07fc..9dd9570 100644 --- a/e2e/config/config.exs +++ b/e2e/config/config.exs @@ -40,8 +40,15 @@ config :e2e, E2e.Mailer, adapter: Swoosh.Adapters.Local config :esbuild, version: "0.25.4", e2e: [ - args: - ~w(js/app.js --bundle --format=esm --splitting --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.), + args: ~w(js/app.js + --bundle + --format=esm + --splitting + --target=es2022 + --outdir=../priv/static/assets/js + --external:/fonts/* + --external:/images/* + --alias:@=.), cd: Path.expand("../assets", __DIR__), env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]} ] diff --git a/e2e/config/dev.exs b/e2e/config/dev.exs index 00a3e01..29d5510 100644 --- a/e2e/config/dev.exs +++ b/e2e/config/dev.exs @@ -25,8 +25,8 @@ config :e2e, E2eWeb.Endpoint, debug_errors: true, secret_key_base: "JQ3bjpj3drUYoLh2G4QBZr8KpmxWvAJGLV6DZzv9mIRqOkQqhpZwtu9np9fwDeSX", watchers: [ - esbuild: {Esbuild, :install_and_run, [:e2e, ~w(--sourcemap --watch)]}, - tailwind: {Tailwind, :install_and_run, [:e2e, ~w(--watch)]} + esbuild: {Esbuild, :install_and_run, [:e2e, ~w(--sourcemap=inline --watch --minify)]}, + tailwind: {Tailwind, :install_and_run, [:e2e, ~w(--watch --minify)]} ] # ## SSL Support @@ -57,11 +57,8 @@ config :e2e, E2eWeb.Endpoint, live_reload: [ web_console_logger: true, patterns: [ - # Static assets, except user uploads ~r"priv/static/(?!uploads/).*\.(js|css|png|jpeg|jpg|gif|svg)$"E, - # Gettext translations ~r"priv/gettext/.*\.po$"E, - # Router, Controllers, LiveViews and LiveComponents ~r"lib/e2e_web/router\.ex$"E, ~r"lib/e2e_web/(controllers|live|components)/.*\.(ex|heex)$"E ] diff --git a/e2e/images/apple-touch-icon-180x180.png b/e2e/images/apple-touch-icon-180x180.png new file mode 100644 index 0000000..13b654c Binary files /dev/null and b/e2e/images/apple-touch-icon-180x180.png differ diff --git a/e2e/images/avatar.png b/e2e/images/avatar.png new file mode 100644 index 0000000..cd084b0 Binary files /dev/null and b/e2e/images/avatar.png differ diff --git a/e2e/images/beach.jpg b/e2e/images/beach.jpg new file mode 100644 index 0000000..3a2e18f Binary files /dev/null and b/e2e/images/beach.jpg differ diff --git a/e2e/images/corex-ui-og.jpg b/e2e/images/corex-ui-og.jpg new file mode 100644 index 0000000..2a0dc6e Binary files /dev/null and b/e2e/images/corex-ui-og.jpg differ diff --git a/e2e/images/fall.jpg b/e2e/images/fall.jpg new file mode 100644 index 0000000..1cbbd98 Binary files /dev/null and b/e2e/images/fall.jpg differ diff --git a/e2e/images/favicon.ico b/e2e/images/favicon.ico new file mode 100644 index 0000000..a263d84 Binary files /dev/null and b/e2e/images/favicon.ico differ diff --git a/e2e/images/maskable-icon-512x512.png b/e2e/images/maskable-icon-512x512.png new file mode 100644 index 0000000..db67fc0 Binary files /dev/null and b/e2e/images/maskable-icon-512x512.png differ diff --git a/e2e/images/pwa-192x192.png b/e2e/images/pwa-192x192.png new file mode 100644 index 0000000..029c1a9 Binary files /dev/null and b/e2e/images/pwa-192x192.png differ diff --git a/e2e/images/pwa-512x512.png b/e2e/images/pwa-512x512.png new file mode 100644 index 0000000..039fe66 Binary files /dev/null and b/e2e/images/pwa-512x512.png differ diff --git a/e2e/images/pwa-64x64.png b/e2e/images/pwa-64x64.png new file mode 100644 index 0000000..5e21a1b Binary files /dev/null and b/e2e/images/pwa-64x64.png differ diff --git a/e2e/images/sand.jpg b/e2e/images/sand.jpg new file mode 100644 index 0000000..2732344 Binary files /dev/null and b/e2e/images/sand.jpg differ diff --git a/e2e/images/star.jpg b/e2e/images/star.jpg new file mode 100644 index 0000000..973d492 Binary files /dev/null and b/e2e/images/star.jpg differ diff --git a/e2e/images/winter.jpg b/e2e/images/winter.jpg new file mode 100644 index 0000000..4182857 Binary files /dev/null and b/e2e/images/winter.jpg differ diff --git a/e2e/lib/e2e_web/components/layouts.ex b/e2e/lib/e2e_web/components/layouts.ex index f18dad4..8465037 100644 --- a/e2e/lib/e2e_web/components/layouts.ex +++ b/e2e/lib/e2e_web/components/layouts.ex @@ -45,10 +45,14 @@ defmodule E2eWeb.Layouts do slot :inner_block, required: true def app(assigns) do + locale = assigns.locale || "en" + assigns = assigns - |> assign(:menu, menu_items(assigns.locale)) - |> assign(:form_menu, form_menu_items(assigns.locale)) + |> assign(:menu, menu_items(locale)) + |> assign(:form_menu, form_menu_items(locale)) + |> assign(:prev_path, prev_next_paths(locale, assigns.current_path || "/", :prev)) + |> assign(:next_path, prev_next_paths(locale, assigns.current_path || "/", :next)) ~H"""
@@ -83,28 +87,27 @@ defmodule E2eWeb.Layouts do
<.tree_view - id="components-menu" + id="form-menu" + class="tree-view navigation px-ui-padding" on_selection_change="handle_menu" - class="tree-view navigation px-ui-padding " redirect value={[@current_path |> String.split("/") |> List.last()]} - items={@menu} + items={@form_menu} > - <:label>Corex Components + <:label>Phoenix Form <:indicator> <.icon name="hero-chevron-right" /> - <.tree_view - id="form-menu" - class="tree-view navigation px-ui-padding" + id="components-menu" on_selection_change="handle_menu" + class="tree-view navigation px-ui-padding " redirect value={[@current_path |> String.split("/") |> List.last()]} - items={@form_menu} + items={@menu} > - <:label>Phoenix Form + <:label>Corex Components <:indicator> <.icon name="hero-chevron-right" /> @@ -135,35 +138,49 @@ defmodule E2eWeb.Layouts do
-
+
+
{render_slot(@inner_block)} @@ -261,6 +278,49 @@ defmodule E2eWeb.Layouts do """ end + defp flat_navigation_paths(locale) do + form_paths = + form_menu_items(locale) + |> flatten_tree_ids() + + menu_paths = + menu_items(locale) + |> flatten_tree_ids() + + form_paths ++ menu_paths + end + + defp flatten_tree_ids(items) when is_list(items) do + Enum.flat_map(items, fn + %{id: id, children: []} when is_binary(id) -> + [id] + + %{id: id, children: nil} when is_binary(id) -> + [id] + + %{id: _id, children: children} when is_list(children) and children != [] -> + flatten_tree_ids(children) + + _ -> + [] + end) + end + + defp prev_next_paths(locale, current_path, direction) do + full_path = "/#{locale}#{current_path}" + paths = flat_navigation_paths(locale) + index = Enum.find_index(paths, &(&1 == full_path)) + + case {direction, index} do + {:prev, nil} -> nil + {:prev, 0} -> nil + {:prev, i} when i > 0 -> Enum.at(paths, i - 1) + {:next, nil} -> nil + {:next, i} when i >= 0 and i < length(paths) - 1 -> Enum.at(paths, i + 1) + _ -> nil + end + end + defp menu_items(locale) do Corex.Tree.new([ [ @@ -274,6 +334,31 @@ defmodule E2eWeb.Layouts do [label: "Async", id: "/#{locale}/async/accordion"] ] ], + [ + label: "Angle Slider", + id: "angle-slider", + children: [ + [label: "Controller", id: "/#{locale}/angle-slider"], + [label: "Live", id: "/#{locale}/live/angle-slider"], + [label: "Controlled", id: "/#{locale}/controlled/angle-slider"] + ] + ], + [ + label: "Avatar", + id: "avatar", + children: [ + [label: "Controller", id: "/#{locale}/avatar"], + [label: "Live", id: "/#{locale}/live/avatar"] + ] + ], + [ + label: "Carousel", + id: "carousel", + children: [ + [label: "Controller", id: "/#{locale}/carousel"], + [label: "Live", id: "/#{locale}/live/carousel"] + ] + ], [ label: "Checkbox", id: "checkbox", @@ -322,6 +407,30 @@ defmodule E2eWeb.Layouts do [label: "Live", id: "/#{locale}/live/dialog"] ] ], + [ + label: "Editable", + id: "editable", + children: [ + [label: "Controller", id: "/#{locale}/editable"], + [label: "Live", id: "/#{locale}/live/editable"] + ] + ], + [ + label: "Floating Panel", + id: "floating-panel", + children: [ + [label: "Controller", id: "/#{locale}/floating-panel"], + [label: "Live", id: "/#{locale}/live/floating-panel"] + ] + ], + [ + label: "Listbox", + id: "listbox", + children: [ + [label: "Controller", id: "/#{locale}/listbox"], + [label: "Live", id: "/#{locale}/live/listbox"] + ] + ], [ label: "Menu", id: "menu", @@ -330,6 +439,38 @@ defmodule E2eWeb.Layouts do [label: "Live", id: "/#{locale}/live/menu"] ] ], + [ + label: "Number Input", + id: "number-input", + children: [ + [label: "Controller", id: "/#{locale}/number-input"], + [label: "Live", id: "/#{locale}/live/number-input"] + ] + ], + [ + label: "Password Input", + id: "password-input", + children: [ + [label: "Controller", id: "/#{locale}/password-input"], + [label: "Live", id: "/#{locale}/live/password-input"] + ] + ], + [ + label: "Pin Input", + id: "pin-input", + children: [ + [label: "Controller", id: "/#{locale}/pin-input"], + [label: "Live", id: "/#{locale}/live/pin-input"] + ] + ], + [ + label: "Radio Group", + id: "radio-group", + children: [ + [label: "Controller", id: "/#{locale}/radio-group"], + [label: "Live", id: "/#{locale}/live/radio-group"] + ] + ], [ label: "Select", id: "select", @@ -362,6 +503,14 @@ defmodule E2eWeb.Layouts do [label: "Live", id: "/#{locale}/live/tabs"] ] ], + [ + label: "Timer", + id: "timer", + children: [ + [label: "Controller", id: "/#{locale}/timer"], + [label: "Live", id: "/#{locale}/live/timer"] + ] + ], [ label: "Toast", id: "toast", diff --git a/e2e/lib/e2e_web/components/layouts/root.html.heex b/e2e/lib/e2e_web/components/layouts/root.html.heex index 8a5540c..de97811 100644 --- a/e2e/lib/e2e_web/components/layouts/root.html.heex +++ b/e2e/lib/e2e_web/components/layouts/root.html.heex @@ -15,12 +15,48 @@ - <.live_title default="E2e" suffix=" · Phoenix Framework"> + <.live_title default="Corex" suffix=" · Elixir Phoenix Framework UI Library"> {assigns[:page_title]} + + + + + + + + + + + + + + + + + + + + + + + + +``` + ## Import Components Add `use Corex` into your `MyAppWeb` `html_helpers` @@ -348,11 +375,11 @@ In order to use the API, you must use an id on the component ## Performance -By default Phoenix esbuild is set to bundle all the JS into a single file. +Corex hooks load component JavaScript only when a DOM element with that hook is mounted. This requires ESM format and code splitting. -### 1.Enable splitting +### 1. Build configuration (ESM and splitting) -In order to enable splitting add the following `--format=esm --splitting` to your esbuild config +Add `--format=esm` and `--splitting` to your esbuild config. ESM is required for dynamic `import()`. Splitting produces separate chunks for each component and shared code, so only the components used on a page are loaded. ```elixir config :esbuild, @@ -365,19 +392,23 @@ config :esbuild, ] ``` -Run `mix assets.build` and see the magic happening +### 2. Enable gzip for Plug.Static -### 2.Enable gzip for Plug.Static +Set `gzip: true` on `Plug.Static` in your endpoint so that pre-compressed `.gz` files are served when the client supports them. -In your `endpoint.ex` enable gzip for developement also +### 3. How dynamic hook loading works -```elexir - plug Plug.Static, - at: "/", - from: :e2e, - gzip: true, - only: E2eWeb.static_paths(), - raise_on_missing_only: code_reloading? -``` +1. **App start** – Corex registers small stubs (e.g. Accordion, Combobox) as LiveSocket hooks. Each stub stores a function like `() => import("corex/accordion")` but does not run it yet. +2. **Page load** – If the page has no Corex components, no component code is loaded. +3. **Component appears** – When LiveView renders an element with `phx-hook="Accordion"`, LiveSocket mounts that hook and calls the stub's `mounted()`. +4. **Dynamic load** – Inside `mounted()`, the stub runs `await import("corex/accordion")`. The browser fetches and executes the accordion chunk for the first time. The stub then delegates to the real hook. +5. **Result** – Each component's JavaScript is loaded only when a DOM element with its `phx-hook` is mounted. If a component never appears on a page, its chunk is never fetched. + +### 4. Compression and dev performance + +In development, watchers output unminified, uncompressed assets. `Plug.Static` with `gzip: true` only serves pre-existing `.gz` files; watchers do not create them. If the app feels slow in development (especially with many nested components): + +1. Run `mix assets.deploy` instead of `mix assets.build` before `mix phx.server` for production-like asset output (minified and compressed). +2. Ensure `gzip: true` is set on `Plug.Static` in your endpoint. -See the [Production guide](production.html) for the final build in production environnement \ No newline at end of file +See the [Production guide](production.html) for the final build in production. \ No newline at end of file diff --git a/lib/components/angle_slider.ex b/lib/components/angle_slider.ex new file mode 100644 index 0000000..1661461 --- /dev/null +++ b/lib/components/angle_slider.ex @@ -0,0 +1,246 @@ +defmodule Corex.AngleSlider do + @moduledoc ~S''' + Phoenix implementation of [Zag.js Angle Slider](https://zagjs.com/components/react/angle-slider). + + ## Examples + + + + ### Basic + + ```heex + <.angle_slider id="angle" class="angle-slider"> + <:label>Angle + + ``` + + ### With marks + + ```heex + <.angle_slider id="angle" class="angle-slider" marker_values={[0, 90, 180, 270]}> + <:label>Angle + + ``` + + ### Controlled + + In controlled mode, use `on_value_change` and `on_value_change_client` so the thumb moves + during drag. Use `on_value_change_end` and `on_value_change_end_client` if you only need + to react when the user releases. + + ```elixir + defmodule MyAppWeb.AngleSliderLive do + use MyAppWeb, :live_view + + def mount(_params, _session, socket) do + {:ok, assign(socket, :angle, 0)} + end + + def handle_event("angle_changed", %{"value" => value}, socket) do + {:noreply, assign(socket, :angle, value)} + end + + def render(assigns) do + ~H""" + <.angle_slider + id="angle" + controlled + value={@angle} + on_value_change="angle_changed" + marker_values={[0, 90, 180, 270]} + class="angle-slider"> + <:label>Angle + + """ + end + end + ``` + + ## API Control + + In order to use the API, you must use an id on the component. + + ***Client-side*** + + ```heex + + ``` + + ***Server-side*** + + ```elixir + def handle_event("set_angle", %{"value" => value}, socket) do + {:noreply, Corex.AngleSlider.set_value(socket, "my-angle-slider", String.to_float(value))} + end + ``` + + ## Machine API + + The slider API exposes the following methods: + + | Method | Type | Description | + |--------|------|-------------| + | value | number | The current value of the angle slider | + | valueAsDegree | string | The current value as a degree string | + | setValue | (value: number) => void | Sets the value of the angle slider | + | dragging | boolean | Whether the slider is being dragged | + + ## Styling + + Use data attributes: `[data-scope="angle-slider"][data-part="root"]`, `control`, `thumb`, `value-text` (with `value` and `text` spans), `marker-group`, `marker`. + ''' + + @doc type: :component + use Phoenix.Component + + alias Corex.AngleSlider.Anatomy.{ + Props, + Root, + Label, + HiddenInput, + Control, + Thumb, + ValueText, + Value, + Text, + MarkerGroup, + Marker + } + + alias Corex.AngleSlider.Connect + + attr(:id, :string, required: false, doc: "The id of the angle slider") + attr(:value, :float, default: 0.0, doc: "The value or controlled value in degrees") + attr(:controlled, :boolean, default: false, doc: "Whether the value is controlled") + attr(:step, :float, default: 1.0, doc: "Step value") + attr(:disabled, :boolean, default: false, doc: "Whether the slider is disabled") + attr(:read_only, :boolean, default: false, doc: "Whether the slider is read-only") + attr(:invalid, :boolean, default: false, doc: "Whether the slider is invalid") + attr(:name, :string, default: nil, doc: "Name for form submission") + attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"], doc: "Direction") + + attr(:on_value_change, :string, + default: nil, + doc: "Server event when value changes (uncontrolled)" + ) + + attr(:on_value_change_client, :string, + default: nil, + doc: "Client event when value changes (uncontrolled)" + ) + + attr(:on_value_change_end, :string, + default: nil, + doc: "Server event when value change ends (controlled)" + ) + + attr(:on_value_change_end_client, :string, + default: nil, + doc: "Client event when value change ends (controlled)" + ) + + attr(:marker_values, :list, + default: [], + doc: "List of angle values to show as markers (e.g. [0, 90, 180, 270])" + ) + + attr(:rest, :global) + + slot(:label, required: false) + + def angle_slider(assigns) do + assigns = + assigns + |> assign_new(:id, fn -> "angle-slider-#{System.unique_integer([:positive])}" end) + |> assign_new(:dir, fn -> "ltr" end) + + ~H""" +
+
+
+ {render_slot(@label)} +
+
+
+
+
+
+
+
+ <%= @value %> + ° +
+ +
+
+ """ + end + + @doc type: :api + @doc """ + Sets the angle slider value from client-side. Returns a `Phoenix.LiveView.JS` command. + + ## Examples + + + """ + def set_value(angle_slider_id, value) when is_binary(angle_slider_id) and is_number(value) do + Phoenix.LiveView.JS.dispatch("phx:angle-slider:set-value", + to: "##{angle_slider_id}", + detail: %{value: value}, + bubbles: false + ) + end + + @doc type: :api + @doc """ + Sets the angle slider value from server-side. Pushes a LiveView event. + + ## Examples + + def handle_event("set_angle", %{"value" => value}, socket) do + angle = String.to_float(value) + {:noreply, Corex.AngleSlider.set_value(socket, "my-angle-slider", angle)} + end + """ + def set_value(socket, angle_slider_id, value) + when is_struct(socket, Phoenix.LiveView.Socket) and is_binary(angle_slider_id) do + angle = + if is_binary(value) do + case Float.parse(value) do + {num, _} -> num + :error -> 0 + end + else + value + end + + Phoenix.LiveView.push_event(socket, "angle_slider_set_value", %{ + angle_slider_id: angle_slider_id, + value: angle + }) + end +end diff --git a/lib/components/angle_slider/anatomy.ex b/lib/components/angle_slider/anatomy.ex new file mode 100644 index 0000000..382aca2 --- /dev/null +++ b/lib/components/angle_slider/anatomy.ex @@ -0,0 +1,115 @@ +defmodule Corex.AngleSlider.Anatomy do + @moduledoc false + + defmodule Props do + @moduledoc false + @enforce_keys [:id] + + defstruct [ + :id, + value: 0, + controlled: false, + step: 1, + disabled: false, + read_only: false, + invalid: false, + name: nil, + dir: "ltr", + on_value_change: nil, + on_value_change_client: nil, + on_value_change_end: nil, + on_value_change_end_client: nil + ] + + @type t :: %__MODULE__{ + id: String.t(), + value: number(), + controlled: boolean(), + step: number(), + disabled: boolean(), + read_only: boolean(), + invalid: boolean(), + name: String.t() | nil, + dir: String.t(), + on_value_change: String.t() | nil, + on_value_change_client: String.t() | nil, + on_value_change_end: String.t() | nil, + on_value_change_end_client: String.t() | nil + } + end + + defmodule Root do + @moduledoc false + defstruct [:id, :dir, :value] + + @type t :: %__MODULE__{id: String.t(), dir: String.t(), value: number()} + end + + defmodule Label do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule HiddenInput do + @moduledoc false + defstruct [:id, :name, :value, :disabled] + + @type t :: %__MODULE__{ + id: String.t(), + name: String.t() | nil, + value: number(), + disabled: boolean() + } + end + + defmodule Control do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule Thumb do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule ValueText do + @moduledoc false + defstruct [:id, :dir, :value] + + @type t :: %__MODULE__{id: String.t(), dir: String.t(), value: number()} + end + + defmodule Value do + @moduledoc false + defstruct [] + + @type t :: %__MODULE__{} + end + + defmodule Text do + @moduledoc false + defstruct [] + + @type t :: %__MODULE__{} + end + + defmodule MarkerGroup do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule Marker do + @moduledoc false + defstruct [:id, :value, :slider_value] + + @type t :: %__MODULE__{id: String.t(), value: number(), slider_value: number()} + end +end diff --git a/lib/components/angle_slider/connect.ex b/lib/components/angle_slider/connect.ex new file mode 100644 index 0000000..8119103 --- /dev/null +++ b/lib/components/angle_slider/connect.ex @@ -0,0 +1,153 @@ +defmodule Corex.AngleSlider.Connect do + @moduledoc false + alias Corex.AngleSlider.Anatomy.{ + Props, + Root, + Label, + HiddenInput, + Control, + Thumb, + ValueText, + Value, + Text, + MarkerGroup, + Marker + } + + defp data_attr(true), do: "" + defp data_attr(false), do: nil + defp data_attr(nil), do: nil + + @spec props(Props.t()) :: map() + def props(assigns) do + %{ + "id" => assigns.id, + "data-default-value" => if(assigns.controlled, do: nil, else: to_string(assigns.value)), + "data-value" => if(assigns.controlled, do: to_string(assigns.value), else: nil), + "data-controlled" => data_attr(assigns.controlled), + "data-step" => to_string(assigns.step), + "data-disabled" => data_attr(assigns.disabled), + "data-read-only" => data_attr(assigns.read_only), + "data-invalid" => data_attr(assigns.invalid), + "data-name" => assigns.name, + "data-dir" => assigns.dir, + "data-on-value-change" => assigns.on_value_change, + "data-on-value-change-client" => assigns.on_value_change_client, + "data-on-value-change-end" => assigns.on_value_change_end, + "data-on-value-change-end-client" => assigns.on_value_change_end_client + } + end + + @spec root(Root.t()) :: map() + def root(assigns) do + value = assigns.value + angle = "#{value}deg" + + %{ + "data-scope" => "angle-slider", + "data-part" => "root", + "dir" => assigns.dir, + "id" => "angle-slider:#{assigns.id}", + "style" => "--value:#{value};--angle:#{angle};" + } + end + + @spec label(Label.t()) :: map() + def label(assigns) do + %{ + "data-scope" => "angle-slider", + "data-part" => "label", + "dir" => assigns.dir, + "id" => "angle-slider:#{assigns.id}:label" + } + end + + @spec hidden_input(HiddenInput.t()) :: map() + def hidden_input(assigns) do + %{ + "data-scope" => "angle-slider", + "data-part" => "hidden-input", + "type" => "hidden", + "name" => assigns.name, + "value" => to_string(assigns.value), + "disabled" => data_attr(assigns.disabled), + "id" => "angle-slider:#{assigns.id}:input" + } + end + + @spec control(Control.t()) :: map() + def control(assigns) do + %{ + "data-scope" => "angle-slider", + "data-part" => "control", + "dir" => assigns.dir, + "id" => "angle-slider:#{assigns.id}:control" + } + end + + @spec thumb(Thumb.t()) :: map() + def thumb(assigns) do + %{ + "data-scope" => "angle-slider", + "data-part" => "thumb", + "dir" => assigns.dir, + "id" => "angle-slider:#{assigns.id}:thumb", + "style" => "rotate:var(--angle);" + } + end + + @spec value_text(ValueText.t()) :: map() + def value_text(assigns) do + %{ + "data-scope" => "angle-slider", + "data-part" => "value-text", + "dir" => assigns.dir, + "id" => "angle-slider:#{assigns.id}:value-text" + } + end + + @spec value(Value.t()) :: map() + def value(_assigns) do + %{ + "data-scope" => "angle-slider", + "data-part" => "value" + } + end + + @spec text(Text.t()) :: map() + def text(_assigns) do + %{ + "data-scope" => "angle-slider", + "data-part" => "text" + } + end + + @spec marker_group(MarkerGroup.t()) :: map() + def marker_group(assigns) do + %{ + "data-scope" => "angle-slider", + "data-part" => "marker-group", + "dir" => assigns.dir, + "id" => "angle-slider:#{assigns.id}:marker-group" + } + end + + @spec marker(Marker.t()) :: map() + def marker(assigns) do + state = + cond do + assigns.value < assigns.slider_value -> "under-value" + assigns.value > assigns.slider_value -> "over-value" + true -> "at-value" + end + + %{ + "data-scope" => "angle-slider", + "data-part" => "marker", + "data-value" => to_string(assigns.value), + "data-state" => state, + "id" => "angle-slider:#{assigns.id}:marker:#{assigns.value}", + "style" => "--marker-value:#{assigns.value};rotate:calc(var(--marker-value) * 1deg);" + } + end +end diff --git a/lib/components/avatar.ex b/lib/components/avatar.ex new file mode 100644 index 0000000..228a707 --- /dev/null +++ b/lib/components/avatar.ex @@ -0,0 +1,85 @@ +defmodule Corex.Avatar do + @moduledoc ~S''' + Phoenix implementation of [Zag.js Avatar](https://zagjs.com/components/react/avatar). + + ## Examples + + ### Basic + + ```heex + <.avatar id="avatar" src="/me.jpg" class="avatar"> + <:fallback>JD + + ``` + + ## Styling + + Use data attributes: `[data-scope="avatar"][data-part="root"]`, `image`, `fallback`, `skeleton`. + ''' + + @doc type: :component + use Phoenix.Component + + alias Corex.Avatar.Anatomy.{Props, Root, Image, Fallback, Skeleton} + alias Corex.Avatar.Connect + + attr(:id, :string, required: false, doc: "The id of the avatar") + attr(:src, :string, default: nil, doc: "Image source URL") + attr(:alt, :string, default: "", doc: "Alternative text for the image") + + attr(:on_status_change, :string, + default: nil, + doc: "Server event when image load status changes" + ) + + attr(:on_status_change_client, :string, + default: nil, + doc: "Client event when image load status changes" + ) + + attr(:rest, :global) + + slot(:fallback, required: true) + + def avatar(assigns) do + assigns = + assigns + |> assign_new(:id, fn -> "avatar-#{System.unique_integer([:positive])}" end) + + ~H""" +
+
+ + + +
+
+ """ + end +end diff --git a/lib/components/avatar/anatomy.ex b/lib/components/avatar/anatomy.ex new file mode 100644 index 0000000..679ed56 --- /dev/null +++ b/lib/components/avatar/anatomy.ex @@ -0,0 +1,48 @@ +defmodule Corex.Avatar.Anatomy do + @moduledoc false + + defmodule Props do + @moduledoc false + @enforce_keys [:id] + + defstruct [ + :id, + on_status_change: nil, + on_status_change_client: nil + ] + + @type t :: %__MODULE__{ + id: String.t(), + on_status_change: String.t() | nil, + on_status_change_client: String.t() | nil + } + end + + defmodule Root do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule Image do + @moduledoc false + defstruct [:id, :src] + + @type t :: %__MODULE__{id: String.t(), src: String.t() | nil} + end + + defmodule Fallback do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule Skeleton do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end +end diff --git a/lib/components/avatar/connect.ex b/lib/components/avatar/connect.ex new file mode 100644 index 0000000..d4bad30 --- /dev/null +++ b/lib/components/avatar/connect.ex @@ -0,0 +1,53 @@ +defmodule Corex.Avatar.Connect do + @moduledoc false + alias Corex.Avatar.Anatomy.{Props, Root, Image, Fallback, Skeleton} + + @spec props(Props.t()) :: map() + def props(assigns) do + %{ + "id" => assigns.id, + "data-on-status-change" => assigns.on_status_change, + "data-on-status-change-client" => assigns.on_status_change_client + } + end + + @spec root(Root.t()) :: map() + def root(assigns) do + %{ + "data-scope" => "avatar", + "data-part" => "root", + "id" => "avatar:#{assigns.id}" + } + end + + @spec image(Image.t()) :: map() + def image(assigns) do + %{ + "data-scope" => "avatar", + "data-part" => "image", + "id" => "avatar:#{assigns.id}:image" + } + |> maybe_put("src", assigns.src) + end + + @spec fallback(Fallback.t()) :: map() + def fallback(assigns) do + %{ + "data-scope" => "avatar", + "data-part" => "fallback", + "id" => "avatar:#{assigns.id}:fallback" + } + end + + @spec skeleton(Skeleton.t()) :: map() + def skeleton(assigns) do + %{ + "data-scope" => "avatar", + "data-part" => "skeleton", + "id" => "avatar:#{assigns.id}:skeleton" + } + end + + defp maybe_put(map, _key, nil), do: map + defp maybe_put(map, key, value), do: Map.put(map, key, value) +end diff --git a/lib/components/carousel.ex b/lib/components/carousel.ex new file mode 100644 index 0000000..8e4da76 --- /dev/null +++ b/lib/components/carousel.ex @@ -0,0 +1,135 @@ +defmodule Corex.Carousel do + @moduledoc ~S''' + Phoenix implementation of [Zag.js Carousel](https://zagjs.com/components/react/carousel). + + ## Examples + + ### Basic with image URLs + + ```heex + <.carousel id="car" items={["/images/beach.jpg", "/images/fall.jpg", "/images/sand.jpg"]} class="carousel"> + <:prev_trigger> + + + <:next_trigger> + + + + ``` + + Items can be URLs (strings) or maps with `:url` and optional `:alt` keys. + + ## Styling + + Use data attributes: `[data-scope="carousel"][data-part="root"]`, `control`, `item-group`, `item`, `prev-trigger`, `next-trigger`, `indicator-group`, `indicator`. + ''' + + @doc type: :component + use Phoenix.Component + + alias Corex.Carousel.Anatomy.{ + Props, + Root, + Control, + ItemGroup, + Item, + PrevTrigger, + NextTrigger, + IndicatorGroup, + Indicator + } + + alias Corex.Carousel.Connect + + attr(:id, :string, required: false) + + attr(:items, :list, + required: true, + doc: "List of image URLs (strings) or maps with :url and optional :alt" + ) + + attr(:page, :integer, default: 0) + attr(:controlled, :boolean, default: false) + attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"]) + attr(:orientation, :string, default: "horizontal", values: ["horizontal", "vertical"]) + attr(:slides_per_page, :integer, default: 1) + attr(:loop, :boolean, default: false) + attr(:allow_mouse_drag, :boolean, default: false) + attr(:spacing, :string, default: "0px") + attr(:on_page_change, :string, default: nil) + attr(:on_page_change_client, :string, default: nil) + attr(:rest, :global) + + slot(:prev_trigger, required: true) + slot(:next_trigger, required: true) + + def carousel(assigns) do + items = List.wrap(assigns.items) + slide_count = length(items) + slides_per_page = assigns.slides_per_page || 1 + per_move = slides_per_page + + total_pages = + 0..max(0, slide_count - 1) + |> Enum.take_every(max(1, per_move)) + |> Enum.count(fn i -> i + slides_per_page <= slide_count end) + + page = assigns.page || 0 + loop = assigns.loop || false + prev_disabled = !loop and page <= 0 + next_disabled = !loop and (total_pages == 0 or page >= total_pages - 1) + + assigns = + assigns + |> assign_new(:id, fn -> "carousel-#{System.unique_integer([:positive])}" end) + |> assign_new(:dir, fn -> "ltr" end) + |> assign(:items, items) + |> assign(:slide_count, slide_count) + |> assign(:prev_disabled, prev_disabled) + |> assign(:next_disabled, next_disabled) + + ~H""" +
+
+
+
+ + {Map.get(item, +
+
+
+ +
+ +
+ +
+
+
+ """ + end +end diff --git a/lib/components/carousel/anatomy.ex b/lib/components/carousel/anatomy.ex new file mode 100644 index 0000000..3445e10 --- /dev/null +++ b/lib/components/carousel/anatomy.ex @@ -0,0 +1,113 @@ +defmodule Corex.Carousel.Anatomy do + @moduledoc false + + defmodule Props do + @moduledoc false + @enforce_keys [:id] + + defstruct [ + :id, + slide_count: 0, + page: 0, + controlled: false, + dir: "ltr", + orientation: "horizontal", + slides_per_page: 1, + loop: false, + autoplay: false, + allow_mouse_drag: false, + spacing: "0px", + on_page_change: nil, + on_page_change_client: nil + ] + + @type t :: %__MODULE__{ + id: String.t(), + slide_count: non_neg_integer(), + page: non_neg_integer(), + controlled: boolean(), + dir: String.t(), + orientation: String.t(), + slides_per_page: non_neg_integer(), + loop: boolean(), + autoplay: boolean(), + allow_mouse_drag: boolean(), + spacing: String.t(), + on_page_change: String.t() | nil, + on_page_change_client: String.t() | nil + } + end + + defmodule Root do + @moduledoc false + defstruct [:id, :dir, :orientation, :slides_per_page, :spacing] + + @type t :: %__MODULE__{ + id: String.t(), + dir: String.t(), + orientation: String.t(), + slides_per_page: non_neg_integer(), + spacing: String.t() + } + end + + defmodule Control do + @moduledoc false + defstruct [:id, :orientation] + + @type t :: %__MODULE__{id: String.t(), orientation: String.t()} + end + + defmodule ItemGroup do + @moduledoc false + defstruct [:id, :orientation, :dir] + + @type t :: %__MODULE__{id: String.t(), orientation: String.t(), dir: String.t()} + end + + defmodule Item do + @moduledoc false + defstruct [:id, :index, :orientation, :slide_count] + + @type t :: %__MODULE__{ + id: String.t(), + index: non_neg_integer(), + orientation: String.t(), + slide_count: non_neg_integer() + } + end + + defmodule PrevTrigger do + @moduledoc false + defstruct [:id, :disabled] + + @type t :: %__MODULE__{id: String.t(), disabled: boolean()} + end + + defmodule NextTrigger do + @moduledoc false + defstruct [:id, :disabled] + + @type t :: %__MODULE__{id: String.t(), disabled: boolean()} + end + + defmodule IndicatorGroup do + @moduledoc false + defstruct [:id, :orientation, :dir] + + @type t :: %__MODULE__{id: String.t(), orientation: String.t(), dir: String.t()} + end + + defmodule Indicator do + @moduledoc false + defstruct [:id, :index, :orientation, :dir, :page] + + @type t :: %__MODULE__{ + id: String.t(), + index: non_neg_integer(), + orientation: String.t(), + dir: String.t(), + page: non_neg_integer() + } + end +end diff --git a/lib/components/carousel/connect.ex b/lib/components/carousel/connect.ex new file mode 100644 index 0000000..96e06e5 --- /dev/null +++ b/lib/components/carousel/connect.ex @@ -0,0 +1,174 @@ +defmodule Corex.Carousel.Connect do + @moduledoc false + alias Corex.Carousel.Anatomy.{ + Props, + Root, + Control, + ItemGroup, + Item, + PrevTrigger, + NextTrigger, + IndicatorGroup, + Indicator + } + + defp data_attr(true), do: "" + defp data_attr(false), do: nil + defp data_attr(nil), do: nil + + @spec props(Props.t()) :: map() + def props(assigns) do + %{ + "id" => assigns.id, + "data-slide-count" => to_string(assigns.slide_count), + "data-page" => if(assigns.controlled, do: to_string(assigns.page), else: nil), + "data-default-page" => if(assigns.controlled, do: nil, else: to_string(assigns.page)), + "data-controlled" => data_attr(assigns.controlled), + "data-dir" => assigns.dir, + "data-orientation" => assigns.orientation, + "data-slides-per-page" => to_string(assigns.slides_per_page), + "data-loop" => data_attr(assigns.loop), + "data-allow-mouse-drag" => data_attr(assigns.allow_mouse_drag), + "data-spacing" => assigns.spacing, + "data-on-page-change" => assigns.on_page_change, + "data-on-page-change-client" => assigns.on_page_change_client + } + end + + @spec root(Root.t()) :: map() + def root(assigns) do + slides_per_page = assigns.slides_per_page || 1 + spacing = assigns.spacing || "0px" + + slide_item_size = + "calc(100% / var(--slides-per-page) - var(--slide-spacing) * (var(--slides-per-page) - 1) / var(--slides-per-page))" + + style = + "--slides-per-page:#{slides_per_page};--slide-spacing:#{spacing};--slide-item-size:#{slide_item_size};aspect-ratio:4/3" + + %{ + "data-scope" => "carousel", + "data-part" => "root", + "data-orientation" => assigns.orientation || "horizontal", + "dir" => assigns.dir, + "id" => "carousel:#{assigns.id}", + "style" => style + } + end + + @spec control(Control.t()) :: map() + def control(assigns) do + %{ + "data-scope" => "carousel", + "data-part" => "control", + "data-orientation" => assigns.orientation || "horizontal", + "id" => "carousel:#{assigns.id}:control" + } + end + + @spec item_group(ItemGroup.t()) :: map() + def item_group(assigns) do + horizontal = (assigns.orientation || "horizontal") == "horizontal" + + style = + if horizontal do + "display:grid;gap:var(--slide-spacing);scroll-snap-type:x mandatory;grid-auto-flow:column;scrollbar-width:none;overscroll-behavior-x:contain;grid-auto-columns:var(--slide-item-size);overflow-x:auto" + else + "display:grid;gap:var(--slide-spacing);scroll-snap-type:y mandatory;grid-auto-flow:row;scrollbar-width:none;overscroll-behavior-y:contain;grid-auto-rows:var(--slide-item-size);overflow-y:auto" + end + + %{ + "data-scope" => "carousel", + "data-part" => "item-group", + "data-orientation" => assigns.orientation || "horizontal", + "dir" => assigns.dir, + "id" => "carousel:#{assigns.id}:item-group", + "aria-live" => "polite", + "style" => style, + "tabindex" => "0" + } + end + + @spec item(Item.t()) :: map() + def item(assigns) do + horizontal = (assigns.orientation || "horizontal") == "horizontal" + size_style = if horizontal, do: "max-width:100%", else: "max-height:100%" + slide_count = assigns.slide_count || 1 + + style = "flex:0 0 auto;#{size_style};scroll-snap-align:start" + + %{ + "data-scope" => "carousel", + "data-part" => "item", + "data-index" => to_string(assigns.index), + "data-orientation" => assigns.orientation || "horizontal", + "dir" => Map.get(assigns, :dir, "ltr"), + "id" => "carousel:#{assigns.id}:item:#{assigns.index}", + "role" => "group", + "aria-roledescription" => "slide", + "aria-label" => "#{assigns.index + 1} of #{slide_count}", + "style" => style + } + end + + @spec prev_trigger(PrevTrigger.t()) :: map() + def prev_trigger(assigns) do + base = %{ + "data-scope" => "carousel", + "data-part" => "prev-trigger", + "type" => "button", + "aria-label" => "Previous slide", + "id" => "carousel:#{assigns.id}:prev" + } + + if Map.get(assigns, :disabled, false) do + Map.put(base, "disabled", "") + else + base + end + end + + @spec next_trigger(NextTrigger.t()) :: map() + def next_trigger(assigns) do + base = %{ + "data-scope" => "carousel", + "data-part" => "next-trigger", + "type" => "button", + "aria-label" => "Next slide", + "id" => "carousel:#{assigns.id}:next" + } + + if Map.get(assigns, :disabled, false) do + Map.put(base, "disabled", "") + else + base + end + end + + @spec indicator_group(IndicatorGroup.t()) :: map() + def indicator_group(assigns) do + %{ + "data-scope" => "carousel", + "data-part" => "indicator-group", + "data-orientation" => assigns.orientation || "horizontal", + "dir" => assigns.dir || "ltr", + "id" => "carousel:#{assigns.id}:indicator-group" + } + end + + @spec indicator(Indicator.t()) :: map() + def indicator(assigns) do + page = Map.get(assigns, :page, 0) + + %{ + "data-scope" => "carousel", + "data-part" => "indicator", + "data-index" => to_string(assigns.index), + "data-current" => data_attr(assigns.index == page), + "data-orientation" => assigns.orientation || "horizontal", + "dir" => assigns.dir || "ltr", + "type" => "button", + "id" => "carousel:#{assigns.id}:indicator:#{assigns.index}" + } + end +end diff --git a/lib/components/combobox/anatomy.ex b/lib/components/combobox/anatomy.ex index 4aebe16..3b1e036 100644 --- a/lib/components/combobox/anatomy.ex +++ b/lib/components/combobox/anatomy.ex @@ -1,6 +1,6 @@ defmodule Corex.Combobox.Anatomy do @moduledoc false - alias Corex.Collection + alias Corex.Tree defmodule Props do @moduledoc false @@ -36,7 +36,7 @@ defmodule Corex.Combobox.Anatomy do @type t :: %__MODULE__{ id: String.t(), - collection: list(Collection.Item.t() | map()), + collection: list(Tree.Item.t() | map()), controlled: boolean(), placeholder: String.t() | nil, value: list(String.t()), diff --git a/lib/components/editable.ex b/lib/components/editable.ex new file mode 100644 index 0000000..7d8162d --- /dev/null +++ b/lib/components/editable.ex @@ -0,0 +1,135 @@ +defmodule Corex.Editable do + @moduledoc ~S''' + Phoenix implementation of [Zag.js Editable](https://zagjs.com/components/react/editable). + + ## Examples + + ### Basic + + ```heex + <.editable id="edit" value="Click to edit" class="editable"> + <:label>Name + <:edit_trigger><.icon name="hero-pencil-square" class="icon" /> + <:submit_trigger><.icon name="hero-check" class="icon" /> + <:cancel_trigger><.icon name="hero-x-mark" class="icon" /> + + ``` + + Required slots: `:label`, `:edit_trigger`, `:submit_trigger`, `:cancel_trigger`. Preview value is managed by the component and the Editable TS hook. + + ## Styling + + Use data attributes: `[data-scope="editable"][data-part="root"]`, `area`, `label`, `input`, `preview`, `edit-trigger`, `control`, `submit-trigger`, `cancel-trigger`. + ''' + + @doc type: :component + use Phoenix.Component + + alias Corex.Editable.Anatomy.{ + Props, + Root, + Area, + Label, + Input, + Preview, + EditTrigger, + Triggers, + SubmitTrigger, + CancelTrigger + } + + alias Corex.Editable.Connect + + attr(:id, :string, required: false) + attr(:value, :string, default: "") + attr(:controlled, :boolean, default: false) + attr(:disabled, :boolean, default: false) + attr(:read_only, :boolean, default: false) + attr(:required, :boolean, default: false) + attr(:invalid, :boolean, default: false) + attr(:name, :string, default: nil) + attr(:form, :string, default: nil) + attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"]) + attr(:edit, :boolean, default: false) + attr(:controlled_edit, :boolean, default: false) + attr(:default_edit, :boolean, default: false) + attr(:placeholder, :string, default: nil) + attr(:activation_mode, :string, default: nil, values: [nil, "dblclick", "focus"]) + attr(:select_on_focus, :boolean, default: true) + attr(:on_value_change, :string, default: nil) + attr(:on_value_change_client, :string, default: nil) + attr(:rest, :global) + + slot(:label, required: true) + slot(:edit_trigger, required: true) + slot(:submit_trigger, required: true) + slot(:cancel_trigger, required: true) + + def editable(assigns) do + empty = String.trim(assigns[:value] || "") == "" + editing = assigns[:default_edit] || false + value_text = if(empty, do: assigns[:placeholder] || "", else: assigns[:value] || "") + + assigns = + assigns + |> assign_new(:id, fn -> "editable-#{System.unique_integer([:positive])}" end) + |> assign_new(:dir, fn -> "ltr" end) + |> assign(:empty, empty) + |> assign(:editing, editing) + |> assign(:value_text, value_text) + + ~H""" +
+
+ +
+
+ + + {@value_text} + +
+
+ + + +
+
+ +
+
+ """ + end +end diff --git a/lib/components/editable/anatomy.ex b/lib/components/editable/anatomy.ex new file mode 100644 index 0000000..97507d7 --- /dev/null +++ b/lib/components/editable/anatomy.ex @@ -0,0 +1,154 @@ +defmodule Corex.Editable.Anatomy do + @moduledoc false + + defmodule Props do + @moduledoc false + @enforce_keys [:id] + + defstruct [ + :id, + value: "", + controlled: false, + disabled: false, + read_only: false, + required: false, + invalid: false, + name: nil, + form: nil, + dir: "ltr", + edit: false, + controlled_edit: false, + default_edit: false, + placeholder: nil, + activation_mode: nil, + select_on_focus: true, + on_value_change: nil, + on_value_change_client: nil + ] + + @type t :: %__MODULE__{ + id: String.t(), + value: String.t(), + controlled: boolean(), + disabled: boolean(), + read_only: boolean(), + required: boolean(), + invalid: boolean(), + name: String.t() | nil, + form: String.t() | nil, + dir: String.t(), + edit: boolean(), + controlled_edit: boolean(), + default_edit: boolean(), + placeholder: String.t() | nil, + activation_mode: String.t() | nil, + select_on_focus: boolean(), + on_value_change: String.t() | nil, + on_value_change_client: String.t() | nil + } + end + + defmodule Root do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule Area do + @moduledoc false + defstruct [:id, :dir, empty: false, editing: false, auto_resize: true] + + @type t :: %__MODULE__{ + id: String.t(), + dir: String.t(), + empty: boolean(), + editing: boolean(), + auto_resize: boolean() + } + end + + defmodule Label do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule Input do + @moduledoc false + defstruct [ + :id, + :disabled, + :value, + :placeholder, + :name, + :form, + :aria_label, + required: false, + read_only: false, + editing: false + ] + + @type t :: %__MODULE__{ + id: String.t(), + disabled: boolean(), + value: String.t() | nil, + placeholder: String.t() | nil, + name: String.t() | nil, + form: String.t() | nil, + aria_label: String.t() | nil, + required: boolean(), + read_only: boolean(), + editing: boolean() + } + end + + defmodule Preview do + @moduledoc false + defstruct [:id, :dir, :value_text, empty: false, editing: false] + + @type t :: %__MODULE__{ + id: String.t(), + dir: String.t(), + value_text: String.t() | nil, + empty: boolean(), + editing: boolean() + } + end + + defmodule EditTrigger do + @moduledoc false + defstruct [:id, :dir, editing: false] + + @type t :: %__MODULE__{id: String.t(), dir: String.t(), editing: boolean()} + end + + defmodule Control do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule Triggers do + @moduledoc false + defstruct [] + + @type t :: %__MODULE__{} + end + + defmodule SubmitTrigger do + @moduledoc false + defstruct [:id, :dir, editing: false] + + @type t :: %__MODULE__{id: String.t(), dir: String.t(), editing: boolean()} + end + + defmodule CancelTrigger do + @moduledoc false + defstruct [:id, :dir, editing: false] + + @type t :: %__MODULE__{id: String.t(), dir: String.t(), editing: boolean()} + end +end diff --git a/lib/components/editable/connect.ex b/lib/components/editable/connect.ex new file mode 100644 index 0000000..cd8ae57 --- /dev/null +++ b/lib/components/editable/connect.ex @@ -0,0 +1,176 @@ +defmodule Corex.Editable.Connect do + @moduledoc false + alias Corex.Editable.Anatomy.{ + Props, + Root, + Area, + Label, + Input, + Preview, + EditTrigger, + Control, + Triggers, + SubmitTrigger, + CancelTrigger + } + + defp data_attr(true), do: "" + defp data_attr(false), do: nil + defp data_attr(nil), do: nil + + @spec props(Props.t()) :: map() + def props(assigns) do + %{ + "id" => assigns.id, + "data-value" => if(assigns.controlled, do: assigns.value, else: nil), + "data-default-value" => if(assigns.controlled, do: nil, else: assigns.value), + "data-controlled" => data_attr(assigns.controlled), + "data-disabled" => data_attr(assigns.disabled), + "data-read-only" => data_attr(assigns.read_only), + "data-required" => data_attr(assigns.required), + "data-invalid" => data_attr(assigns.invalid), + "data-name" => assigns.name, + "data-form" => assigns.form, + "data-dir" => assigns.dir, + "data-edit" => if(assigns.controlled_edit, do: data_attr(assigns.edit), else: nil), + "data-default-edit" => + if(assigns.controlled_edit, do: nil, else: data_attr(assigns.default_edit)), + "data-controlled-edit" => data_attr(assigns.controlled_edit), + "data-placeholder" => assigns.placeholder, + "data-activation-mode" => assigns.activation_mode, + "data-select-on-focus" => data_attr(assigns.select_on_focus), + "data-on-value-change" => assigns.on_value_change, + "data-on-value-change-client" => assigns.on_value_change_client + } + end + + @spec root(Root.t()) :: map() + def root(assigns) do + %{ + "data-scope" => "editable", + "data-part" => "root", + "dir" => assigns.dir, + "id" => "editable:#{assigns.id}" + } + end + + @spec area(Area.t()) :: map() + def area(assigns) do + base = %{ + "data-scope" => "editable", + "data-part" => "area", + "dir" => assigns.dir, + "id" => "editable:#{assigns.id}:area", + "data-focus" => data_attr(assigns.editing), + "data-placeholder-shown" => data_attr(assigns.empty) + } + + if assigns.auto_resize do + Map.put(base, "style", "display:inline-grid") + else + base + end + end + + @spec label(Label.t()) :: map() + def label(assigns) do + %{ + "data-scope" => "editable", + "data-part" => "label", + "dir" => assigns.dir, + "id" => "editable:#{assigns.id}:label", + "for" => "editable:#{assigns.id}:input" + } + end + + @spec input(Input.t()) :: map() + def input(assigns) do + %{ + "data-scope" => "editable", + "data-part" => "input", + "disabled" => data_attr(assigns.disabled), + "id" => "editable:#{assigns.id}:input", + "required" => data_attr(assigns.required), + "readonly" => data_attr(assigns.read_only), + "aria-label" => assigns.aria_label || "editable input", + "data-disabled" => data_attr(assigns.disabled), + "data-readonly" => data_attr(assigns.read_only) + } + |> maybe_put("placeholder", assigns.placeholder) + |> maybe_put("name", assigns.name) + |> maybe_put("form", assigns.form) + |> maybe_put("value", assigns.value) + |> maybe_put("hidden", if(assigns.editing, do: nil, else: "")) + end + + @spec preview(Preview.t()) :: map() + def preview(assigns) do + %{ + "data-scope" => "editable", + "data-part" => "preview", + "dir" => assigns.dir, + "id" => "editable:#{assigns.id}:preview", + "data-placeholder-shown" => data_attr(assigns.empty), + "aria-label" => "edit", + "tabindex" => "0", + "hidden" => if(assigns.editing, do: "", else: nil) + } + end + + @spec edit_trigger(EditTrigger.t()) :: map() + def edit_trigger(assigns) do + %{ + "data-scope" => "editable", + "data-part" => "edit-trigger", + "type" => "button", + "dir" => assigns.dir, + "id" => "editable:#{assigns.id}:edit-trigger", + "aria-label" => "edit", + "hidden" => if(assigns.editing, do: "", else: nil) + } + end + + @spec control(Control.t()) :: map() + def control(assigns) do + %{ + "data-scope" => "editable", + "data-part" => "control", + "dir" => assigns.dir, + "id" => "editable:#{assigns.id}:control" + } + end + + @spec triggers(Triggers.t()) :: map() + def triggers(_assigns) do + %{"data-scope" => "editable", "data-part" => "triggers"} + end + + @spec submit_trigger(SubmitTrigger.t()) :: map() + def submit_trigger(assigns) do + %{ + "data-scope" => "editable", + "data-part" => "submit-trigger", + "type" => "button", + "dir" => assigns.dir, + "id" => "editable:#{assigns.id}:submit-trigger", + "aria-label" => "submit", + "hidden" => if(assigns.editing, do: nil, else: "") + } + end + + @spec cancel_trigger(CancelTrigger.t()) :: map() + def cancel_trigger(assigns) do + %{ + "data-scope" => "editable", + "data-part" => "cancel-trigger", + "type" => "button", + "dir" => assigns.dir, + "id" => "editable:#{assigns.id}:cancel-trigger", + "aria-label" => "cancel", + "hidden" => if(assigns.editing, do: nil, else: "") + } + end + + defp maybe_put(map, _key, nil), do: map + defp maybe_put(map, key, value), do: Map.put(map, key, value) +end diff --git a/lib/components/floating_panel.ex b/lib/components/floating_panel.ex new file mode 100644 index 0000000..e8e8366 --- /dev/null +++ b/lib/components/floating_panel.ex @@ -0,0 +1,177 @@ +defmodule Corex.FloatingPanel do + @moduledoc ~S''' + Phoenix implementation of [Zag.js Floating Panel](https://zagjs.com/components/react/floating-panel). + + ## Examples + + ### Basic + + ```heex + <.floating_panel id="my-floating-panel" default_open={false} class="floating-panel"> + <:open_trigger>Close panel + <:closed_trigger>Open panel + <:minimize_trigger> + <.icon name="hero-arrow-down-left" class="icon" /> + + <:maximize_trigger> + <.icon name="hero-arrows-pointing-out" class="icon" /> + + <:default_trigger> + <.icon name="hero-rectangle-stack" class="icon" /> + + <:close_trigger> + <.icon name="hero-x-mark" class="icon" /> + + <:content> +

+ Congue molestie ipsum gravida a. Sed ac eros luctus, cursus turpis + non, pellentesque elit. Pellentesque sagittis fermentum. +

+ + + ``` + + Required slots: `:open_trigger`, `:closed_trigger`, `:minimize_trigger`, `:maximize_trigger`, `:default_trigger`, `:close_trigger`, `:content`. + + ## Styling + + Use data attributes: `[data-scope="floating-panel"][data-part="root"]`, `trigger`, `positioner`, `content`, `title`, `header`, `body`, `drag-trigger`, `resize-trigger`, `close-trigger`, `control`, `stage-trigger`. + ''' + + @doc type: :component + use Phoenix.Component + + alias Corex.FloatingPanel.Anatomy.{ + Props, + Root, + Trigger, + Positioner, + Content, + Title, + Header, + Body, + DragTrigger, + ResizeTrigger, + CloseTrigger, + Control, + StageTrigger + } + + alias Corex.FloatingPanel.Connect + + @resize_axes ~w(n e w s ne se sw nw) + @stages ~w(minimized maximized default) + + attr(:id, :string, required: false) + attr(:open, :boolean, default: nil) + attr(:default_open, :boolean, default: false) + attr(:controlled, :boolean, default: false) + attr(:draggable, :boolean, default: true) + attr(:resizable, :boolean, default: true) + attr(:allow_overflow, :boolean, default: true) + attr(:close_on_escape, :boolean, default: true) + attr(:disabled, :boolean, default: false) + attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"]) + attr(:size, :map, default: nil) + attr(:default_size, :map, default: nil) + attr(:position, :map, default: nil) + attr(:default_position, :map, default: nil) + attr(:min_size, :map, default: nil) + attr(:max_size, :map, default: nil) + attr(:persist_rect, :boolean, default: false) + attr(:grid_size, :integer, default: 1) + attr(:on_open_change, :string, default: nil) + attr(:on_open_change_client, :string, default: nil) + attr(:on_position_change, :string, default: nil) + attr(:on_size_change, :string, default: nil) + attr(:on_stage_change, :string, default: nil) + attr(:rest, :global) + + slot(:open_trigger, required: true) + slot(:closed_trigger, required: true) + slot(:minimize_trigger, required: true) + slot(:maximize_trigger, required: true) + slot(:default_trigger, required: true) + slot(:close_trigger, required: true) + slot(:content, required: true) + + def floating_panel(assigns) do + initial_open = if assigns[:controlled], do: assigns[:open], else: assigns[:default_open] + + assigns = + assigns + |> assign_new(:id, fn -> "floating-panel-#{System.unique_integer([:positive])}" end) + |> assign_new(:dir, fn -> "ltr" end) + |> assign_new(:open, fn -> false end) + |> assign(:initial_open, initial_open) + |> assign(:resize_axes, @resize_axes) + |> assign(:stages, @stages) + + ~H""" +
+
+ +
+
+
+
+
Panel
+
+ + + + +
+
+
+
+ {render_slot(@content)} +
+
+
+
+
+
+ """ + end +end diff --git a/lib/components/floating_panel/anatomy.ex b/lib/components/floating_panel/anatomy.ex new file mode 100644 index 0000000..c269a61 --- /dev/null +++ b/lib/components/floating_panel/anatomy.ex @@ -0,0 +1,144 @@ +defmodule Corex.FloatingPanel.Anatomy do + @moduledoc false + + defmodule Props do + @moduledoc false + @enforce_keys [:id] + + defstruct [ + :id, + :size, + :default_size, + :position, + :default_position, + :min_size, + :max_size, + open: false, + default_open: false, + controlled: false, + draggable: true, + resizable: true, + allow_overflow: true, + close_on_escape: true, + disabled: false, + dir: "ltr", + persist_rect: false, + grid_size: 1, + on_open_change: nil, + on_open_change_client: nil, + on_position_change: nil, + on_size_change: nil, + on_stage_change: nil + ] + + @type t :: %__MODULE__{ + id: String.t(), + open: boolean(), + default_open: boolean(), + controlled: boolean(), + draggable: boolean(), + resizable: boolean(), + allow_overflow: boolean(), + close_on_escape: boolean(), + disabled: boolean(), + dir: String.t(), + size: map() | nil, + default_size: map() | nil, + position: map() | nil, + default_position: map() | nil, + min_size: map() | nil, + max_size: map() | nil, + persist_rect: boolean(), + grid_size: number(), + on_open_change: String.t() | nil, + on_open_change_client: String.t() | nil, + on_position_change: String.t() | nil, + on_size_change: String.t() | nil, + on_stage_change: String.t() | nil + } + end + + defmodule Root do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule Trigger do + @moduledoc false + defstruct [:id, initial_open: true] + + @type t :: %__MODULE__{id: String.t(), initial_open: boolean()} + end + + defmodule Positioner do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule Content do + @moduledoc false + defstruct [:id, initial_open: true] + + @type t :: %__MODULE__{id: String.t(), initial_open: boolean()} + end + + defmodule Title do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule Header do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule Body do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule DragTrigger do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule ResizeTrigger do + @moduledoc false + defstruct [:id, :axis] + + @type t :: %__MODULE__{id: String.t(), axis: String.t()} + end + + defmodule CloseTrigger do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule Control do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule StageTrigger do + @moduledoc false + defstruct [:id, :stage] + + @type t :: %__MODULE__{id: String.t(), stage: String.t()} + end +end diff --git a/lib/components/floating_panel/connect.ex b/lib/components/floating_panel/connect.ex new file mode 100644 index 0000000..b2b759a --- /dev/null +++ b/lib/components/floating_panel/connect.ex @@ -0,0 +1,187 @@ +defmodule Corex.FloatingPanel.Connect do + @moduledoc false + alias Corex.FloatingPanel.Anatomy.{ + Props, + Root, + Trigger, + Positioner, + Content, + Title, + Header, + Body, + DragTrigger, + ResizeTrigger, + CloseTrigger, + Control, + StageTrigger + } + + defp data_attr(true), do: "" + defp data_attr(false), do: nil + defp data_attr(nil), do: nil + + defp encode_size(nil), do: nil + + defp encode_size(%{width: w, height: h}) when is_number(w) and is_number(h), + do: Corex.Json.encode!(%{width: w, height: h}) + + defp encode_size(_), do: nil + + defp encode_point(nil), do: nil + + defp encode_point(%{x: x, y: y}) when is_number(x) and is_number(y), + do: Corex.Json.encode!(%{x: x, y: y}) + + defp encode_point(_), do: nil + + @spec props(Props.t()) :: map() + def props(assigns) do + %{ + "id" => assigns.id, + "data-open" => data_attr(assigns.open), + "data-default-open" => data_attr(assigns.default_open), + "data-controlled" => data_attr(assigns.controlled), + "data-draggable" => data_attr(assigns.draggable), + "data-resizable" => data_attr(assigns.resizable), + "data-allow-overflow" => data_attr(assigns.allow_overflow), + "data-close-on-escape" => data_attr(assigns.close_on_escape), + "data-disabled" => data_attr(assigns.disabled), + "data-dir" => assigns.dir, + "data-size" => encode_size(assigns.size), + "data-default-size" => encode_size(assigns.default_size), + "data-position" => encode_point(assigns.position), + "data-default-position" => encode_point(assigns.default_position), + "data-min-size" => encode_size(assigns.min_size), + "data-max-size" => encode_size(assigns.max_size), + "data-persist-rect" => data_attr(assigns.persist_rect), + "data-grid-size" => to_string(assigns.grid_size), + "data-on-open-change" => assigns.on_open_change, + "data-on-open-change-client" => assigns.on_open_change_client, + "data-on-position-change" => assigns.on_position_change, + "data-on-size-change" => assigns.on_size_change, + "data-on-stage-change" => assigns.on_stage_change + } + end + + @spec root(Root.t()) :: map() + def root(assigns) do + %{ + "data-scope" => "floating-panel", + "data-part" => "root", + "dir" => assigns.dir, + "id" => "floating-panel:#{assigns.id}" + } + end + + @spec trigger(Trigger.t()) :: map() + def trigger(assigns) do + base = %{ + "data-scope" => "floating-panel", + "data-part" => "trigger", + "type" => "button", + "id" => "floating-panel:#{assigns.id}:trigger" + } + + Map.put(base, "data-state", if(assigns.initial_open, do: "open", else: "closed")) + end + + @spec positioner(Positioner.t()) :: map() + def positioner(assigns) do + %{ + "data-scope" => "floating-panel", + "data-part" => "positioner", + "id" => "floating-panel:#{assigns.id}:positioner" + } + end + + @spec content(Content.t()) :: map() + def content(assigns) do + base = %{ + "data-scope" => "floating-panel", + "data-part" => "content", + "id" => "floating-panel:#{assigns.id}:content" + } + + if assigns.initial_open do + base + else + Map.merge(base, %{"data-state" => "closed", "hidden" => ""}) + end + end + + @spec title(Title.t()) :: map() + def title(assigns) do + %{ + "data-scope" => "floating-panel", + "data-part" => "title", + "id" => "floating-panel:#{assigns.id}:title" + } + end + + @spec header(Header.t()) :: map() + def header(assigns) do + %{ + "data-scope" => "floating-panel", + "data-part" => "header", + "id" => "floating-panel:#{assigns.id}:header" + } + end + + @spec body(Body.t()) :: map() + def body(assigns) do + %{ + "data-scope" => "floating-panel", + "data-part" => "body", + "id" => "floating-panel:#{assigns.id}:body" + } + end + + @spec drag_trigger(DragTrigger.t()) :: map() + def drag_trigger(assigns) do + %{ + "data-scope" => "floating-panel", + "data-part" => "drag-trigger", + "id" => "floating-panel:#{assigns.id}:drag-trigger" + } + end + + @spec resize_trigger(ResizeTrigger.t()) :: map() + def resize_trigger(assigns) do + %{ + "data-scope" => "floating-panel", + "data-part" => "resize-trigger", + "data-axis" => assigns.axis, + "id" => "floating-panel:#{assigns.id}:resize:#{assigns.axis}" + } + end + + @spec close_trigger(CloseTrigger.t()) :: map() + def close_trigger(assigns) do + %{ + "data-scope" => "floating-panel", + "data-part" => "close-trigger", + "type" => "button", + "id" => "floating-panel:#{assigns.id}:close" + } + end + + @spec control(Control.t()) :: map() + def control(assigns) do + %{ + "data-scope" => "floating-panel", + "data-part" => "control", + "id" => "floating-panel:#{assigns.id}:control" + } + end + + @spec stage_trigger(StageTrigger.t()) :: map() + def stage_trigger(assigns) do + %{ + "data-scope" => "floating-panel", + "data-part" => "stage-trigger", + "data-stage" => assigns.stage, + "type" => "button", + "id" => "floating-panel:#{assigns.id}:stage:#{assigns.stage}" + } + end +end diff --git a/lib/components/listbox.ex b/lib/components/listbox.ex new file mode 100644 index 0000000..f678849 --- /dev/null +++ b/lib/components/listbox.ex @@ -0,0 +1,308 @@ +defmodule Corex.Listbox do + @moduledoc ~S''' + Phoenix implementation of [Zag.js Listbox](https://zagjs.com/components/react/listbox). + + ## Examples + + + + ### Minimal + + ```heex + <.listbox + id="my-listbox" + class="listbox" + collection={[ + %{label: "France", id: "fra", disabled: true}, + %{label: "Belgium", id: "bel"}, + %{label: "Germany", id: "deu"}, + %{label: "Netherlands", id: "nld"}, + %{label: "Switzerland", id: "che"}, + %{label: "Austria", id: "aut"} + ]} + > + <:label>Choose a country + <:item_indicator> + <.icon name="hero-check" /> + + + ``` + + ### Grouped + + ```heex + <.listbox + class="listbox" + collection={[ + %{label: "France", id: "fra", group: "Europe"}, + %{label: "Belgium", id: "bel", group: "Europe"}, + %{label: "Germany", id: "deu", group: "Europe"}, + %{label: "Netherlands", id: "nld", group: "Europe"}, + %{label: "Switzerland", id: "che", group: "Europe"}, + %{label: "Austria", id: "aut", group: "Europe"}, + %{label: "Japan", id: "jpn", group: "Asia"}, + %{label: "China", id: "chn", group: "Asia"}, + %{label: "South Korea", id: "kor", group: "Asia"}, + %{label: "Thailand", id: "tha", group: "Asia"}, + %{label: "USA", id: "usa", group: "North America"}, + %{label: "Canada", id: "can", group: "North America"}, + %{label: "Mexico", id: "mex", group: "North America"} + ]} + > + <:label>Choose a country + <:item_indicator> + <.icon name="hero-check" /> + + + ``` + + ### Custom + + This example requires the installation of [Flagpack](https://hex.pm/packages/flagpack). + Use the `:item` slot with `:let={%{item: entry}}` to access the entry map. + + ```heex + <.listbox + class="listbox" + collection={[ + %{label: "France", id: "fra"}, + %{label: "Belgium", id: "bel"}, + %{label: "Germany", id: "deu"}, + %{label: "Netherlands", id: "nld"}, + %{label: "Switzerland", id: "che"}, + %{label: "Austria", id: "aut"} + ]} + > + <:label> + Country of residence + + <:item :let={%{item: entry}}> + + {entry.label} + + <:item_indicator> + <.icon name="hero-check" /> + + + ``` + + ### Custom Grouped + + ```heex + <.listbox + class="listbox" + collection={[ + %{label: "France", id: "fra", group: "Europe"}, + %{label: "Belgium", id: "bel", group: "Europe"}, + %{label: "Germany", id: "deu", group: "Europe"}, + %{label: "Japan", id: "jpn", group: "Asia"}, + %{label: "China", id: "chn", group: "Asia"}, + %{label: "South Korea", id: "kor", group: "Asia"} + ]} + > + <:item :let={%{item: entry}}> + + {entry.label} + + <:item_indicator> + <.icon name="hero-check" /> + + + ``` + + + + ## Styling + + Use data attributes: `[data-scope="listbox"][data-part="root"]`, `content`, `item`, `item-text`, `item-indicator`, `item-group`, `item-group-label`. + ''' + + @doc type: :component + use Phoenix.Component + + alias Corex.Listbox.Anatomy.{ + Props, + Root, + Label, + ValueText, + Input, + Content, + ItemGroup, + ItemGroupLabel, + Item, + ItemText, + ItemIndicator + } + + alias Corex.Listbox.Connect + import Corex.Helpers, only: [validate_value!: 1] + + attr(:id, :string, required: false) + + attr(:collection, :list, + required: true, + doc: "List of Corex.List.Item or maps with id/value, label, disabled, group" + ) + + attr(:value, :list, default: [], doc: "Selected value(s)") + attr(:controlled, :boolean, default: false) + attr(:disabled, :boolean, default: false) + attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"]) + attr(:orientation, :string, default: "vertical", values: ["horizontal", "vertical"]) + attr(:loop_focus, :boolean, default: false) + attr(:selection_mode, :string, default: "single", values: ["single", "multiple", "extended"]) + attr(:select_on_highlight, :boolean, default: false) + attr(:deselectable, :boolean, default: false) + attr(:typeahead, :boolean, default: false) + attr(:on_value_change, :string, default: nil) + attr(:on_value_change_client, :string, default: nil) + attr(:aria_label, :string, default: nil, doc: "Accessible name when no label slot is provided") + attr(:rest, :global) + + slot(:label, required: false) + slot(:item, required: false) + slot(:item_indicator, required: false) + + def listbox(assigns) do + items = normalize_collection(assigns.collection) + has_groups = Enum.any?(items, &Map.get(&1, :group)) + + groups = + if has_groups, + do: items |> Enum.map(& &1.group) |> Enum.uniq() |> Enum.reject(&is_nil/1), + else: [] + + assigns = + assigns + |> assign_new(:id, fn -> "listbox-#{System.unique_integer([:positive])}" end) + |> assign_new(:dir, fn -> "ltr" end) + |> assign(:value, validate_value!(assigns[:value] || [])) + |> assign(:collection, items) + |> assign(:items, items) + |> assign(:has_groups, has_groups) + |> assign(:groups, groups) + + ~H""" +
+
+ + + +
+
+
{group_id}
+
+ {entry[:label]} + <%= for item_slot <- @item || [] do %> + <%= render_slot(item_slot, %{item: entry, value: entry_value(entry), label: entry[:label]}) %> + <% end %> + +
+
+
+ {entry[:label]} + <%= for item_slot <- @item || [] do %> + <%= render_slot(item_slot, %{item: entry, value: entry_value(entry), label: entry[:label]}) %> + <% end %> + +
+
+
+
+ """ + end + + defp entry_value(entry) do + to_string( + Map.get(entry, :value) || Map.get(entry, :id) || Map.get(entry, "value") || + Map.get(entry, "id") || "" + ) + end + + defp entry_selected?(entry, value_list) do + Enum.member?(value_list, entry_value(entry)) + end + + defp content_attrs(id, dir, orientation, has_label) do + Connect.content(%Content{id: id, dir: dir}) + |> Map.put("data-layout", "list") + |> Map.put("data-orientation", orientation) + |> then(fn attrs -> + if has_label, do: Map.put(attrs, "aria-labelledby", "select:#{id}:label"), else: attrs + end) + end + + defp item_attrs(id, entry) do + base = Connect.item(%Item{id: id, item: entry, value: entry_value(entry)}) + + if Map.get(entry, :disabled) do + base + |> Map.put("data-disabled", "") + |> Map.put("aria-disabled", "true") + else + base + end + end + + defp normalize_collection(items) when is_list(items) do + Enum.map(items, fn + %Corex.List.Item{} = item -> + %{ + id: item.id, + value: item.id, + label: item.label, + disabled: item.disabled, + group: item.group + } + + m when is_map(m) -> + %{ + id: Map.get(m, :id), + value: Map.get(m, :value) || Map.get(m, :id), + label: Map.get(m, :label), + disabled: !!Map.get(m, :disabled), + group: Map.get(m, :group) + } + end) + end +end diff --git a/lib/components/listbox/anatomy.ex b/lib/components/listbox/anatomy.ex new file mode 100644 index 0000000..d42eacc --- /dev/null +++ b/lib/components/listbox/anatomy.ex @@ -0,0 +1,112 @@ +defmodule Corex.Listbox.Anatomy do + @moduledoc false + + defmodule Props do + @moduledoc false + @enforce_keys [:id] + + defstruct [ + :id, + collection: [], + value: [], + controlled: false, + disabled: false, + dir: "ltr", + orientation: "vertical", + loop_focus: false, + selection_mode: "single", + select_on_highlight: false, + deselectable: false, + typeahead: false, + on_value_change: nil, + on_value_change_client: nil + ] + + @type t :: %__MODULE__{ + id: String.t(), + collection: list(map()), + value: list(String.t()), + controlled: boolean(), + disabled: boolean(), + dir: String.t(), + orientation: String.t(), + loop_focus: boolean(), + selection_mode: String.t(), + select_on_highlight: boolean(), + deselectable: boolean(), + typeahead: boolean(), + on_value_change: String.t() | nil, + on_value_change_client: String.t() | nil + } + end + + defmodule Root do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule Label do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule ValueText do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule Input do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule Content do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule ItemGroup do + @moduledoc false + defstruct [:id, :group_id] + + @type t :: %__MODULE__{id: String.t(), group_id: String.t()} + end + + defmodule ItemGroupLabel do + @moduledoc false + defstruct [:id, :html_for] + + @type t :: %__MODULE__{id: String.t(), html_for: String.t()} + end + + defmodule Item do + @moduledoc false + defstruct [:id, :item, :value] + + @type t :: %__MODULE__{id: String.t(), item: map(), value: String.t()} + end + + defmodule ItemText do + @moduledoc false + defstruct [:id, :item] + + @type t :: %__MODULE__{id: String.t(), item: map()} + end + + defmodule ItemIndicator do + @moduledoc false + defstruct [:id, :item] + + @type t :: %__MODULE__{id: String.t(), item: map()} + end +end diff --git a/lib/components/listbox/connect.ex b/lib/components/listbox/connect.ex new file mode 100644 index 0000000..1e2c7b6 --- /dev/null +++ b/lib/components/listbox/connect.ex @@ -0,0 +1,179 @@ +defmodule Corex.Listbox.Connect do + @moduledoc false + alias Corex.Listbox.Anatomy.{ + Props, + Root, + Label, + ValueText, + Input, + Content, + ItemGroup, + ItemGroupLabel, + Item, + ItemText, + ItemIndicator + } + + import Corex.Helpers, only: [validate_value!: 1] + + defp data_attr(true), do: "" + defp data_attr(false), do: nil + defp data_attr(nil), do: nil + + defp encode_collection(items) when is_list(items) do + Enum.map(items, fn + %Corex.List.Item{} = item -> + %{ + "id" => item.id, + "value" => item.id, + "label" => item.label, + "disabled" => !!item.disabled, + "group" => item.group + } + + m when is_map(m) -> + %{ + "id" => Map.get(m, :id) || Map.get(m, "id"), + "value" => Map.get(m, :value) || Map.get(m, "value") || Map.get(m, :id), + "label" => Map.get(m, :label) || Map.get(m, "label"), + "disabled" => !!Map.get(m, :disabled, false), + "group" => Map.get(m, :group) + } + end) + end + + @spec props(Props.t()) :: map() + def props(assigns) do + %{ + "id" => assigns.id, + "data-collection" => Corex.Json.encode!(encode_collection(assigns.collection)), + "data-value" => + if assigns.controlled do + Enum.join(validate_value!(assigns.value), ",") + else + nil + end, + "data-default-value" => + if assigns.controlled do + nil + else + Enum.join(validate_value!(assigns.value), ",") + end, + "data-controlled" => data_attr(assigns.controlled), + "data-disabled" => data_attr(assigns.disabled), + "data-dir" => assigns.dir, + "data-orientation" => assigns.orientation, + "data-loop-focus" => data_attr(assigns.loop_focus), + "data-selection-mode" => assigns.selection_mode, + "data-select-on-highlight" => data_attr(assigns.select_on_highlight), + "data-deselectable" => data_attr(assigns.deselectable), + "data-typeahead" => data_attr(assigns.typeahead), + "data-on-value-change" => assigns.on_value_change, + "data-on-value-change-client" => assigns.on_value_change_client + } + end + + @spec root(Root.t()) :: map() + def root(assigns) do + %{ + "data-scope" => "listbox", + "data-part" => "root", + "dir" => assigns.dir, + "id" => "listbox:#{assigns.id}" + } + end + + @spec label(Label.t()) :: map() + def label(assigns) do + %{ + "data-scope" => "listbox", + "data-part" => "label", + "dir" => assigns.dir, + "id" => "select:#{assigns.id}:label" + } + end + + @spec value_text(ValueText.t()) :: map() + def value_text(assigns) do + %{ + "data-scope" => "listbox", + "data-part" => "value-text", + "id" => "listbox:#{assigns.id}:value-text" + } + end + + @spec input(Input.t()) :: map() + def input(assigns) do + %{ + "data-scope" => "listbox", + "data-part" => "input", + "id" => "listbox:#{assigns.id}:input" + } + end + + @spec content(Content.t()) :: map() + def content(assigns) do + %{ + "data-scope" => "listbox", + "data-part" => "content", + "dir" => assigns.dir, + "id" => "listbox:#{assigns.id}:content" + } + end + + @spec item_group(ItemGroup.t()) :: map() + def item_group(assigns) do + %{ + "data-scope" => "listbox", + "data-part" => "item-group", + "data-id" => assigns.group_id, + "id" => "listbox:#{assigns.id}:item-group:#{assigns.group_id}" + } + end + + @spec item_group_label(ItemGroupLabel.t()) :: map() + def item_group_label(assigns) do + %{ + "data-scope" => "listbox", + "data-part" => "item-group-label", + "id" => "listbox:#{assigns.id}:item-group-label:#{assigns.html_for}" + } + end + + @spec item(Item.t()) :: map() + def item(assigns) do + %{ + "data-scope" => "listbox", + "data-part" => "item", + "data-value" => assigns.value, + "id" => "listbox:#{assigns.id}:item:#{assigns.value}" + } + end + + defp item_value(item) do + Map.get(item, :value) || Map.get(item, "value") || Map.get(item, :id) || Map.get(item, "id") || + "" + end + + @spec item_text(ItemText.t()) :: map() + def item_text(assigns) do + val = item_value(assigns.item) + + %{ + "data-scope" => "listbox", + "data-part" => "item-text", + "id" => "listbox:#{assigns.id}:item-text:#{val}" + } + end + + @spec item_indicator(ItemIndicator.t()) :: map() + def item_indicator(assigns) do + val = item_value(assigns.item) + + %{ + "data-scope" => "listbox", + "data-part" => "item-indicator", + "id" => "listbox:#{assigns.id}:item-indicator:#{val}" + } + end +end diff --git a/lib/components/number_input.ex b/lib/components/number_input.ex new file mode 100644 index 0000000..8d4e15b --- /dev/null +++ b/lib/components/number_input.ex @@ -0,0 +1,142 @@ +defmodule Corex.NumberInput do + @moduledoc ~S''' + Phoenix implementation of [Zag.js Number Input](https://zagjs.com/components/react/number-input). + + ## Examples + + + + ### Basic + + ```heex + <.number_input id="num" class="number-input"> + <:label>Quantity + + ``` + + ### With triggers + + ```heex + <.number_input id="num" class="number-input"> + <:label>Quantity + <:decrement_trigger><.icon name="hero-chevron-down" class="icon" /> + <:increment_trigger><.icon name="hero-chevron-up" class="icon" /> + + ``` + + ### With scrubber + + ```heex + <.number_input id="num" scrubber class="number-input"> + <:label>Quantity + <:scrubber_trigger><.icon name="hero-arrows-up-down" class="icon rotate-90" /> + + ``` + + + + Optional slots `:decrement_trigger`, `:increment_trigger`, and `:scrubber_trigger` render the button content (e.g. icons). When omitted, no content is shown. + + ## Styling + + Use data attributes: `[data-scope="number-input"][data-part="root"]`, `control`, `input`, `trigger-group`, `decrement-trigger`, `increment-trigger`, `scrubber`. + ''' + + @doc type: :component + use Phoenix.Component + + alias Corex.NumberInput.Anatomy.{ + Props, + Root, + Label, + Control, + Input, + TriggerGroup, + DecrementTrigger, + IncrementTrigger, + Scrubber + } + + alias Corex.NumberInput.Connect + + attr(:id, :string, required: false) + attr(:value, :string, default: nil) + attr(:default_value, :string, default: nil) + attr(:controlled, :boolean, default: false) + attr(:min, :float, default: nil) + attr(:max, :float, default: nil) + attr(:step, :float, default: 1.0) + attr(:disabled, :boolean, default: false) + attr(:read_only, :boolean, default: false) + attr(:invalid, :boolean, default: false) + attr(:required, :boolean, default: false) + attr(:allow_mouse_wheel, :boolean, default: false) + attr(:name, :string, default: nil) + attr(:form, :string, default: nil) + attr(:on_value_change, :string, default: nil) + attr(:on_value_change_client, :string, default: nil) + + attr(:scrubber, :boolean, + default: false, + doc: "When true, show scrubber instead of increment/decrement buttons" + ) + + attr(:rest, :global) + + slot(:label, required: false) + slot(:decrement_trigger, required: false) + slot(:increment_trigger, required: false) + slot(:scrubber_trigger, required: false) + + def number_input(assigns) do + assigns = + assigns + |> assign_new(:id, fn -> "number-input-#{System.unique_integer([:positive])}" end) + + ~H""" +
+
+ +
+ +
+ + + +
+
+
+
+ """ + end +end diff --git a/lib/components/number_input/anatomy.ex b/lib/components/number_input/anatomy.ex new file mode 100644 index 0000000..de4f33b --- /dev/null +++ b/lib/components/number_input/anatomy.ex @@ -0,0 +1,109 @@ +defmodule Corex.NumberInput.Anatomy do + @moduledoc false + + defmodule Props do + @moduledoc false + @enforce_keys [:id] + + defstruct [ + :id, + value: nil, + default_value: nil, + controlled: false, + min: nil, + max: nil, + step: 1, + disabled: false, + read_only: false, + invalid: false, + required: false, + allow_mouse_wheel: false, + name: nil, + form: nil, + on_value_change: nil, + on_value_change_client: nil + ] + + @type t :: %__MODULE__{ + id: String.t(), + value: String.t() | nil, + default_value: String.t() | nil, + controlled: boolean(), + min: number() | nil, + max: number() | nil, + step: number(), + disabled: boolean(), + read_only: boolean(), + invalid: boolean(), + required: boolean(), + allow_mouse_wheel: boolean(), + name: String.t() | nil, + form: String.t() | nil, + on_value_change: String.t() | nil, + on_value_change_client: String.t() | nil + } + end + + defmodule Root do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule Label do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule Control do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule ValueText do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule Input do + @moduledoc false + defstruct [:id, :disabled] + + @type t :: %__MODULE__{id: String.t(), disabled: boolean()} + end + + defmodule TriggerGroup do + @moduledoc false + defstruct [] + + @type t :: %__MODULE__{} + end + + defmodule DecrementTrigger do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule IncrementTrigger do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule Scrubber do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end +end diff --git a/lib/components/number_input/connect.ex b/lib/components/number_input/connect.ex new file mode 100644 index 0000000..ae35c52 --- /dev/null +++ b/lib/components/number_input/connect.ex @@ -0,0 +1,121 @@ +defmodule Corex.NumberInput.Connect do + @moduledoc false + alias Corex.NumberInput.Anatomy.{ + Props, + Root, + Label, + Control, + Input, + TriggerGroup, + DecrementTrigger, + IncrementTrigger, + Scrubber + } + + defp data_attr(true), do: "" + defp data_attr(false), do: nil + defp data_attr(nil), do: nil + + defp num_attr(nil), do: nil + defp num_attr(n) when is_number(n), do: to_string(n) + + @spec props(Props.t()) :: map() + def props(assigns) do + %{ + "id" => assigns.id, + "data-value" => if(assigns.controlled, do: assigns.value, else: nil), + "data-default-value" => if(assigns.controlled, do: nil, else: assigns.default_value), + "data-controlled" => data_attr(assigns.controlled), + "data-min" => num_attr(assigns.min), + "data-max" => num_attr(assigns.max), + "data-step" => num_attr(assigns.step), + "data-disabled" => data_attr(assigns.disabled), + "data-read-only" => data_attr(assigns.read_only), + "data-invalid" => data_attr(assigns.invalid), + "data-required" => data_attr(assigns.required), + "data-allow-mouse-wheel" => data_attr(assigns.allow_mouse_wheel), + "data-name" => assigns.name, + "data-form" => assigns.form, + "data-on-value-change" => assigns.on_value_change, + "data-on-value-change-client" => assigns.on_value_change_client + } + end + + @spec root(Root.t()) :: map() + def root(assigns) do + %{ + "data-scope" => "number-input", + "data-part" => "root", + "id" => "number-input:#{assigns.id}" + } + end + + @spec label(Label.t()) :: map() + def label(assigns) do + %{ + "data-scope" => "number-input", + "data-part" => "label", + "id" => "number-input:#{assigns.id}:label", + "for" => "number-input:#{assigns.id}:input" + } + end + + @spec control(Control.t()) :: map() + def control(assigns) do + %{ + "data-scope" => "number-input", + "data-part" => "control", + "id" => "number-input:#{assigns.id}:control" + } + end + + @spec trigger_group(TriggerGroup.t()) :: map() + def trigger_group(_assigns) do + %{ + "data-part" => "trigger-group" + } + end + + @spec input(Input.t()) :: map() + def input(assigns) do + %{ + "data-scope" => "number-input", + "data-part" => "input", + "disabled" => data_attr(assigns.disabled), + "id" => "number-input:#{assigns.id}:input" + } + end + + @spec decrement_trigger(DecrementTrigger.t()) :: map() + def decrement_trigger(assigns) do + %{ + "data-scope" => "number-input", + "data-part" => "decrement-trigger", + "type" => "button", + "id" => "number-input:#{assigns.id}:dec", + "aria-label" => "Decrease value" + } + end + + @spec increment_trigger(IncrementTrigger.t()) :: map() + def increment_trigger(assigns) do + %{ + "data-scope" => "number-input", + "data-part" => "increment-trigger", + "type" => "button", + "id" => "number-input:#{assigns.id}:inc", + "aria-label" => "Increase value" + } + end + + @spec scrubber(Scrubber.t()) :: map() + def scrubber(assigns) do + %{ + "data-scope" => "number-input", + "data-part" => "scrubber", + "type" => "button", + "id" => "number-input:#{assigns.id}:scrubber", + "aria-label" => "Scrub to adjust value" + } + end +end diff --git a/lib/components/password_input.ex b/lib/components/password_input.ex new file mode 100644 index 0000000..f045d91 --- /dev/null +++ b/lib/components/password_input.ex @@ -0,0 +1,107 @@ +defmodule Corex.PasswordInput do + @moduledoc ~S''' + Phoenix implementation of [Zag.js Password Input](https://zagjs.com/components/react/password-input). + + ## Examples + + ### Basic + + ```heex + <.password_input id="pwd" class="password-input"> + <:label>Password + <:visible_indicator><.icon name="hero-eye" /> + <:hidden_indicator><.icon name="hero-eye-slash" /> + + ``` + + ## Styling + + Use data attributes: `[data-scope="password-input"][data-part="root"]`, `label`, `control`, `input`, `visibility-trigger`, `indicator`. + ''' + + @doc type: :component + use Phoenix.Component + + alias Corex.PasswordInput.Anatomy.{ + Props, + Root, + Label, + Control, + Input, + VisibilityTrigger, + Indicator + } + + alias Corex.PasswordInput.Connect + + attr(:id, :string, required: false) + attr(:visible, :boolean, default: false) + attr(:controlled_visible, :boolean, default: false) + attr(:disabled, :boolean, default: false) + attr(:invalid, :boolean, default: false) + attr(:read_only, :boolean, default: false) + attr(:required, :boolean, default: false) + attr(:ignore_password_managers, :boolean, default: false) + attr(:name, :string, default: nil) + attr(:form, :string, default: nil) + attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"]) + + attr(:auto_complete, :string, + default: "current-password", + values: ["current-password", "new-password"] + ) + + attr(:on_visibility_change, :string, default: nil) + attr(:on_visibility_change_client, :string, default: nil) + attr(:rest, :global) + + slot(:label, required: false) + slot(:visible_indicator, required: true, doc: "Icon shown when password is visible") + slot(:hidden_indicator, required: true, doc: "Icon shown when password is hidden") + + def password_input(assigns) do + assigns = + assigns + |> assign_new(:id, fn -> "password-input-#{System.unique_integer([:positive])}" end) + |> assign_new(:dir, fn -> "ltr" end) + + ~H""" +
+
+ +
+ + +
+
+
+ """ + end +end diff --git a/lib/components/password_input/anatomy.ex b/lib/components/password_input/anatomy.ex new file mode 100644 index 0000000..0bf86a2 --- /dev/null +++ b/lib/components/password_input/anatomy.ex @@ -0,0 +1,84 @@ +defmodule Corex.PasswordInput.Anatomy do + @moduledoc false + + defmodule Props do + @moduledoc false + @enforce_keys [:id] + + defstruct [ + :id, + visible: false, + controlled_visible: false, + disabled: false, + invalid: false, + read_only: false, + required: false, + ignore_password_managers: false, + name: nil, + form: nil, + dir: "ltr", + auto_complete: "current-password", + on_visibility_change: nil, + on_visibility_change_client: nil + ] + + @type t :: %__MODULE__{ + id: String.t(), + visible: boolean(), + controlled_visible: boolean(), + disabled: boolean(), + invalid: boolean(), + read_only: boolean(), + required: boolean(), + ignore_password_managers: boolean(), + name: String.t() | nil, + form: String.t() | nil, + dir: String.t(), + auto_complete: String.t(), + on_visibility_change: String.t() | nil, + on_visibility_change_client: String.t() | nil + } + end + + defmodule Root do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule Label do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule Control do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule Input do + @moduledoc false + defstruct [:id, :disabled] + + @type t :: %__MODULE__{id: String.t(), disabled: boolean()} + end + + defmodule VisibilityTrigger do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule Indicator do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end +end diff --git a/lib/components/password_input/connect.ex b/lib/components/password_input/connect.ex new file mode 100644 index 0000000..c38847e --- /dev/null +++ b/lib/components/password_input/connect.ex @@ -0,0 +1,99 @@ +defmodule Corex.PasswordInput.Connect do + @moduledoc false + alias Corex.PasswordInput.Anatomy.{ + Props, + Root, + Label, + Control, + Input, + VisibilityTrigger, + Indicator + } + + defp data_attr(true), do: "" + defp data_attr(false), do: nil + defp data_attr(nil), do: nil + + @spec props(Props.t()) :: map() + def props(assigns) do + %{ + "id" => assigns.id, + "data-visible" => if(assigns.controlled_visible, do: data_attr(assigns.visible), else: nil), + "data-default-visible" => + if(assigns.controlled_visible, do: nil, else: data_attr(assigns.visible)), + "data-controlled-visible" => data_attr(assigns.controlled_visible), + "data-disabled" => data_attr(assigns.disabled), + "data-invalid" => data_attr(assigns.invalid), + "data-read-only" => data_attr(assigns.read_only), + "data-required" => data_attr(assigns.required), + "data-ignore-password-managers" => data_attr(assigns.ignore_password_managers), + "data-name" => assigns.name, + "data-form" => assigns.form, + "data-dir" => assigns.dir, + "data-auto-complete" => assigns.auto_complete, + "data-on-visibility-change" => assigns.on_visibility_change, + "data-on-visibility-change-client" => assigns.on_visibility_change_client + } + end + + @spec root(Root.t()) :: map() + def root(assigns) do + %{ + "data-scope" => "password-input", + "data-part" => "root", + "dir" => assigns.dir, + "id" => "password-input:#{assigns.id}" + } + end + + @spec label(Label.t()) :: map() + def label(assigns) do + %{ + "data-scope" => "password-input", + "data-part" => "label", + "dir" => assigns.dir, + "for" => "p-input-#{assigns.id}-input" + } + end + + @spec control(Control.t()) :: map() + def control(assigns) do + %{ + "data-scope" => "password-input", + "data-part" => "control", + "dir" => assigns.dir, + "id" => "password-input:#{assigns.id}:control" + } + end + + @spec input(Input.t()) :: map() + def input(assigns) do + %{ + "data-scope" => "password-input", + "data-part" => "input", + "disabled" => data_attr(assigns.disabled), + "id" => "p-input-#{assigns.id}-input" + } + end + + @spec visibility_trigger(VisibilityTrigger.t()) :: map() + def visibility_trigger(assigns) do + %{ + "data-scope" => "password-input", + "data-part" => "visibility-trigger", + "type" => "button", + "dir" => assigns.dir, + "aria-label" => "Toggle password visibility" + } + end + + @spec indicator(Indicator.t()) :: map() + def indicator(assigns) do + %{ + "data-scope" => "password-input", + "data-part" => "indicator", + "dir" => assigns.dir, + "aria-hidden" => "true" + } + end +end diff --git a/lib/components/pin_input.ex b/lib/components/pin_input.ex new file mode 100644 index 0000000..ba89450 --- /dev/null +++ b/lib/components/pin_input.ex @@ -0,0 +1,110 @@ +defmodule Corex.PinInput do + @moduledoc ~S''' + Phoenix implementation of [Zag.js Pin Input](https://zagjs.com/components/react/pin-input). + + ## Examples + + ### Basic + + ```heex + <.pin_input id="pin" count={4} class="pin-input"> + <:label>Code + + ``` + + ## Styling + + Use data attributes: `[data-scope="pin-input"][data-part="root"]`, `label`, `control`, `input` (per index). + ''' + + @doc type: :component + use Phoenix.Component + + alias Corex.PinInput.Anatomy.{Props, Root, Label, HiddenInput, Control, Input} + alias Corex.PinInput.Connect + import Corex.Helpers, only: [validate_value!: 1] + + attr(:id, :string, required: false) + attr(:value, :list, default: [], doc: "Controlled value (list of single chars)") + attr(:default_value, :list, default: [], doc: "Uncontrolled initial value") + attr(:controlled, :boolean, default: false) + attr(:count, :integer, default: 4, doc: "Number of input boxes") + attr(:disabled, :boolean, default: false) + attr(:invalid, :boolean, default: false) + attr(:required, :boolean, default: false) + attr(:read_only, :boolean, default: false) + attr(:mask, :boolean, default: false) + attr(:otp, :boolean, default: false) + attr(:blur_on_complete, :boolean, default: false) + attr(:select_on_focus, :boolean, default: false) + attr(:name, :string, default: nil) + attr(:form, :string, default: nil) + attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"]) + attr(:type, :string, default: "numeric", values: ["alphanumeric", "numeric", "alphabetic"]) + attr(:placeholder, :string, default: "○") + attr(:on_value_change, :string, default: nil) + attr(:on_value_change_client, :string, default: nil) + attr(:on_value_complete, :string, default: nil) + attr(:rest, :global) + + slot(:label, required: false) + + def pin_input(assigns) do + assigns = + assigns + |> assign_new(:id, fn -> "pin-input-#{System.unique_integer([:positive])}" end) + |> assign_new(:dir, fn -> "ltr" end) + |> assign(:value, validate_value!(assigns[:value] || [])) + |> assign(:default_value, validate_value!(assigns[:default_value] || [])) + + value_str = Enum.join(assigns.value, "") + default_value_str = Enum.join(assigns.default_value, "") + + ~H""" +
+
+ + +
+ +
+
+
+ """ + end +end diff --git a/lib/components/pin_input/anatomy.ex b/lib/components/pin_input/anatomy.ex new file mode 100644 index 0000000..941e4dd --- /dev/null +++ b/lib/components/pin_input/anatomy.ex @@ -0,0 +1,89 @@ +defmodule Corex.PinInput.Anatomy do + @moduledoc false + + defmodule Props do + @moduledoc false + @enforce_keys [:id] + + defstruct [ + :id, + value: [], + controlled: false, + count: 4, + disabled: false, + invalid: false, + required: false, + read_only: false, + mask: false, + otp: false, + blur_on_complete: false, + select_on_focus: false, + name: nil, + form: nil, + dir: "ltr", + type: "numeric", + placeholder: "○", + on_value_change: nil, + on_value_change_client: nil, + on_value_complete: nil + ] + + @type t :: %__MODULE__{ + id: String.t(), + value: list(String.t()), + controlled: boolean(), + count: non_neg_integer(), + disabled: boolean(), + invalid: boolean(), + required: boolean(), + read_only: boolean(), + mask: boolean(), + otp: boolean(), + blur_on_complete: boolean(), + select_on_focus: boolean(), + name: String.t() | nil, + form: String.t() | nil, + dir: String.t(), + type: String.t(), + placeholder: String.t(), + on_value_change: String.t() | nil, + on_value_change_client: String.t() | nil, + on_value_complete: String.t() | nil + } + end + + defmodule Root do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule Label do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule HiddenInput do + @moduledoc false + defstruct [:id, :name, :value] + + @type t :: %__MODULE__{id: String.t(), name: String.t() | nil, value: String.t()} + end + + defmodule Control do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule Input do + @moduledoc false + defstruct [:id, :index] + + @type t :: %__MODULE__{id: String.t(), index: non_neg_integer()} + end +end diff --git a/lib/components/pin_input/connect.ex b/lib/components/pin_input/connect.ex new file mode 100644 index 0000000..598e1dc --- /dev/null +++ b/lib/components/pin_input/connect.ex @@ -0,0 +1,96 @@ +defmodule Corex.PinInput.Connect do + @moduledoc false + alias Corex.PinInput.Anatomy.{Props, Root, Label, HiddenInput, Control, Input} + import Corex.Helpers, only: [validate_value!: 1] + + defp data_attr(true), do: "" + defp data_attr(false), do: nil + defp data_attr(nil), do: nil + + @spec props(Props.t()) :: map() + def props(assigns) do + value_str = + if is_list(assigns.value), do: Enum.join(validate_value!(assigns.value), ","), else: "" + + default_value_str = if assigns.controlled, do: nil, else: value_str + + %{ + "id" => assigns.id, + "data-value" => if(assigns.controlled, do: value_str, else: nil), + "data-default-value" => default_value_str, + "data-controlled" => data_attr(assigns.controlled), + "data-count" => to_string(assigns.count), + "data-disabled" => data_attr(assigns.disabled), + "data-invalid" => data_attr(assigns.invalid), + "data-required" => data_attr(assigns.required), + "data-read-only" => data_attr(assigns.read_only), + "data-mask" => data_attr(assigns.mask), + "data-otp" => data_attr(assigns.otp), + "data-blur-on-complete" => data_attr(assigns.blur_on_complete), + "data-select-on-focus" => data_attr(assigns.select_on_focus), + "data-name" => assigns.name, + "data-form" => assigns.form, + "data-dir" => assigns.dir, + "data-type" => assigns.type, + "data-placeholder" => assigns.placeholder, + "data-on-value-change" => assigns.on_value_change, + "data-on-value-change-client" => assigns.on_value_change_client, + "data-on-value-complete" => assigns.on_value_complete + } + end + + @spec root(Root.t()) :: map() + def root(assigns) do + %{ + "data-scope" => "pin-input", + "data-part" => "root", + "dir" => assigns.dir, + "id" => "pin-input:#{assigns.id}" + } + end + + @spec label(Label.t()) :: map() + def label(assigns) do + %{ + "data-scope" => "pin-input", + "data-part" => "label", + "dir" => assigns.dir, + "id" => "pin-input:#{assigns.id}:label" + } + end + + @spec hidden_input(HiddenInput.t()) :: map() + def hidden_input(assigns) do + %{ + "data-scope" => "pin-input", + "data-part" => "hidden-input", + "type" => "hidden", + "name" => assigns.name, + "value" => assigns.value, + "id" => "pin-input:#{assigns.id}:hidden-input" + } + end + + @spec control(Control.t()) :: map() + def control(assigns) do + %{ + "data-scope" => "pin-input", + "data-part" => "control", + "dir" => assigns.dir, + "id" => "pin-input:#{assigns.id}:control" + } + end + + @spec input(Input.t()) :: map() + def input(assigns) do + digit = assigns.index + 1 + + %{ + "data-scope" => "pin-input", + "data-part" => "input", + "data-index" => to_string(assigns.index), + "id" => "pin-input:#{assigns.id}:input:#{assigns.index}", + "aria-label" => "Digit #{digit}" + } + end +end diff --git a/lib/components/radio_group.ex b/lib/components/radio_group.ex new file mode 100644 index 0000000..50c3bb5 --- /dev/null +++ b/lib/components/radio_group.ex @@ -0,0 +1,168 @@ +defmodule Corex.RadioGroup do + @moduledoc ~S''' + Phoenix implementation of [Zag.js Radio Group](https://zagjs.com/components/react/radio-group). + + ## Examples + + ### Basic (without indicator) + + ```heex + <.radio_group id="rg" name="choice" items={[["1", "Option A"], ["2", "Option B"]]} class="radio-group"> + <:label>Choose one + + ``` + + ### With indicator + + ```heex + <.radio_group id="rg" name="choice" items={[["1", "Option A"], ["2", "Option B"]]} class="radio-group"> + <:label>Choose one + <:item_control><.icon name="hero-check" class="data-checked" /> + + ``` + + Items can be a list of `{value, label}` tuples or a list of maps with `:value`, `:label`, and optional `:disabled`, `:invalid`. Optional `:item_control` slot renders the check indicator for each item; when omitted, no indicator is shown. + + ## Styling + + Use data attributes: `[data-scope="radio-group"][data-part="root"]`, `label`, `indicator`, `item`, `item-text`, `item-control`, `item-hidden-input`. + ''' + + @doc type: :component + use Phoenix.Component + + alias Corex.RadioGroup.Anatomy.{ + Props, + Root, + Label, + Indicator, + Item, + ItemText, + ItemControl, + ItemHiddenInput + } + + alias Corex.RadioGroup.Connect + + attr(:id, :string, required: false) + attr(:value, :string, default: nil) + attr(:default_value, :string, default: nil) + attr(:controlled, :boolean, default: false) + attr(:name, :string, default: nil) + attr(:form, :string, default: nil) + attr(:disabled, :boolean, default: false) + attr(:invalid, :boolean, default: false) + attr(:required, :boolean, default: false) + attr(:read_only, :boolean, default: false) + attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"]) + attr(:orientation, :string, default: "vertical", values: ["horizontal", "vertical"]) + attr(:on_value_change, :string, default: nil) + attr(:on_value_change_client, :string, default: nil) + + attr(:items, :list, + required: true, + doc: "List of [value, label] or %{value: ..., label: ..., disabled: ..., invalid: ...}" + ) + + attr(:rest, :global) + + slot(:label, required: false) + slot(:item_control, required: false) + slot(:item, required: false) + + def radio_group(assigns) do + assigns = + assigns + |> assign_new(:id, fn -> "radio-group-#{System.unique_integer([:positive])}" end) + |> assign_new(:dir, fn -> "ltr" end) + |> assign(:items, normalize_items(assigns.items)) + + ~H""" +
+
+
+ {render_slot(@label)} +
+
+ + +
+
+ """ + end + + defp normalize_items(items) when is_list(items) do + Enum.map(items, fn + {value, label} -> + %{value: to_string(value), label: to_string(label), disabled: false, invalid: false} + + [value, label] -> + %{value: to_string(value), label: to_string(label), disabled: false, invalid: false} + + %{value: v, label: l} = m -> + %{ + value: to_string(v), + label: to_string(l), + disabled: !!Map.get(m, :disabled), + invalid: !!Map.get(m, :invalid) + } + + other -> + raise ArgumentError, + "radio_group items must be {value, label}, [value, label], or %{value: ..., label: ...}, got: #{inspect(other)}" + end) + end +end diff --git a/lib/components/radio_group/anatomy.ex b/lib/components/radio_group/anatomy.ex new file mode 100644 index 0000000..75c29c9 --- /dev/null +++ b/lib/components/radio_group/anatomy.ex @@ -0,0 +1,119 @@ +defmodule Corex.RadioGroup.Anatomy do + @moduledoc false + + defmodule Props do + @moduledoc false + @enforce_keys [:id] + + defstruct [ + :id, + value: nil, + controlled: false, + name: nil, + form: nil, + disabled: false, + invalid: false, + required: false, + read_only: false, + dir: "ltr", + orientation: "vertical", + on_value_change: nil, + on_value_change_client: nil + ] + + @type t :: %__MODULE__{ + id: String.t(), + value: String.t() | nil, + controlled: boolean(), + name: String.t() | nil, + form: String.t() | nil, + disabled: boolean(), + invalid: boolean(), + required: boolean(), + read_only: boolean(), + dir: String.t(), + orientation: String.t(), + on_value_change: String.t() | nil, + on_value_change_client: String.t() | nil + } + end + + defmodule Root do + @moduledoc false + defstruct [:id, :dir, :orientation, :has_label] + + @type t :: %__MODULE__{ + id: String.t(), + dir: String.t(), + orientation: String.t(), + has_label: boolean() + } + end + + defmodule Label do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule Indicator do + @moduledoc false + defstruct [:id, :dir] + + @type t :: %__MODULE__{id: String.t(), dir: String.t()} + end + + defmodule Item do + @moduledoc false + defstruct [:id, :value, :disabled, :invalid, :checked] + + @type t :: %__MODULE__{ + id: String.t(), + value: String.t(), + disabled: boolean(), + invalid: boolean(), + checked: boolean() + } + end + + defmodule ItemText do + @moduledoc false + defstruct [:id, :value, :disabled, :invalid] + + @type t :: %__MODULE__{ + id: String.t(), + value: String.t(), + disabled: boolean(), + invalid: boolean() + } + end + + defmodule ItemControl do + @moduledoc false + defstruct [:id, :value, :disabled, :invalid, :checked] + + @type t :: %__MODULE__{ + id: String.t(), + value: String.t(), + disabled: boolean(), + invalid: boolean(), + checked: boolean() + } + end + + defmodule ItemHiddenInput do + @moduledoc false + defstruct [:id, :value, :disabled, :invalid, :name, :form, :checked] + + @type t :: %__MODULE__{ + id: String.t(), + value: String.t(), + disabled: boolean(), + invalid: boolean(), + name: String.t() | nil, + form: String.t() | nil, + checked: boolean() + } + end +end diff --git a/lib/components/radio_group/connect.ex b/lib/components/radio_group/connect.ex new file mode 100644 index 0000000..3d47955 --- /dev/null +++ b/lib/components/radio_group/connect.ex @@ -0,0 +1,138 @@ +defmodule Corex.RadioGroup.Connect do + @moduledoc false + alias Corex.RadioGroup.Anatomy.{ + Props, + Root, + Label, + Indicator, + Item, + ItemText, + ItemControl, + ItemHiddenInput + } + + defp data_attr(true), do: "" + defp data_attr(false), do: nil + defp data_attr(nil), do: nil + + @spec props(Props.t()) :: map() + def props(assigns) do + %{ + "id" => assigns.id, + "data-value" => if(assigns.controlled, do: assigns.value, else: nil), + "data-default-value" => if(assigns.controlled, do: nil, else: assigns.value), + "data-controlled" => data_attr(assigns.controlled), + "data-name" => assigns.name, + "data-form" => assigns.form, + "data-disabled" => data_attr(assigns.disabled), + "data-invalid" => data_attr(assigns.invalid), + "data-required" => data_attr(assigns.required), + "data-read-only" => data_attr(assigns.read_only), + "data-dir" => assigns.dir, + "data-orientation" => assigns.orientation, + "data-on-value-change" => assigns.on_value_change, + "data-on-value-change-client" => assigns.on_value_change_client + } + end + + @spec root(Root.t()) :: map() + def root(assigns) do + base = %{ + "data-scope" => "radio-group", + "data-part" => "root", + "role" => "radiogroup", + "dir" => assigns.dir, + "data-orientation" => assigns.orientation, + "id" => "radio-group:#{assigns.id}", + "style" => "position:relative;" + } + + if assigns.has_label do + Map.put(base, "aria-labelledby", "radio-group:#{assigns.id}:label") + else + base + end + end + + @spec label(Label.t()) :: map() + def label(assigns) do + %{ + "data-scope" => "radio-group", + "data-part" => "label", + "dir" => assigns.dir, + "id" => "radio-group:#{assigns.id}:label" + } + end + + @spec indicator(Indicator.t()) :: map() + def indicator(assigns) do + %{ + "data-scope" => "radio-group", + "data-part" => "indicator", + "dir" => assigns.dir, + "id" => "radio-group:#{assigns.id}:indicator", + "hidden" => "", + "style" => + "position:absolute;width:0;height:0;overflow:hidden;clip:rect(0,0,0,0);margin:-1px;padding:0;border:0;" + } + end + + @spec item(Item.t()) :: map() + def item(assigns) do + %{ + "data-scope" => "radio-group", + "data-part" => "item", + "data-value" => assigns.value, + "data-disabled" => data_attr(assigns.disabled), + "data-invalid" => data_attr(assigns.invalid), + "data-state" => if(assigns.checked, do: "checked", else: "unchecked"), + "id" => "radio-group:#{assigns.id}:item:#{assigns.value}" + } + end + + @spec item_text(ItemText.t()) :: map() + def item_text(assigns) do + %{ + "data-scope" => "radio-group", + "data-part" => "item-text", + "data-value" => assigns.value, + "data-disabled" => data_attr(assigns.disabled), + "data-invalid" => data_attr(assigns.invalid), + "id" => "radio-group:#{assigns.id}:item-text:#{assigns.value}" + } + end + + @spec item_control(ItemControl.t()) :: map() + def item_control(assigns) do + %{ + "data-scope" => "radio-group", + "data-part" => "item-control", + "aria-hidden" => "true", + "data-value" => assigns.value, + "data-state" => if(assigns.checked, do: "checked", else: "unchecked"), + "data-disabled" => data_attr(assigns.disabled), + "data-invalid" => data_attr(assigns.invalid), + "id" => "radio-group:#{assigns.id}:item-control:#{assigns.value}" + } + end + + @spec item_hidden_input(ItemHiddenInput.t()) :: map() + def item_hidden_input(assigns) do + %{ + "data-scope" => "radio-group", + "data-part" => "item-hidden-input", + "type" => "radio", + "name" => assigns.name, + "form" => assigns.form, + "value" => assigns.value, + "checked" => data_attr(assigns.checked), + "disabled" => data_attr(assigns.disabled), + "data-value" => assigns.value, + "data-disabled" => data_attr(assigns.disabled), + "data-invalid" => data_attr(assigns.invalid), + "id" => "radio-group:#{assigns.id}:item-hidden-input:#{assigns.value}", + "style" => + "border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;white-space:nowrap;word-wrap:normal;" + } + end +end diff --git a/lib/components/select/anatomy.ex b/lib/components/select/anatomy.ex index 9725c97..5d1d086 100644 --- a/lib/components/select/anatomy.ex +++ b/lib/components/select/anatomy.ex @@ -1,9 +1,10 @@ defmodule Corex.Select.Anatomy do @moduledoc false - alias Corex.Collection + alias Corex.Tree defmodule Props do @moduledoc false + alias Corex.Tree @enforce_keys [:id] defstruct [ @@ -31,7 +32,7 @@ defmodule Corex.Select.Anatomy do @type t :: %__MODULE__{ id: String.t(), - collection: list(Collection.Item.t() | map()), + collection: list(Tree.Item.t() | map()), controlled: boolean(), placeholder: String.t() | nil, value: list(String.t()), diff --git a/lib/components/timer.ex b/lib/components/timer.ex new file mode 100644 index 0000000..f77938c --- /dev/null +++ b/lib/components/timer.ex @@ -0,0 +1,119 @@ +defmodule Corex.Timer do + @moduledoc ~S''' + Phoenix implementation of [Zag.js Timer](https://zagjs.com/components/react/timer). + + ## Examples + + ### Basic + + ```heex + <.timer id="t" start_ms={60_000} class="timer"> + <:start_trigger><.icon name="hero-play" class="icon" /> + <:pause_trigger><.icon name="hero-pause" class="icon" /> + <:resume_trigger><.icon name="hero-play" class="icon" /> + <:reset_trigger><.icon name="hero-arrow-path" class="icon" /> + + ``` + + ### Countdown + + ```heex + <.timer id="t" countdown start_ms={90_000} target_ms={0} auto_start class="timer"> + <:start_trigger><.icon name="hero-play" class="icon" /> + <:pause_trigger><.icon name="hero-pause" class="icon" /> + <:resume_trigger><.icon name="hero-play" class="icon" /> + <:reset_trigger><.icon name="hero-arrow-path" class="icon" /> + + ``` + + Required slots: `:start_trigger`, `:pause_trigger`, `:resume_trigger`, `:reset_trigger`. + + ## Styling + + Use data attributes: `[data-scope="timer"][data-part="root"]`, `area`, `item`, `separator`, `control`, `action-trigger`. + ''' + + @doc type: :component + use Phoenix.Component + + alias Corex.Timer.Anatomy.{Props, Root, Area, Control, Item, Separator, ActionTrigger} + alias Corex.Timer.Connect + + attr(:id, :string, required: false) + attr(:countdown, :boolean, default: false) + attr(:start_ms, :integer, default: 0) + attr(:target_ms, :integer, default: nil) + attr(:auto_start, :boolean, default: false) + attr(:interval, :integer, default: 1000) + attr(:on_tick, :string, default: nil) + attr(:on_complete, :string, default: nil) + attr(:rest, :global) + + slot(:start_trigger, required: true) + slot(:pause_trigger, required: true) + slot(:resume_trigger, required: true) + slot(:reset_trigger, required: true) + + def timer(assigns) do + assigns = + assigns + |> assign_new(:id, fn -> "timer-#{System.unique_integer([:positive])}" end) + |> assign(:time_values, time_values(assigns.start_ms)) + |> assign(:running, assigns.auto_start) + |> assign(:paused, false) + + ~H""" +
+
+
+
+
:
+
+
:
+
+
:
+
+
+
+ + + + +
+
+
+ """ + end + + defp time_values(ms) do + ms = max(0, ms) + seconds = div(rem(ms, 60_000), 1_000) + minutes = div(rem(ms, 3_600_000), 60_000) + hours = div(rem(ms, 86_400_000), 3_600_000) + days = div(ms, 86_400_000) + + %{days: days, hours: hours, minutes: minutes, seconds: seconds} + end +end diff --git a/lib/components/timer/anatomy.ex b/lib/components/timer/anatomy.ex new file mode 100644 index 0000000..12cf475 --- /dev/null +++ b/lib/components/timer/anatomy.ex @@ -0,0 +1,72 @@ +defmodule Corex.Timer.Anatomy do + @moduledoc false + + defmodule Props do + @moduledoc false + @enforce_keys [:id] + + defstruct [ + :id, + countdown: false, + start_ms: 0, + target_ms: nil, + auto_start: false, + interval: 1000, + on_tick: nil, + on_complete: nil + ] + + @type t :: %__MODULE__{ + id: String.t(), + countdown: boolean(), + start_ms: non_neg_integer(), + target_ms: non_neg_integer() | nil, + auto_start: boolean(), + interval: non_neg_integer(), + on_tick: String.t() | nil, + on_complete: String.t() | nil + } + end + + defmodule Root do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule Area do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule Control do + @moduledoc false + defstruct [:id] + + @type t :: %__MODULE__{id: String.t()} + end + + defmodule Item do + @moduledoc false + defstruct [:id, :type, :value] + + @type t :: %__MODULE__{id: String.t() | nil, type: binary(), value: non_neg_integer()} + end + + defmodule Separator do + @moduledoc false + defstruct [] + + @type t :: %__MODULE__{} + end + + defmodule ActionTrigger do + @moduledoc false + defstruct [:action, hidden: false] + + @type t :: %__MODULE__{action: String.t(), hidden: boolean()} + end +end diff --git a/lib/components/timer/connect.ex b/lib/components/timer/connect.ex new file mode 100644 index 0000000..30d6468 --- /dev/null +++ b/lib/components/timer/connect.ex @@ -0,0 +1,93 @@ +defmodule Corex.Timer.Connect do + @moduledoc false + alias Corex.Timer.Anatomy.{Props, Root, Area, Control, Item, Separator, ActionTrigger} + + defp data_attr(true), do: "" + defp data_attr(false), do: nil + defp data_attr(nil), do: nil + + @spec props(Props.t()) :: map() + def props(assigns) do + %{ + "id" => assigns.id, + "data-countdown" => data_attr(assigns.countdown), + "data-start-ms" => to_string(assigns.start_ms), + "data-target-ms" => if(assigns.target_ms, do: to_string(assigns.target_ms), else: nil), + "data-auto-start" => data_attr(assigns.auto_start), + "data-interval" => to_string(assigns.interval), + "data-on-tick" => assigns.on_tick, + "data-on-complete" => assigns.on_complete + } + end + + @spec root(Root.t()) :: map() + def root(assigns) do + %{ + "data-scope" => "timer", + "data-part" => "root", + "id" => "timer:#{assigns.id}" + } + end + + @spec area(Area.t()) :: map() + def area(assigns) do + %{ + "data-scope" => "timer", + "data-part" => "area", + "id" => "timer:#{assigns.id}:area" + } + end + + @spec control(Control.t()) :: map() + def control(assigns) do + %{ + "data-scope" => "timer", + "data-part" => "control", + "id" => "timer:#{assigns.id}:control" + } + end + + @spec item(Item.t()) :: map() + def item(assigns) do + value = Map.get(assigns, :value, 0) + + %{ + "data-scope" => "timer", + "data-part" => "item", + "data-type" => assigns.type, + "style" => "--value:#{value};" + } + end + + @spec separator(Separator.t()) :: map() + def separator(_assigns) do + %{ + "data-scope" => "timer", + "data-part" => "separator", + "aria-hidden" => "true" + } + end + + defp action_label("start"), do: "Start" + defp action_label("pause"), do: "Pause" + defp action_label("resume"), do: "Resume" + defp action_label("reset"), do: "Reset" + defp action_label(_), do: "Timer action" + + @spec action_trigger(ActionTrigger.t()) :: map() + def action_trigger(assigns) do + base = %{ + "data-scope" => "timer", + "data-part" => "action-trigger", + "data-action" => assigns.action, + "type" => "button", + "aria-label" => action_label(assigns.action) + } + + if assigns.hidden do + Map.put(base, "hidden", "") + else + base + end + end +end diff --git a/lib/corex.ex b/lib/corex.ex index 7ed7812..3e8e323 100644 --- a/lib/corex.ex +++ b/lib/corex.ex @@ -33,7 +33,18 @@ defmodule Corex do date_picker: {Corex.DatePicker, [date_picker: 1]}, signature_pad: {Corex.SignaturePad, [signature_pad: 1]}, menu: {Corex.Menu, [menu: 1]}, - tree_view: {Corex.TreeView, [tree_view: 1, tree_item: 1, tree_branch: 1]} + tree_view: {Corex.TreeView, [tree_view: 1, tree_item: 1, tree_branch: 1]}, + angle_slider: {Corex.AngleSlider, [angle_slider: 1]}, + avatar: {Corex.Avatar, [avatar: 1]}, + carousel: {Corex.Carousel, [carousel: 1]}, + editable: {Corex.Editable, [editable: 1]}, + floating_panel: {Corex.FloatingPanel, [floating_panel: 1]}, + listbox: {Corex.Listbox, [listbox: 1]}, + number_input: {Corex.NumberInput, [number_input: 1]}, + password_input: {Corex.PasswordInput, [password_input: 1]}, + pin_input: {Corex.PinInput, [pin_input: 1]}, + radio_group: {Corex.RadioGroup, [radio_group: 1]}, + timer: {Corex.Timer, [timer: 1]} } defmacro __using__(opts \\ []) do diff --git a/mix.exs b/mix.exs index cd083ff..bef873c 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Corex.MixProject do use Mix.Project - @version "0.1.0-alpha.22" + @version "0.1.0-alpha.23" @elixir_requirement "~> 1.15" def project do @@ -17,7 +17,7 @@ defmodule Corex.MixProject do "Accessible and unstyled UI components library written in Elixir and TypeScript that integrates Zag.js state machines into the Phoenix Framework.", package: package(), source_url: "https://github.com/corex-ui/corex", - homepage_url: "https://corex-ui.com", + homepage_url: "https://corex.gigalixirapp.com/en", docs: &docs/0 ] end @@ -46,7 +46,10 @@ defmodule Corex.MixProject do defp aliases do [ + compile: [©_design/1, "compile"], "assets.build": [ + &clean_static_assets/1, + ©_design/1, "esbuild module", "esbuild cdn", "esbuild cdn_min", @@ -57,6 +60,23 @@ defmodule Corex.MixProject do ] end + defp clean_static_assets(_) do + static = Path.join([__DIR__, "priv", "static"]) + design_dest = Path.join([__DIR__, "priv", "design"]) + File.rm_rf(Path.join(static, "cache_manifest.json")) + File.rm_rf(design_dest) + end + + defp copy_design(_) do + source = Path.join([__DIR__, "design"]) + destination = Path.join([__DIR__, "priv", "design"]) + + if File.exists?(source) and File.dir?(source) do + File.mkdir_p!(Path.dirname(destination)) + File.cp_r!(source, destination, force: true) + end + end + defp package do [ maintainers: ["Karim Semmoud"], @@ -96,17 +116,28 @@ defmodule Corex.MixProject do [ Components: [ Corex.Accordion, + Corex.AngleSlider, + Corex.Avatar, + Corex.Carousel, Corex.Checkbox, - Corex.Combobox, Corex.Clipboard, + Corex.Combobox, Corex.Collapsible, Corex.DatePicker, Corex.Dialog, + Corex.Editable, + Corex.FloatingPanel, + Corex.Listbox, Corex.Menu, + Corex.NumberInput, + Corex.PasswordInput, + Corex.PinInput, + Corex.RadioGroup, Corex.Select, Corex.SignaturePad, Corex.Switch, Corex.Tabs, + Corex.Timer, Corex.Toast, Corex.ToggleGroup, Corex.TreeView diff --git a/package.json b/package.json index 3c1c3dc..3e39a63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "corex", - "version": "0.1.0-alpha.22", + "version": "0.1.0-alpha.23", "description": "The official JavaScript client for the Corex Phoenix Hooks.", "license": "MIT", "module": "./priv/static/corex.mjs", @@ -13,17 +13,28 @@ "require": "./priv/static/corex.cjs.js" }, "./accordion": "./priv/static/accordion.mjs", + "./angle-slider": "./priv/static/angle-slider.mjs", + "./avatar": "./priv/static/avatar.mjs", + "./carousel": "./priv/static/carousel.mjs", "./checkbox": "./priv/static/checkbox.mjs", "./clipboard": "./priv/static/clipboard.mjs", "./collapsible": "./priv/static/collapsible.mjs", "./combobox": "./priv/static/combobox.mjs", "./date-picker": "./priv/static/date-picker.mjs", "./dialog": "./priv/static/dialog.mjs", + "./editable": "./priv/static/editable.mjs", + "./floating-panel": "./priv/static/floating-panel.mjs", + "./listbox": "./priv/static/listbox.mjs", "./menu": "./priv/static/menu.mjs", + "./number-input": "./priv/static/number-input.mjs", + "./password-input": "./priv/static/password-input.mjs", + "./pin-input": "./priv/static/pin-input.mjs", + "./radio-group": "./priv/static/radio-group.mjs", "./select": "./priv/static/select.mjs", "./signature-pad": "./priv/static/signature-pad.mjs", "./switch": "./priv/static/switch.mjs", "./tabs": "./priv/static/tabs.mjs", + "./timer": "./priv/static/timer.mjs", "./toast": "./priv/static/toast.mjs", "./toggle-group": "./priv/static/toggle-group.mjs", "./tree-view": "./priv/static/tree-view.mjs" @@ -39,7 +50,6 @@ "priv/design/**", "assets/**" ], - "sideEffects": false, "scripts": { "lint": "eslint assets", "lint:fix": "eslint assets --fix", @@ -52,8 +62,10 @@ "@eslint/js": "^9.39.2", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", - "@zag-js/accordion": "^1.33.1", +"@zag-js/accordion": "^1.33.1", + "@zag-js/angle-slider": "^1.33.1", "@zag-js/avatar": "^1.33.1", + "@zag-js/carousel": "^1.33.1", "@zag-js/collection": "^1.33.1", "@zag-js/checkbox": "^1.33.1", "@zag-js/clipboard": "^1.33.1", @@ -61,13 +73,20 @@ "@zag-js/combobox": "^1.33.1", "@zag-js/date-picker": "^1.33.1", "@zag-js/dialog": "^1.33.1", + "@zag-js/editable": "^1.33.1", "@zag-js/floating-panel": "^1.33.1", - "@zag-js/menu": "^1.33.1", + "@zag-js/listbox": "^1.33.1", + "@zag-js/menu": "^1.33.1", + "@zag-js/number-input": "^1.33.1", + "@zag-js/password-input": "^1.33.1", + "@zag-js/pin-input": "^1.33.1", "@zag-js/popper": "^1.33.1", + "@zag-js/radio-group": "^1.33.1", "@zag-js/select": "^1.33.1", "@zag-js/signature-pad": "^1.33.1", "@zag-js/switch": "^1.33.1", "@zag-js/tabs": "^1.33.1", + "@zag-js/timer": "^1.33.1", "@zag-js/toast": "^1.33.1", "@zag-js/toggle-group": "^1.33.1", "@zag-js/tree-view": "^1.33.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 341012f..2d7728f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,9 +23,15 @@ importers: '@zag-js/accordion': specifier: ^1.33.1 version: 1.33.1 + '@zag-js/angle-slider': + specifier: ^1.33.1 + version: 1.33.1 '@zag-js/avatar': specifier: ^1.33.1 version: 1.33.1 + '@zag-js/carousel': + specifier: ^1.33.1 + version: 1.33.1 '@zag-js/checkbox': specifier: ^1.33.1 version: 1.33.1 @@ -47,15 +53,33 @@ importers: '@zag-js/dialog': specifier: ^1.33.1 version: 1.33.1 + '@zag-js/editable': + specifier: ^1.33.1 + version: 1.33.1 '@zag-js/floating-panel': specifier: ^1.33.1 version: 1.33.1 + '@zag-js/listbox': + specifier: ^1.33.1 + version: 1.33.1 '@zag-js/menu': specifier: ^1.33.1 version: 1.33.1 + '@zag-js/number-input': + specifier: ^1.33.1 + version: 1.33.1 + '@zag-js/password-input': + specifier: ^1.33.1 + version: 1.33.1 + '@zag-js/pin-input': + specifier: ^1.33.1 + version: 1.33.1 '@zag-js/popper': specifier: ^1.33.1 version: 1.33.1 + '@zag-js/radio-group': + specifier: ^1.33.1 + version: 1.33.1 '@zag-js/select': specifier: ^1.33.1 version: 1.33.1 @@ -68,6 +92,9 @@ importers: '@zag-js/tabs': specifier: ^1.33.1 version: 1.33.1 + '@zag-js/timer': + specifier: ^1.33.1 + version: 1.33.1 '@zag-js/toast': specifier: ^1.33.1 version: 1.33.1 @@ -177,6 +204,9 @@ packages: '@internationalized/date@3.11.0': resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==} + '@internationalized/number@3.6.5': + resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==} + '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -255,12 +285,18 @@ packages: '@zag-js/anatomy@1.33.1': resolution: {integrity: sha512-iME14VHGGEPNMakilI6qvEkv9sll4AFZHpeoMLpczesw5hmqQjjNRifDTPR+idqCb8O8PdkAPE9hyMeP+4JjtA==} + '@zag-js/angle-slider@1.33.1': + resolution: {integrity: sha512-Y44IND5koNWD/EMKEWJbuEnzNW9y1WsrQFFvKRsMp/m3n60hiLa8qtZHoZWm8eOZCKFlsjVJ0gueEuZp43nobA==} + '@zag-js/aria-hidden@1.33.1': resolution: {integrity: sha512-TpRAtssDHVfra5qxigk7w1NMf/crKu615INu6GAbNNMUBWD1rPZAfxdg/xe/BAcxLy+XM5/q62dVSNvpjXzN3g==} '@zag-js/avatar@1.33.1': resolution: {integrity: sha512-D8HBPvIVLoty14CDx6wWfdfcalr/pf2FgJ0N7VTgExvZt8t64JWJarL75ZkIB3ROaNe4RMFdzabz1uc7BlcDyg==} + '@zag-js/carousel@1.33.1': + resolution: {integrity: sha512-FB72jCHhTTn0gXsWwDT/DrGMpBHQTxlKvwjEiBGkcprWVpptN0WGJR+EtX2Si/668sdH/471rew2DKA+h5k6Tw==} + '@zag-js/checkbox@1.33.1': resolution: {integrity: sha512-3rIPXB3O7hZukyjKpRAOn+Ob7jByBmDNU7wdpS2HRv7Urv9i5jUExlwayevw/a6JHQaT7mR1dL4culTyX+fJVA==} @@ -298,6 +334,9 @@ packages: '@zag-js/dom-query@1.33.1': resolution: {integrity: sha512-Iyl0D3nLvJuMkkuRy22xhj4pkzexUCDlRpCzqIrOMDKsmFka/WV9PIclZKVpMECTi9dEQmJuGTjBVaCOReLu+Q==} + '@zag-js/editable@1.33.1': + resolution: {integrity: sha512-uLLwopl5naET76ND+/GZDVMlXaAIwepAhmfNA+Esj4Upgtd3lpD5SNzJiVuyzZ0ewVyp2cuXHHAfNiibhkoFlA==} + '@zag-js/floating-panel@1.33.1': resolution: {integrity: sha512-MKtFyC3xxCUmHEnugR+KMcVIX7FdHsoZfDxcKc74h+2M6FAmk6YB8lByoY9pkCR9ems/5DkHcMU9cVVJ9kiFqA==} @@ -310,21 +349,39 @@ packages: '@zag-js/interact-outside@1.33.1': resolution: {integrity: sha512-XnqwYsGw0GVmjBpDziwWXKE/+KeZLgRnjEpyVr6HMATMGD+c4j6TmIbI9OGEaWliLuwvHdTclkmK4WYTaAGmiw==} + '@zag-js/listbox@1.33.1': + resolution: {integrity: sha512-8XT+6T82xG3BJwC7VYu/I1W8Hxyjgpke8tB1odQSWOV23pVXXPbol7wQbtoieSVeNDsZD8K12CpB40oRVrcSHA==} + '@zag-js/live-region@1.33.1': resolution: {integrity: sha512-KbU2wUSMd01fY7dgc9WhvU2x07FxNHKSCrn+fFUnB+Qoy6iiVv0A729JDbzPUUcpBV0BFoQ3qNdBDVyBalbpaQ==} '@zag-js/menu@1.33.1': resolution: {integrity: sha512-QihwaFCgGcrPbJSoP73nt749/rlUANiIrCU//8WWfQTgv0NBJprBD7d3banDNlK9ZSGmvELcpyQ/fKU4cfn0GQ==} + '@zag-js/number-input@1.33.1': + resolution: {integrity: sha512-5YKr8uagIDGXp3hIqo4IUBGxS5WhH0xM1CQf2zimfDWvBOng+Y+MH/4Lwu9wKuyIq/J3SJqsjO+2OOF7u6ju/g==} + + '@zag-js/password-input@1.33.1': + resolution: {integrity: sha512-pJrz50JhQLTfiatehATr40udJYggYmJ7V/7/dBKqthGpMwoaVV3bmtKFSenFGc2mMb5Rlf9KKqHO/dYB7jpNiA==} + + '@zag-js/pin-input@1.33.1': + resolution: {integrity: sha512-q6/DRsIV6ZDKzkFmdzbcsVBm7+I7hMlrsLr/P/jH0/fYE5T9t+1m9ll5j7/5RHFJHQ1WajHpdt5ad5mfXMuxKA==} + '@zag-js/popper@1.33.1': resolution: {integrity: sha512-DNKRh/SRXB2wcvVYK1wvcEufS4vfVXJOv23QUee761bTv4nrPNll5pZFsYEHatiCNkAmO0MRRYA2Sc6jk9nxNA==} + '@zag-js/radio-group@1.33.1': + resolution: {integrity: sha512-W/T8Hea3Z4mWCErm2fJc/EYabxRkKHFJStSClyllqknF3Y+b42MaKGuub1IcACO3pe6csLTkomdxy1qDLWl/dg==} + '@zag-js/rect-utils@1.33.1': resolution: {integrity: sha512-vCIgZF/z8oeYfUhGUgRiNEfOS8on4rUXi4vtL4IvHSdAv5VxZw4ODoLhIzRGT3BwsiMfr8qJ8fmrcR2oFRFQgA==} '@zag-js/remove-scroll@1.33.1': resolution: {integrity: sha512-5+Mvboqlmv8EdJoixAbGrftFVWZTznsVJn40BuB/6fYQeqdsZ2vFmSmSIr7btFOPcj3BcTMo0SbWNNta3fAOrg==} + '@zag-js/scroll-snap@1.33.1': + resolution: {integrity: sha512-GLEb+YJj800ia2zyTFxVZomQ1cFSShazUQ/1uAxX0Lj7+aZK88cZhIn7AI0+yBXTPBS0zrZDhBPsGEDQX+Q9Fw==} + '@zag-js/select@1.33.1': resolution: {integrity: sha512-eG+Ftdse0zvCAkXBMNZVBlM+KNvFRKHToxlxgid6wOd5QgRGwr4HaJuWaz908nBIZRYMFVvC+lLaygUVORHmGg==} @@ -340,6 +397,9 @@ packages: '@zag-js/tabs@1.33.1': resolution: {integrity: sha512-Xquhso7jUch9UrG5N+5vNfR8S2bWUk6EDpBBArY0X5oPSnlzgwJcjWh98hH1QyHX3JmWZN4kAfVKUxNdQxRnVw==} + '@zag-js/timer@1.33.1': + resolution: {integrity: sha512-GgqntefAEQbf66aNgA6NL9Rtrrxcd0/IJVddTj1/xihCnJ8u6AOU4syG5tie0Tpc2caDAntOwlYjpEy3n2AGcA==} + '@zag-js/toast@1.33.1': resolution: {integrity: sha512-kI2/VJcBQGgHpmuWiIDqPn8ejFEODh5YhjWbnvjGRG+x3XoPuMq6hhxXV6VWJslbZJtTmzxDcP+Xamdrf1hbZA==} @@ -818,6 +878,10 @@ snapshots: dependencies: '@swc/helpers': 0.5.18 + '@internationalized/number@3.6.5': + dependencies: + '@swc/helpers': 0.5.18 + '@pkgr/core@0.2.9': {} '@swc/helpers@0.5.18': @@ -929,6 +993,15 @@ snapshots: '@zag-js/anatomy@1.33.1': {} + '@zag-js/angle-slider@1.33.1': + dependencies: + '@zag-js/anatomy': 1.33.1 + '@zag-js/core': 1.33.1 + '@zag-js/dom-query': 1.33.1 + '@zag-js/rect-utils': 1.33.1 + '@zag-js/types': 1.33.1 + '@zag-js/utils': 1.33.1 + '@zag-js/aria-hidden@1.33.1': dependencies: '@zag-js/dom-query': 1.33.1 @@ -941,6 +1014,15 @@ snapshots: '@zag-js/types': 1.33.1 '@zag-js/utils': 1.33.1 + '@zag-js/carousel@1.33.1': + dependencies: + '@zag-js/anatomy': 1.33.1 + '@zag-js/core': 1.33.1 + '@zag-js/dom-query': 1.33.1 + '@zag-js/scroll-snap': 1.33.1 + '@zag-js/types': 1.33.1 + '@zag-js/utils': 1.33.1 + '@zag-js/checkbox@1.33.1': dependencies: '@zag-js/anatomy': 1.33.1 @@ -1026,6 +1108,15 @@ snapshots: dependencies: '@zag-js/types': 1.33.1 + '@zag-js/editable@1.33.1': + dependencies: + '@zag-js/anatomy': 1.33.1 + '@zag-js/core': 1.33.1 + '@zag-js/dom-query': 1.33.1 + '@zag-js/interact-outside': 1.33.1 + '@zag-js/types': 1.33.1 + '@zag-js/utils': 1.33.1 + '@zag-js/floating-panel@1.33.1': dependencies: '@zag-js/anatomy': 1.33.1 @@ -1050,6 +1141,16 @@ snapshots: '@zag-js/dom-query': 1.33.1 '@zag-js/utils': 1.33.1 + '@zag-js/listbox@1.33.1': + dependencies: + '@zag-js/anatomy': 1.33.1 + '@zag-js/collection': 1.33.1 + '@zag-js/core': 1.33.1 + '@zag-js/dom-query': 1.33.1 + '@zag-js/focus-visible': 1.33.1 + '@zag-js/types': 1.33.1 + '@zag-js/utils': 1.33.1 + '@zag-js/live-region@1.33.1': {} '@zag-js/menu@1.33.1': @@ -1063,18 +1164,56 @@ snapshots: '@zag-js/types': 1.33.1 '@zag-js/utils': 1.33.1 + '@zag-js/number-input@1.33.1': + dependencies: + '@internationalized/number': 3.6.5 + '@zag-js/anatomy': 1.33.1 + '@zag-js/core': 1.33.1 + '@zag-js/dom-query': 1.33.1 + '@zag-js/types': 1.33.1 + '@zag-js/utils': 1.33.1 + + '@zag-js/password-input@1.33.1': + dependencies: + '@zag-js/anatomy': 1.33.1 + '@zag-js/core': 1.33.1 + '@zag-js/dom-query': 1.33.1 + '@zag-js/types': 1.33.1 + '@zag-js/utils': 1.33.1 + + '@zag-js/pin-input@1.33.1': + dependencies: + '@zag-js/anatomy': 1.33.1 + '@zag-js/core': 1.33.1 + '@zag-js/dom-query': 1.33.1 + '@zag-js/types': 1.33.1 + '@zag-js/utils': 1.33.1 + '@zag-js/popper@1.33.1': dependencies: '@floating-ui/dom': 1.7.5 '@zag-js/dom-query': 1.33.1 '@zag-js/utils': 1.33.1 + '@zag-js/radio-group@1.33.1': + dependencies: + '@zag-js/anatomy': 1.33.1 + '@zag-js/core': 1.33.1 + '@zag-js/dom-query': 1.33.1 + '@zag-js/focus-visible': 1.33.1 + '@zag-js/types': 1.33.1 + '@zag-js/utils': 1.33.1 + '@zag-js/rect-utils@1.33.1': {} '@zag-js/remove-scroll@1.33.1': dependencies: '@zag-js/dom-query': 1.33.1 + '@zag-js/scroll-snap@1.33.1': + dependencies: + '@zag-js/dom-query': 1.33.1 + '@zag-js/select@1.33.1': dependencies: '@zag-js/anatomy': 1.33.1 @@ -1116,6 +1255,14 @@ snapshots: '@zag-js/types': 1.33.1 '@zag-js/utils': 1.33.1 + '@zag-js/timer@1.33.1': + dependencies: + '@zag-js/anatomy': 1.33.1 + '@zag-js/core': 1.33.1 + '@zag-js/dom-query': 1.33.1 + '@zag-js/types': 1.33.1 + '@zag-js/utils': 1.33.1 + '@zag-js/toast@1.33.1': dependencies: '@zag-js/anatomy': 1.33.1 diff --git a/priv/static/accordion.mjs b/priv/static/accordion.mjs index c16d972..e6bd251 100644 --- a/priv/static/accordion.mjs +++ b/priv/static/accordion.mjs @@ -22,7 +22,7 @@ import { queryAll, remove, warn -} from "./chunk-GFGFZBBD.mjs"; +} from "./chunk-IXOYOLUJ.mjs"; // ../node_modules/.pnpm/@zag-js+accordion@1.33.1/node_modules/@zag-js/accordion/dist/index.mjs var anatomy = createAnatomy("accordion").parts("root", "item", "itemTrigger", "itemContent", "itemIndicator"); diff --git a/priv/static/angle-slider.mjs b/priv/static/angle-slider.mjs new file mode 100644 index 0000000..dde35ee --- /dev/null +++ b/priv/static/angle-slider.mjs @@ -0,0 +1,601 @@ +import { + createRect, + getPointAngle +} from "./chunk-BMVNROAE.mjs"; +import { + Component, + VanillaMachine, + createAnatomy, + createMachine, + createProps, + createSplitProps, + dataAttr, + getBoolean, + getEventPoint, + getEventStep, + getNativeEvent, + getNumber, + getString, + isLeftClick, + normalizeProps, + raf, + setElementValue, + snapValueToStep, + trackPointerMove +} from "./chunk-IXOYOLUJ.mjs"; + +// ../node_modules/.pnpm/@zag-js+angle-slider@1.33.1/node_modules/@zag-js/angle-slider/dist/index.mjs +var anatomy = createAnatomy("angle-slider").parts( + "root", + "label", + "thumb", + "valueText", + "control", + "track", + "markerGroup", + "marker" +); +var parts = anatomy.build(); +var getRootId = (ctx) => ctx.ids?.root ?? `angle-slider:${ctx.id}`; +var getThumbId = (ctx) => ctx.ids?.thumb ?? `angle-slider:${ctx.id}:thumb`; +var getHiddenInputId = (ctx) => ctx.ids?.hiddenInput ?? `angle-slider:${ctx.id}:input`; +var getControlId = (ctx) => ctx.ids?.control ?? `angle-slider:${ctx.id}:control`; +var getValueTextId = (ctx) => ctx.ids?.valueText ?? `angle-slider:${ctx.id}:value-text`; +var getLabelId = (ctx) => ctx.ids?.label ?? `angle-slider:${ctx.id}:label`; +var getHiddenInputEl = (ctx) => ctx.getById(getHiddenInputId(ctx)); +var getControlEl = (ctx) => ctx.getById(getControlId(ctx)); +var getThumbEl = (ctx) => ctx.getById(getThumbId(ctx)); +var MIN_VALUE = 0; +var MAX_VALUE = 359; +function getAngle(controlEl, point, angularOffset) { + const rect = createRect(controlEl.getBoundingClientRect()); + const angle = getPointAngle(rect, point); + if (angularOffset != null) { + return angle - angularOffset; + } + return angle; +} +function clampAngle(degree) { + return Math.min(Math.max(degree, MIN_VALUE), MAX_VALUE); +} +function constrainAngle(degree, step) { + const clampedDegree = clampAngle(degree); + const upperStep = Math.ceil(clampedDegree / step); + const nearestStep = Math.round(clampedDegree / step); + return upperStep >= clampedDegree / step ? upperStep * step === MAX_VALUE ? MIN_VALUE : upperStep * step : nearestStep * step; +} +function snapAngleToStep(value, step) { + return snapValueToStep(value, MIN_VALUE, MAX_VALUE, step); +} +function connect(service, normalize) { + const { state, send, context, prop, computed, scope } = service; + const dragging = state.matches("dragging"); + const value = context.get("value"); + const valueAsDegree = computed("valueAsDegree"); + const disabled = prop("disabled"); + const invalid = prop("invalid"); + const readOnly = prop("readOnly"); + const interactive = computed("interactive"); + const ariaLabel = prop("aria-label"); + const ariaLabelledBy = prop("aria-labelledby"); + return { + value, + valueAsDegree, + dragging, + setValue(value2) { + send({ type: "VALUE.SET", value: value2 }); + }, + getRootProps() { + return normalize.element({ + ...parts.root.attrs, + id: getRootId(scope), + "data-disabled": dataAttr(disabled), + "data-invalid": dataAttr(invalid), + "data-readonly": dataAttr(readOnly), + style: { + "--value": value, + "--angle": valueAsDegree + } + }); + }, + getLabelProps() { + return normalize.label({ + ...parts.label.attrs, + id: getLabelId(scope), + htmlFor: getHiddenInputId(scope), + "data-disabled": dataAttr(disabled), + "data-invalid": dataAttr(invalid), + "data-readonly": dataAttr(readOnly), + onClick(event) { + if (!interactive) return; + event.preventDefault(); + getThumbEl(scope)?.focus(); + } + }); + }, + getHiddenInputProps() { + return normalize.element({ + type: "hidden", + value, + name: prop("name"), + id: getHiddenInputId(scope) + }); + }, + getControlProps() { + return normalize.element({ + ...parts.control.attrs, + role: "presentation", + id: getControlId(scope), + "data-disabled": dataAttr(disabled), + "data-invalid": dataAttr(invalid), + "data-readonly": dataAttr(readOnly), + onPointerDown(event) { + if (!interactive) return; + if (!isLeftClick(event)) return; + const point = getEventPoint(event); + const controlEl = event.currentTarget; + const thumbEl = getThumbEl(scope); + const composedPath = getNativeEvent(event).composedPath(); + const isOverThumb = thumbEl && composedPath.includes(thumbEl); + let angularOffset = null; + if (isOverThumb) { + const clickAngle = getAngle(controlEl, point); + angularOffset = clickAngle - value; + } + send({ type: "CONTROL.POINTER_DOWN", point, angularOffset }); + event.stopPropagation(); + }, + style: { + touchAction: "none", + userSelect: "none", + WebkitUserSelect: "none" + } + }); + }, + getThumbProps() { + return normalize.element({ + ...parts.thumb.attrs, + id: getThumbId(scope), + role: "slider", + "aria-label": ariaLabel, + "aria-labelledby": ariaLabelledBy ?? getLabelId(scope), + "aria-valuemax": 360, + "aria-valuemin": 0, + "aria-valuenow": value, + tabIndex: readOnly || interactive ? 0 : void 0, + "data-disabled": dataAttr(disabled), + "data-invalid": dataAttr(invalid), + "data-readonly": dataAttr(readOnly), + onFocus() { + send({ type: "THUMB.FOCUS" }); + }, + onBlur() { + send({ type: "THUMB.BLUR" }); + }, + onKeyDown(event) { + if (!interactive) return; + const step = getEventStep(event) * prop("step"); + switch (event.key) { + case "ArrowLeft": + case "ArrowUp": + event.preventDefault(); + send({ type: "THUMB.ARROW_DEC", step }); + break; + case "ArrowRight": + case "ArrowDown": + event.preventDefault(); + send({ type: "THUMB.ARROW_INC", step }); + break; + case "Home": + event.preventDefault(); + send({ type: "THUMB.HOME" }); + break; + case "End": + event.preventDefault(); + send({ type: "THUMB.END" }); + break; + } + }, + style: { + rotate: `var(--angle)` + } + }); + }, + getValueTextProps() { + return normalize.element({ + ...parts.valueText.attrs, + id: getValueTextId(scope) + }); + }, + getMarkerGroupProps() { + return normalize.element({ + ...parts.markerGroup.attrs + }); + }, + getMarkerProps(props2) { + let markerState; + if (props2.value < value) { + markerState = "under-value"; + } else if (props2.value > value) { + markerState = "over-value"; + } else { + markerState = "at-value"; + } + return normalize.element({ + ...parts.marker.attrs, + "data-value": props2.value, + "data-state": markerState, + "data-disabled": dataAttr(disabled), + style: { + "--marker-value": props2.value, + rotate: `calc(var(--marker-value) * 1deg)` + } + }); + } + }; +} +var machine = createMachine({ + props({ props: props2 }) { + return { + step: 1, + defaultValue: 0, + ...props2 + }; + }, + context({ prop, bindable }) { + return { + value: bindable(() => ({ + defaultValue: prop("defaultValue"), + value: prop("value"), + onChange(value) { + prop("onValueChange")?.({ value, valueAsDegree: `${value}deg` }); + } + })) + }; + }, + refs() { + return { + thumbDragOffset: null + }; + }, + computed: { + interactive: ({ prop }) => !(prop("disabled") || prop("readOnly")), + valueAsDegree: ({ context }) => `${context.get("value")}deg` + }, + watch({ track, context, action }) { + track([() => context.get("value")], () => { + action(["syncInputElement"]); + }); + }, + initialState() { + return "idle"; + }, + on: { + "VALUE.SET": { + actions: ["setValue"] + } + }, + states: { + idle: { + on: { + "CONTROL.POINTER_DOWN": { + target: "dragging", + actions: ["setThumbDragOffset", "setPointerValue", "focusThumb"] + }, + "THUMB.FOCUS": { + target: "focused" + } + } + }, + focused: { + on: { + "CONTROL.POINTER_DOWN": { + target: "dragging", + actions: ["setThumbDragOffset", "setPointerValue", "focusThumb"] + }, + "THUMB.ARROW_DEC": { + actions: ["decrementValue", "invokeOnChangeEnd"] + }, + "THUMB.ARROW_INC": { + actions: ["incrementValue", "invokeOnChangeEnd"] + }, + "THUMB.HOME": { + actions: ["setValueToMin", "invokeOnChangeEnd"] + }, + "THUMB.END": { + actions: ["setValueToMax", "invokeOnChangeEnd"] + }, + "THUMB.BLUR": { + target: "idle" + } + } + }, + dragging: { + entry: ["focusThumb"], + effects: ["trackPointerMove"], + on: { + "DOC.POINTER_UP": { + target: "focused", + actions: ["invokeOnChangeEnd", "clearThumbDragOffset"] + }, + "DOC.POINTER_MOVE": { + actions: ["setPointerValue"] + } + } + } + }, + implementations: { + effects: { + trackPointerMove({ scope, send }) { + return trackPointerMove(scope.getDoc(), { + onPointerMove(info) { + send({ type: "DOC.POINTER_MOVE", point: info.point }); + }, + onPointerUp() { + send({ type: "DOC.POINTER_UP" }); + } + }); + } + }, + actions: { + syncInputElement({ scope, context }) { + const inputEl = getHiddenInputEl(scope); + setElementValue(inputEl, context.get("value").toString()); + }, + invokeOnChangeEnd({ context, prop, computed }) { + prop("onValueChangeEnd")?.({ + value: context.get("value"), + valueAsDegree: computed("valueAsDegree") + }); + }, + setPointerValue({ scope, event, context, prop, refs }) { + const controlEl = getControlEl(scope); + if (!controlEl) return; + const angularOffset = refs.get("thumbDragOffset"); + const deg = getAngle(controlEl, event.point, angularOffset); + context.set("value", constrainAngle(deg, prop("step"))); + }, + setValueToMin({ context }) { + context.set("value", MIN_VALUE); + }, + setValueToMax({ context }) { + context.set("value", MAX_VALUE); + }, + setValue({ context, event }) { + context.set("value", clampAngle(event.value)); + }, + decrementValue({ context, event, prop }) { + const value = snapAngleToStep(context.get("value") - event.step, event.step ?? prop("step")); + context.set("value", value); + }, + incrementValue({ context, event, prop }) { + const value = snapAngleToStep(context.get("value") + event.step, event.step ?? prop("step")); + context.set("value", value); + }, + focusThumb({ scope }) { + raf(() => { + getThumbEl(scope)?.focus({ preventScroll: true }); + }); + }, + setThumbDragOffset({ refs, event }) { + refs.set("thumbDragOffset", event.angularOffset ?? null); + }, + clearThumbDragOffset({ refs }) { + refs.set("thumbDragOffset", null); + } + } + } +}); +var props = createProps()([ + "aria-label", + "aria-labelledby", + "dir", + "disabled", + "getRootNode", + "id", + "ids", + "invalid", + "name", + "onValueChange", + "onValueChangeEnd", + "readOnly", + "step", + "value", + "defaultValue" +]); +var splitProps = createSplitProps(props); + +// components/angle-slider.ts +var AngleSlider = class extends Component { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initMachine(props2) { + return new VanillaMachine(machine, props2); + } + initApi() { + return connect(this.machine.service, normalizeProps); + } + render() { + const rootEl = this.el.querySelector('[data-scope="angle-slider"][data-part="root"]') ?? this.el; + this.spreadProps(rootEl, this.api.getRootProps()); + const labelEl = this.el.querySelector( + '[data-scope="angle-slider"][data-part="label"]' + ); + if (labelEl) this.spreadProps(labelEl, this.api.getLabelProps()); + const hiddenInputEl = this.el.querySelector( + '[data-scope="angle-slider"][data-part="hidden-input"]' + ); + if (hiddenInputEl) this.spreadProps(hiddenInputEl, this.api.getHiddenInputProps()); + const controlEl = this.el.querySelector( + '[data-scope="angle-slider"][data-part="control"]' + ); + if (controlEl) this.spreadProps(controlEl, this.api.getControlProps()); + const thumbEl = this.el.querySelector( + '[data-scope="angle-slider"][data-part="thumb"]' + ); + if (thumbEl) this.spreadProps(thumbEl, this.api.getThumbProps()); + const valueTextEl = this.el.querySelector( + '[data-scope="angle-slider"][data-part="value-text"]' + ); + if (valueTextEl) { + this.spreadProps(valueTextEl, this.api.getValueTextProps()); + const valueSpan = valueTextEl.querySelector( + '[data-scope="angle-slider"][data-part="value"]' + ); + if (valueSpan && valueSpan.textContent !== String(this.api.value)) { + valueSpan.textContent = String(this.api.value); + } + } + const markerGroupEl = this.el.querySelector( + '[data-scope="angle-slider"][data-part="marker-group"]' + ); + if (markerGroupEl) this.spreadProps(markerGroupEl, this.api.getMarkerGroupProps()); + this.el.querySelectorAll('[data-scope="angle-slider"][data-part="marker"]').forEach((markerEl) => { + const valueStr = markerEl.dataset.value; + if (valueStr == null) return; + const value = Number(valueStr); + if (Number.isNaN(value)) return; + this.spreadProps(markerEl, this.api.getMarkerProps({ value })); + }); + } +}; + +// hooks/angle-slider.ts +var AngleSliderHook = { + mounted() { + const el = this.el; + const value = getNumber(el, "value"); + const defaultValue = getNumber(el, "defaultValue"); + const controlled = getBoolean(el, "controlled"); + let skipNextOnValueChange = false; + const zag = new AngleSlider(el, { + id: el.id, + ...controlled && value !== void 0 ? { value } : { defaultValue: defaultValue ?? 0 }, + step: getNumber(el, "step") ?? 1, + disabled: getBoolean(el, "disabled"), + readOnly: getBoolean(el, "readOnly"), + invalid: getBoolean(el, "invalid"), + name: getString(el, "name"), + dir: getString(el, "dir", ["ltr", "rtl"]), + onValueChange: (details) => { + if (skipNextOnValueChange) { + skipNextOnValueChange = false; + return; + } + if (controlled) { + skipNextOnValueChange = true; + zag.api.setValue(details.value); + } else { + const hiddenInput = el.querySelector( + '[data-scope="angle-slider"][data-part="hidden-input"]' + ); + if (hiddenInput) { + hiddenInput.value = String(details.value); + hiddenInput.dispatchEvent(new Event("input", { bubbles: true })); + hiddenInput.dispatchEvent(new Event("change", { bubbles: true })); + } + } + const eventName = getString(el, "onValueChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { + value: details.value, + valueAsDegree: details.valueAsDegree, + id: el.id + }); + } + const eventNameClient = getString(el, "onValueChangeClient"); + if (eventNameClient) { + el.dispatchEvent( + new CustomEvent(eventNameClient, { + bubbles: true, + detail: { value: details, id: el.id } + }) + ); + } + }, + onValueChangeEnd: (details) => { + if (controlled) { + const hiddenInput = el.querySelector( + '[data-scope="angle-slider"][data-part="hidden-input"]' + ); + if (hiddenInput) { + hiddenInput.value = String(details.value); + hiddenInput.dispatchEvent(new Event("input", { bubbles: true })); + hiddenInput.dispatchEvent(new Event("change", { bubbles: true })); + } + } + const eventName = getString(el, "onValueChangeEnd"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { + value: details.value, + valueAsDegree: details.valueAsDegree, + id: el.id + }); + } + const eventNameClient = getString(el, "onValueChangeEndClient"); + if (eventNameClient) { + el.dispatchEvent( + new CustomEvent(eventNameClient, { + bubbles: true, + detail: { value: details, id: el.id } + }) + ); + } + } + }); + zag.init(); + this.angleSlider = zag; + this.handlers = []; + this.onSetValue = (event) => { + const { value: value2 } = event.detail; + zag.api.setValue(value2); + }; + el.addEventListener("phx:angle-slider:set-value", this.onSetValue); + this.handlers.push( + this.handleEvent( + "angle_slider_set_value", + (payload) => { + const targetId = payload.angle_slider_id; + if (targetId) { + const matches = el.id === targetId || el.id === `angle-slider:${targetId}`; + if (!matches) return; + } + zag.api.setValue(payload.value); + } + ) + ); + this.handlers.push( + this.handleEvent("angle_slider_value", () => { + this.pushEvent("angle_slider_value_response", { + value: zag.api.value, + valueAsDegree: zag.api.valueAsDegree, + dragging: zag.api.dragging + }); + }) + ); + }, + updated() { + const value = getNumber(this.el, "value"); + const controlled = getBoolean(this.el, "controlled"); + this.angleSlider?.updateProps({ + id: this.el.id, + ...controlled && value !== void 0 ? { value } : {}, + step: getNumber(this.el, "step") ?? 1, + disabled: getBoolean(this.el, "disabled"), + readOnly: getBoolean(this.el, "readOnly"), + invalid: getBoolean(this.el, "invalid"), + name: getString(this.el, "name") + }); + }, + destroyed() { + if (this.onSetValue) { + this.el.removeEventListener("phx:angle-slider:set-value", this.onSetValue); + } + if (this.handlers) { + for (const h of this.handlers) this.removeHandleEvent(h); + } + this.angleSlider?.destroy(); + } +}; +export { + AngleSliderHook as AngleSlider +}; diff --git a/priv/static/avatar.mjs b/priv/static/avatar.mjs new file mode 100644 index 0000000..16aaf2b --- /dev/null +++ b/priv/static/avatar.mjs @@ -0,0 +1,239 @@ +import { + Component, + VanillaMachine, + createAnatomy, + createMachine, + createProps, + createSplitProps, + getString, + normalizeProps, + observeAttributes, + observeChildren +} from "./chunk-IXOYOLUJ.mjs"; + +// ../node_modules/.pnpm/@zag-js+avatar@1.33.1/node_modules/@zag-js/avatar/dist/index.mjs +var anatomy = createAnatomy("avatar").parts("root", "image", "fallback"); +var parts = anatomy.build(); +var getRootId = (ctx) => ctx.ids?.root ?? `avatar:${ctx.id}`; +var getImageId = (ctx) => ctx.ids?.image ?? `avatar:${ctx.id}:image`; +var getFallbackId = (ctx) => ctx.ids?.fallback ?? `avatar:${ctx.id}:fallback`; +var getRootEl = (ctx) => ctx.getById(getRootId(ctx)); +var getImageEl = (ctx) => ctx.getById(getImageId(ctx)); +function connect(service, normalize) { + const { state, send, prop, scope } = service; + const loaded = state.matches("loaded"); + return { + loaded, + setSrc(src) { + const img = getImageEl(scope); + img?.setAttribute("src", src); + }, + setLoaded() { + send({ type: "img.loaded", src: "api" }); + }, + setError() { + send({ type: "img.error", src: "api" }); + }, + getRootProps() { + return normalize.element({ + ...parts.root.attrs, + dir: prop("dir"), + id: getRootId(scope) + }); + }, + getImageProps() { + return normalize.img({ + ...parts.image.attrs, + hidden: !loaded, + dir: prop("dir"), + id: getImageId(scope), + "data-state": loaded ? "visible" : "hidden", + onLoad() { + send({ type: "img.loaded", src: "element" }); + }, + onError() { + send({ type: "img.error", src: "element" }); + } + }); + }, + getFallbackProps() { + return normalize.element({ + ...parts.fallback.attrs, + dir: prop("dir"), + id: getFallbackId(scope), + hidden: loaded, + "data-state": loaded ? "hidden" : "visible" + }); + } + }; +} +var machine = createMachine({ + initialState() { + return "loading"; + }, + effects: ["trackImageRemoval", "trackSrcChange"], + on: { + "src.change": { + target: "loading" + }, + "img.unmount": { + target: "error" + } + }, + states: { + loading: { + entry: ["checkImageStatus"], + on: { + "img.loaded": { + target: "loaded", + actions: ["invokeOnLoad"] + }, + "img.error": { + target: "error", + actions: ["invokeOnError"] + } + } + }, + error: { + on: { + "img.loaded": { + target: "loaded", + actions: ["invokeOnLoad"] + } + } + }, + loaded: { + on: { + "img.error": { + target: "error", + actions: ["invokeOnError"] + } + } + } + }, + implementations: { + actions: { + invokeOnLoad({ prop }) { + prop("onStatusChange")?.({ status: "loaded" }); + }, + invokeOnError({ prop }) { + prop("onStatusChange")?.({ status: "error" }); + }, + checkImageStatus({ send, scope }) { + const imageEl = getImageEl(scope); + if (!imageEl?.complete) return; + const type = hasLoaded(imageEl) ? "img.loaded" : "img.error"; + send({ type, src: "ssr" }); + } + }, + effects: { + trackImageRemoval({ send, scope }) { + const rootEl = getRootEl(scope); + return observeChildren(rootEl, { + callback(records) { + const removedNodes = Array.from(records[0].removedNodes); + const removed = removedNodes.find( + (node) => node.nodeType === Node.ELEMENT_NODE && node.matches("[data-scope=avatar][data-part=image]") + ); + if (removed) { + send({ type: "img.unmount" }); + } + } + }); + }, + trackSrcChange({ send, scope }) { + const imageEl = getImageEl(scope); + return observeAttributes(imageEl, { + attributes: ["src", "srcset"], + callback() { + send({ type: "src.change" }); + } + }); + } + } + } +}); +function hasLoaded(image) { + return image.complete && image.naturalWidth !== 0 && image.naturalHeight !== 0; +} +var props = createProps()(["dir", "id", "ids", "onStatusChange", "getRootNode"]); +var splitProps = createSplitProps(props); + +// components/avatar.ts +var Avatar = class extends Component { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initMachine(props2) { + return new VanillaMachine(machine, props2); + } + initApi() { + return connect(this.machine.service, normalizeProps); + } + render() { + const rootEl = this.el.querySelector('[data-scope="avatar"][data-part="root"]') ?? this.el; + this.spreadProps(rootEl, this.api.getRootProps()); + const imageEl = this.el.querySelector('[data-scope="avatar"][data-part="image"]'); + if (imageEl) this.spreadProps(imageEl, this.api.getImageProps()); + const fallbackEl = this.el.querySelector( + '[data-scope="avatar"][data-part="fallback"]' + ); + if (fallbackEl) this.spreadProps(fallbackEl, this.api.getFallbackProps()); + const skeletonEl = this.el.querySelector( + '[data-scope="avatar"][data-part="skeleton"]' + ); + if (skeletonEl) { + const state = this.machine.service.state; + const loaded = state.matches("loaded"); + const error = state.matches("error"); + skeletonEl.hidden = loaded || error; + skeletonEl.setAttribute("data-state", loaded || error ? "hidden" : "visible"); + } + } +}; + +// hooks/avatar.ts +var AvatarHook = { + mounted() { + const el = this.el; + const src = getString(el, "src"); + const zag = new Avatar(el, { + id: el.id, + onStatusChange: (details) => { + const eventName = getString(el, "onStatusChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { status: details.status, id: el.id }); + } + const clientName = getString(el, "onStatusChangeClient"); + if (clientName) { + el.dispatchEvent( + new CustomEvent(clientName, { + bubbles: true, + detail: { value: details, id: el.id } + }) + ); + } + }, + ...src !== void 0 ? {} : {} + }); + zag.init(); + this.avatar = zag; + if (src !== void 0) { + zag.api.setSrc(src); + } + this.handlers = []; + }, + updated() { + const src = getString(this.el, "src"); + if (src !== void 0 && this.avatar) { + this.avatar.api.setSrc(src); + } + }, + destroyed() { + if (this.handlers) { + for (const h of this.handlers) this.removeHandleEvent(h); + } + this.avatar?.destroy(); + } +}; +export { + AvatarHook as Avatar +}; diff --git a/priv/static/carousel.mjs b/priv/static/carousel.mjs new file mode 100644 index 0000000..90616c9 --- /dev/null +++ b/priv/static/carousel.mjs @@ -0,0 +1,1129 @@ +import { + Component, + VanillaMachine, + add, + addDomEvent, + ariaAttr, + callAll, + clampValue, + contains, + createAnatomy, + createMachine, + createProps, + createSplitProps, + dataAttr, + ensureProps, + getBoolean, + getComputedStyle as getComputedStyle2, + getDir, + getEventKey, + getEventTarget, + getNumber, + getString, + getTabbables, + isFocusable, + isLeftClick, + isObject, + nextIndex, + normalizeProps, + prevIndex, + queryAll, + raf, + remove, + resizeObserverBorderBox, + throttle, + trackPointerMove, + uniq +} from "./chunk-IXOYOLUJ.mjs"; + +// ../node_modules/.pnpm/@zag-js+scroll-snap@1.33.1/node_modules/@zag-js/scroll-snap/dist/index.mjs +var getDirection = (element) => getComputedStyle2(element).direction; +function getScrollPadding(element) { + const style = getComputedStyle2(element); + const rect = element.getBoundingClientRect(); + let xBeforeRaw = style.getPropertyValue("scroll-padding-left").replace("auto", "0px"); + let yBeforeRaw = style.getPropertyValue("scroll-padding-top").replace("auto", "0px"); + let xAfterRaw = style.getPropertyValue("scroll-padding-right").replace("auto", "0px"); + let yAfterRaw = style.getPropertyValue("scroll-padding-bottom").replace("auto", "0px"); + function convert(raw, size) { + let n = parseFloat(raw); + if (/%/.test(raw)) { + n /= 100; + n *= size; + } + return Number.isNaN(n) ? 0 : n; + } + let xBefore = convert(xBeforeRaw, rect.width); + let yBefore = convert(yBeforeRaw, rect.height); + let xAfter = convert(xAfterRaw, rect.width); + let yAfter = convert(yAfterRaw, rect.height); + return { + x: { before: xBefore, after: xAfter }, + y: { before: yBefore, after: yAfter } + }; +} +function isRectIntersecting(a, b, axis = "both") { + return axis === "x" && a.right >= b.left && a.left <= b.right || axis === "y" && a.bottom >= b.top && a.top <= b.bottom || axis === "both" && a.right >= b.left && a.left <= b.right && a.bottom >= b.top && a.top <= b.bottom; +} +function getDescendants(parent) { + let children = []; + for (const child of parent.children) { + children = children.concat(child, getDescendants(child)); + } + return children; +} +function getSnapPositions(parent, subtree = false) { + const parentRect = parent.getBoundingClientRect(); + const dir = getDirection(parent); + const isRtl = dir === "rtl"; + const positions = { + x: { start: [], center: [], end: [] }, + y: { start: [], center: [], end: [] } + }; + const children = subtree ? getDescendants(parent) : parent.children; + for (const axis of ["x", "y"]) { + const orthogonalAxis = axis === "x" ? "y" : "x"; + const axisStart = axis === "x" ? "left" : "top"; + const axisEnd = axis === "x" ? "right" : "bottom"; + const axisSize = axis === "x" ? "width" : "height"; + const axisScroll = axis === "x" ? "scrollLeft" : "scrollTop"; + const useRtlCalc = isRtl && axis === "x"; + for (const child of children) { + const childRect = child.getBoundingClientRect(); + if (!isRectIntersecting(parentRect, childRect, orthogonalAxis)) { + continue; + } + const childStyle = getComputedStyle2(child); + let [childAlignY, childAlignX] = childStyle.getPropertyValue("scroll-snap-align").split(" "); + if (typeof childAlignX === "undefined") { + childAlignX = childAlignY; + } + const childAlign = axis === "x" ? childAlignX : childAlignY; + let childOffsetStart; + let childOffsetEnd; + let childOffsetCenter; + if (useRtlCalc) { + const scrollOffset = Math.abs(parent[axisScroll]); + const rightOffset = parentRect[axisEnd] - childRect[axisEnd] + scrollOffset; + childOffsetStart = rightOffset; + childOffsetEnd = rightOffset + childRect[axisSize]; + childOffsetCenter = rightOffset + childRect[axisSize] / 2; + } else { + childOffsetStart = childRect[axisStart] - parentRect[axisStart] + parent[axisScroll]; + childOffsetEnd = childOffsetStart + childRect[axisSize]; + childOffsetCenter = childOffsetStart + childRect[axisSize] / 2; + } + switch (childAlign) { + case "none": + break; + case "start": + positions[axis].start.push({ node: child, position: childOffsetStart }); + break; + case "center": + positions[axis].center.push({ node: child, position: childOffsetCenter }); + break; + case "end": + positions[axis].end.push({ node: child, position: childOffsetEnd }); + break; + } + } + } + return positions; +} +function getScrollSnapPositions(element) { + const dir = getDirection(element); + const rect = element.getBoundingClientRect(); + const scrollPadding = getScrollPadding(element); + const snapPositions = getSnapPositions(element); + const maxScroll = { + x: element.scrollWidth - element.offsetWidth, + y: element.scrollHeight - element.offsetHeight + }; + const isRtl = dir === "rtl"; + const usesNegativeScrollLeft = isRtl && element.scrollLeft <= 0; + let xPositions; + if (isRtl) { + xPositions = uniq2( + [ + ...snapPositions.x.start.map((v) => v.position - scrollPadding.x.after), + ...snapPositions.x.center.map((v) => v.position - rect.width / 2), + ...snapPositions.x.end.map((v) => v.position - rect.width + scrollPadding.x.before) + ].map(clamp(0, maxScroll.x)) + ); + if (usesNegativeScrollLeft) { + xPositions = xPositions.map((pos) => -pos); + } + } else { + xPositions = uniq2( + [ + ...snapPositions.x.start.map((v) => v.position - scrollPadding.x.before), + ...snapPositions.x.center.map((v) => v.position - rect.width / 2), + ...snapPositions.x.end.map((v) => v.position - rect.width + scrollPadding.x.after) + ].map(clamp(0, maxScroll.x)) + ); + } + return { + x: xPositions, + y: uniq2( + [ + ...snapPositions.y.start.map((v) => v.position - scrollPadding.y.before), + ...snapPositions.y.center.map((v) => v.position - rect.height / 2), + ...snapPositions.y.end.map((v) => v.position - rect.height + scrollPadding.y.after) + ].map(clamp(0, maxScroll.y)) + ) + }; +} +function findSnapPoint(parent, axis, predicate) { + const dir = getDirection(parent); + const scrollPadding = getScrollPadding(parent); + const snapPositions = getSnapPositions(parent); + const items = [...snapPositions[axis].start, ...snapPositions[axis].center, ...snapPositions[axis].end]; + const isRtl = dir === "rtl"; + const usesNegativeScrollLeft = isRtl && axis === "x" && parent.scrollLeft <= 0; + for (const item of items) { + if (predicate(item.node)) { + let position; + if (axis === "x" && isRtl) { + position = item.position - scrollPadding.x.after; + if (usesNegativeScrollLeft) { + position = -position; + } + } else { + position = item.position - (axis === "x" ? scrollPadding.x.before : scrollPadding.y.before); + } + return position; + } + } +} +var uniq2 = (arr) => [...new Set(arr)]; +var clamp = (min, max) => (value) => Math.max(min, Math.min(max, value)); + +// ../node_modules/.pnpm/@zag-js+carousel@1.33.1/node_modules/@zag-js/carousel/dist/index.mjs +var anatomy = createAnatomy("carousel").parts( + "root", + "itemGroup", + "item", + "control", + "nextTrigger", + "prevTrigger", + "indicatorGroup", + "indicator", + "autoplayTrigger", + "progressText" +); +var parts = anatomy.build(); +var getRootId = (ctx) => ctx.ids?.root ?? `carousel:${ctx.id}`; +var getItemId = (ctx, index) => ctx.ids?.item?.(index) ?? `carousel:${ctx.id}:item:${index}`; +var getItemGroupId = (ctx) => ctx.ids?.itemGroup ?? `carousel:${ctx.id}:item-group`; +var getNextTriggerId = (ctx) => ctx.ids?.nextTrigger ?? `carousel:${ctx.id}:next-trigger`; +var getPrevTriggerId = (ctx) => ctx.ids?.prevTrigger ?? `carousel:${ctx.id}:prev-trigger`; +var getIndicatorGroupId = (ctx) => ctx.ids?.indicatorGroup ?? `carousel:${ctx.id}:indicator-group`; +var getIndicatorId = (ctx, index) => ctx.ids?.indicator?.(index) ?? `carousel:${ctx.id}:indicator:${index}`; +var getItemGroupEl = (ctx) => ctx.getById(getItemGroupId(ctx)); +var getItemEls = (ctx) => queryAll(getItemGroupEl(ctx), `[data-part=item]`); +var getIndicatorEl = (ctx, page) => ctx.getById(getIndicatorId(ctx, page)); +var syncTabIndex = (ctx) => { + const el = getItemGroupEl(ctx); + if (!el) return; + const tabbables = getTabbables(el); + el.setAttribute("tabindex", tabbables.length > 0 ? "-1" : "0"); +}; +function connect(service, normalize) { + const { state, context, computed, send, scope, prop } = service; + const isPlaying = state.matches("autoplay"); + const isDragging = state.matches("dragging"); + const canScrollNext = computed("canScrollNext"); + const canScrollPrev = computed("canScrollPrev"); + const horizontal = computed("isHorizontal"); + const autoSize = prop("autoSize"); + const pageSnapPoints = Array.from(context.get("pageSnapPoints")); + const page = context.get("page"); + const slidesPerPage = prop("slidesPerPage"); + const padding = prop("padding"); + const translations = prop("translations"); + return { + isPlaying, + isDragging, + page, + pageSnapPoints, + canScrollNext, + canScrollPrev, + getProgress() { + return page / pageSnapPoints.length; + }, + getProgressText() { + const details = { page: page + 1, totalPages: pageSnapPoints.length }; + return translations.progressText?.(details) ?? ""; + }, + scrollToIndex(index, instant) { + send({ type: "INDEX.SET", index, instant }); + }, + scrollTo(index, instant) { + send({ type: "PAGE.SET", index, instant }); + }, + scrollNext(instant) { + send({ type: "PAGE.NEXT", instant }); + }, + scrollPrev(instant) { + send({ type: "PAGE.PREV", instant }); + }, + play() { + send({ type: "AUTOPLAY.START" }); + }, + pause() { + send({ type: "AUTOPLAY.PAUSE" }); + }, + isInView(index) { + return Array.from(context.get("slidesInView")).includes(index); + }, + refresh() { + send({ type: "SNAP.REFRESH" }); + }, + getRootProps() { + return normalize.element({ + ...parts.root.attrs, + id: getRootId(scope), + role: "region", + "aria-roledescription": "carousel", + "data-orientation": prop("orientation"), + dir: prop("dir"), + style: { + "--slides-per-page": slidesPerPage, + "--slide-spacing": prop("spacing"), + "--slide-item-size": autoSize ? "auto" : "calc(100% / var(--slides-per-page) - var(--slide-spacing) * (var(--slides-per-page) - 1) / var(--slides-per-page))" + } + }); + }, + getItemGroupProps() { + return normalize.element({ + ...parts.itemGroup.attrs, + id: getItemGroupId(scope), + "data-orientation": prop("orientation"), + "data-dragging": dataAttr(isDragging), + dir: prop("dir"), + "aria-live": isPlaying ? "off" : "polite", + onFocus(event) { + if (!contains(event.currentTarget, getEventTarget(event))) return; + send({ type: "VIEWPORT.FOCUS" }); + }, + onBlur(event) { + if (contains(event.currentTarget, event.relatedTarget)) return; + send({ type: "VIEWPORT.BLUR" }); + }, + onMouseDown(event) { + if (event.defaultPrevented) return; + if (!prop("allowMouseDrag")) return; + if (!isLeftClick(event)) return; + const target = getEventTarget(event); + if (isFocusable(target) && target !== event.currentTarget) return; + event.preventDefault(); + send({ type: "DRAGGING.START" }); + }, + onWheel: throttle((event) => { + const axis = prop("orientation") === "horizontal" ? "deltaX" : "deltaY"; + const isScrollingLeft = event[axis] < 0; + if (isScrollingLeft && !computed("canScrollPrev")) return; + const isScrollingRight = event[axis] > 0; + if (isScrollingRight && !computed("canScrollNext")) return; + send({ type: "USER.SCROLL" }); + }, 150), + onTouchStart() { + send({ type: "USER.SCROLL" }); + }, + style: { + display: autoSize ? "flex" : "grid", + gap: "var(--slide-spacing)", + scrollSnapType: [horizontal ? "x" : "y", prop("snapType")].join(" "), + gridAutoFlow: horizontal ? "column" : "row", + scrollbarWidth: "none", + overscrollBehaviorX: "contain", + [horizontal ? "gridAutoColumns" : "gridAutoRows"]: autoSize ? void 0 : "var(--slide-item-size)", + [horizontal ? "scrollPaddingInline" : "scrollPaddingBlock"]: padding, + [horizontal ? "paddingInline" : "paddingBlock"]: padding, + [horizontal ? "overflowX" : "overflowY"]: "auto" + } + }); + }, + getItemProps(props2) { + const isInView = context.get("slidesInView").includes(props2.index); + return normalize.element({ + ...parts.item.attrs, + id: getItemId(scope, props2.index), + dir: prop("dir"), + role: "group", + "data-index": props2.index, + "data-inview": dataAttr(isInView), + "aria-roledescription": "slide", + "data-orientation": prop("orientation"), + "aria-label": translations.item(props2.index, prop("slideCount")), + "aria-hidden": ariaAttr(!isInView), + style: { + flex: "0 0 auto", + [horizontal ? "maxWidth" : "maxHeight"]: "100%", + scrollSnapAlign: (() => { + const snapAlign = props2.snapAlign ?? "start"; + const slidesPerMove = prop("slidesPerMove"); + const perMove = slidesPerMove === "auto" ? Math.floor(prop("slidesPerPage")) : slidesPerMove; + const shouldSnap = (props2.index + perMove) % perMove === 0; + return shouldSnap ? snapAlign : void 0; + })() + } + }); + }, + getControlProps() { + return normalize.element({ + ...parts.control.attrs, + "data-orientation": prop("orientation") + }); + }, + getPrevTriggerProps() { + return normalize.button({ + ...parts.prevTrigger.attrs, + id: getPrevTriggerId(scope), + type: "button", + disabled: !canScrollPrev, + dir: prop("dir"), + "aria-label": translations.prevTrigger, + "data-orientation": prop("orientation"), + "aria-controls": getItemGroupId(scope), + onClick(event) { + if (event.defaultPrevented) return; + send({ type: "PAGE.PREV", src: "trigger" }); + } + }); + }, + getNextTriggerProps() { + return normalize.button({ + ...parts.nextTrigger.attrs, + dir: prop("dir"), + id: getNextTriggerId(scope), + type: "button", + "aria-label": translations.nextTrigger, + "data-orientation": prop("orientation"), + "aria-controls": getItemGroupId(scope), + disabled: !canScrollNext, + onClick(event) { + if (event.defaultPrevented) return; + send({ type: "PAGE.NEXT", src: "trigger" }); + } + }); + }, + getIndicatorGroupProps() { + return normalize.element({ + ...parts.indicatorGroup.attrs, + dir: prop("dir"), + id: getIndicatorGroupId(scope), + "data-orientation": prop("orientation"), + onKeyDown(event) { + if (event.defaultPrevented) return; + const src = "indicator"; + const keyMap = { + ArrowDown(event2) { + if (horizontal) return; + send({ type: "PAGE.NEXT", src }); + event2.preventDefault(); + }, + ArrowUp(event2) { + if (horizontal) return; + send({ type: "PAGE.PREV", src }); + event2.preventDefault(); + }, + ArrowRight(event2) { + if (!horizontal) return; + send({ type: "PAGE.NEXT", src }); + event2.preventDefault(); + }, + ArrowLeft(event2) { + if (!horizontal) return; + send({ type: "PAGE.PREV", src }); + event2.preventDefault(); + }, + Home(event2) { + send({ type: "PAGE.SET", index: 0, src }); + event2.preventDefault(); + }, + End(event2) { + send({ type: "PAGE.SET", index: pageSnapPoints.length - 1, src }); + event2.preventDefault(); + } + }; + const key = getEventKey(event, { + dir: prop("dir"), + orientation: prop("orientation") + }); + const exec = keyMap[key]; + exec?.(event); + } + }); + }, + getIndicatorProps(props2) { + return normalize.button({ + ...parts.indicator.attrs, + dir: prop("dir"), + id: getIndicatorId(scope, props2.index), + type: "button", + "data-orientation": prop("orientation"), + "data-index": props2.index, + "data-readonly": dataAttr(props2.readOnly), + "data-current": dataAttr(props2.index === page), + "aria-label": translations.indicator(props2.index), + onClick(event) { + if (event.defaultPrevented) return; + if (props2.readOnly) return; + send({ type: "PAGE.SET", index: props2.index, src: "indicator" }); + } + }); + }, + getAutoplayTriggerProps() { + return normalize.button({ + ...parts.autoplayTrigger.attrs, + type: "button", + "data-orientation": prop("orientation"), + "data-pressed": dataAttr(isPlaying), + "aria-label": isPlaying ? translations.autoplayStop : translations.autoplayStart, + onClick(event) { + if (event.defaultPrevented) return; + send({ type: isPlaying ? "AUTOPLAY.PAUSE" : "AUTOPLAY.START" }); + } + }); + }, + getProgressTextProps() { + return normalize.element({ + ...parts.progressText.attrs + }); + } + }; +} +var machine = createMachine({ + props({ props: props2 }) { + ensureProps(props2, ["slideCount"], "carousel"); + return { + dir: "ltr", + defaultPage: 0, + orientation: "horizontal", + snapType: "mandatory", + loop: !!props2.autoplay, + slidesPerPage: 1, + slidesPerMove: "auto", + spacing: "0px", + autoplay: false, + allowMouseDrag: false, + inViewThreshold: 0.6, + autoSize: false, + ...props2, + translations: { + nextTrigger: "Next slide", + prevTrigger: "Previous slide", + indicator: (index) => `Go to slide ${index + 1}`, + item: (index, count) => `${index + 1} of ${count}`, + autoplayStart: "Start slide rotation", + autoplayStop: "Stop slide rotation", + progressText: ({ page, totalPages }) => `${page} / ${totalPages}`, + ...props2.translations + } + }; + }, + refs() { + return { + timeoutRef: void 0 + }; + }, + initialState({ prop }) { + return prop("autoplay") ? "autoplay" : "idle"; + }, + context({ prop, bindable, getContext }) { + return { + page: bindable(() => ({ + defaultValue: prop("defaultPage"), + value: prop("page"), + onChange(page) { + const ctx = getContext(); + const pageSnapPoints = ctx.get("pageSnapPoints"); + prop("onPageChange")?.({ page, pageSnapPoint: pageSnapPoints[page] }); + } + })), + pageSnapPoints: bindable(() => { + return { + defaultValue: prop("autoSize") ? Array.from({ length: prop("slideCount") }, (_, i) => i) : getPageSnapPoints(prop("slideCount"), prop("slidesPerMove"), prop("slidesPerPage")) + }; + }), + slidesInView: bindable(() => ({ + defaultValue: [] + })) + }; + }, + computed: { + isRtl: ({ prop }) => prop("dir") === "rtl", + isHorizontal: ({ prop }) => prop("orientation") === "horizontal", + canScrollNext: ({ prop, context }) => prop("loop") || context.get("page") < context.get("pageSnapPoints").length - 1, + canScrollPrev: ({ prop, context }) => prop("loop") || context.get("page") > 0, + autoplayInterval: ({ prop }) => { + const autoplay = prop("autoplay"); + return isObject(autoplay) ? autoplay.delay : 4e3; + } + }, + watch({ track, action, context, prop, send }) { + track([() => prop("slidesPerPage"), () => prop("slidesPerMove")], () => { + action(["setSnapPoints"]); + }); + track([() => context.get("page")], () => { + action(["scrollToPage", "focusIndicatorEl"]); + }); + track([() => prop("orientation"), () => prop("autoSize"), () => prop("dir")], () => { + action(["setSnapPoints", "scrollToPage"]); + }); + track([() => prop("slideCount")], () => { + send({ type: "SNAP.REFRESH", src: "slide.count" }); + }); + track([() => !!prop("autoplay")], () => { + send({ type: prop("autoplay") ? "AUTOPLAY.START" : "AUTOPLAY.PAUSE", src: "autoplay.prop.change" }); + }); + }, + on: { + "PAGE.NEXT": { + target: "idle", + actions: ["clearScrollEndTimer", "setNextPage"] + }, + "PAGE.PREV": { + target: "idle", + actions: ["clearScrollEndTimer", "setPrevPage"] + }, + "PAGE.SET": { + target: "idle", + actions: ["clearScrollEndTimer", "setPage"] + }, + "INDEX.SET": { + target: "idle", + actions: ["clearScrollEndTimer", "setMatchingPage"] + }, + "SNAP.REFRESH": { + actions: ["setSnapPoints", "clampPage"] + }, + "PAGE.SCROLL": { + actions: ["scrollToPage"] + } + }, + effects: ["trackSlideMutation", "trackSlideIntersections", "trackSlideResize"], + entry: ["setSnapPoints", "setPage"], + exit: ["clearScrollEndTimer"], + states: { + idle: { + on: { + "DRAGGING.START": { + target: "dragging", + actions: ["invokeDragStart"] + }, + "AUTOPLAY.START": { + target: "autoplay", + actions: ["invokeAutoplayStart"] + }, + "USER.SCROLL": { + target: "userScroll" + }, + "VIEWPORT.FOCUS": { + target: "focus" + } + } + }, + focus: { + effects: ["trackKeyboardScroll"], + on: { + "VIEWPORT.BLUR": { + target: "idle" + }, + "PAGE.NEXT": { + actions: ["clearScrollEndTimer", "setNextPage"] + }, + "PAGE.PREV": { + actions: ["clearScrollEndTimer", "setPrevPage"] + }, + "PAGE.SET": { + actions: ["clearScrollEndTimer", "setPage"] + }, + "INDEX.SET": { + actions: ["clearScrollEndTimer", "setMatchingPage"] + }, + "USER.SCROLL": { + target: "userScroll" + } + } + }, + dragging: { + effects: ["trackPointerMove"], + entry: ["disableScrollSnap"], + on: { + DRAGGING: { + actions: ["scrollSlides", "invokeDragging"] + }, + "DRAGGING.END": { + target: "idle", + actions: ["endDragging", "invokeDraggingEnd"] + } + } + }, + userScroll: { + effects: ["trackScroll"], + on: { + "DRAGGING.START": { + target: "dragging", + actions: ["invokeDragStart"] + }, + "SCROLL.END": [ + { + guard: "isFocused", + target: "focus", + actions: ["setClosestPage"] + }, + { + target: "idle", + actions: ["setClosestPage"] + } + ] + } + }, + autoplay: { + effects: ["trackDocumentVisibility", "trackScroll", "autoUpdateSlide"], + exit: ["invokeAutoplayEnd"], + on: { + "AUTOPLAY.TICK": { + actions: ["setNextPage", "invokeAutoplay"] + }, + "DRAGGING.START": { + target: "dragging", + actions: ["invokeDragStart"] + }, + "AUTOPLAY.PAUSE": { + target: "idle" + } + } + } + }, + implementations: { + guards: { + isFocused: ({ scope }) => scope.isActiveElement(getItemGroupEl(scope)) + }, + effects: { + autoUpdateSlide({ computed, send }) { + const id = setInterval(() => { + send({ + type: computed("canScrollNext") ? "AUTOPLAY.TICK" : "AUTOPLAY.PAUSE", + src: "autoplay.interval" + }); + }, computed("autoplayInterval")); + return () => clearInterval(id); + }, + trackSlideMutation({ scope, send }) { + const el = getItemGroupEl(scope); + if (!el) return; + const win = scope.getWin(); + const observer = new win.MutationObserver(() => { + send({ type: "SNAP.REFRESH", src: "slide.mutation" }); + syncTabIndex(scope); + }); + syncTabIndex(scope); + observer.observe(el, { childList: true, subtree: true }); + return () => observer.disconnect(); + }, + trackSlideResize({ scope, send }) { + const el = getItemGroupEl(scope); + if (!el) return; + const exec = () => { + send({ type: "SNAP.REFRESH", src: "slide.resize" }); + }; + raf(() => { + exec(); + raf(() => { + send({ type: "PAGE.SCROLL", instant: true }); + }); + }); + const itemEls = getItemEls(scope); + itemEls.forEach(exec); + const cleanups = itemEls.map((el2) => resizeObserverBorderBox.observe(el2, exec)); + return callAll(...cleanups); + }, + trackSlideIntersections({ scope, prop, context }) { + const el = getItemGroupEl(scope); + const win = scope.getWin(); + const observer = new win.IntersectionObserver( + (entries) => { + const slidesInView = entries.reduce((acc, entry) => { + const target = entry.target; + const index = Number(target.dataset.index ?? "-1"); + if (index == null || Number.isNaN(index) || index === -1) return acc; + return entry.isIntersecting ? add(acc, index) : remove(acc, index); + }, context.get("slidesInView")); + context.set("slidesInView", uniq(slidesInView)); + }, + { + root: el, + threshold: prop("inViewThreshold") + } + ); + getItemEls(scope).forEach((slide) => observer.observe(slide)); + return () => observer.disconnect(); + }, + trackScroll({ send, refs, scope }) { + const el = getItemGroupEl(scope); + if (!el) return; + const onScroll = () => { + clearTimeout(refs.get("timeoutRef")); + refs.set("timeoutRef", void 0); + refs.set( + "timeoutRef", + setTimeout(() => { + send({ type: "SCROLL.END" }); + }, 150) + ); + }; + return addDomEvent(el, "scroll", onScroll, { passive: true }); + }, + trackDocumentVisibility({ scope, send }) { + const doc = scope.getDoc(); + const onVisibilityChange = () => { + if (doc.visibilityState === "visible") return; + send({ type: "AUTOPLAY.PAUSE", src: "doc.hidden" }); + }; + return addDomEvent(doc, "visibilitychange", onVisibilityChange); + }, + trackPointerMove({ scope, send }) { + const doc = scope.getDoc(); + return trackPointerMove(doc, { + onPointerMove({ event }) { + send({ type: "DRAGGING", left: -event.movementX, top: -event.movementY }); + }, + onPointerUp() { + send({ type: "DRAGGING.END" }); + } + }); + }, + trackKeyboardScroll({ scope, send, context }) { + const win = scope.getWin(); + const onKeyDown = (event) => { + switch (event.key) { + case "ArrowRight": + event.preventDefault(); + send({ type: "PAGE.NEXT" }); + break; + case "ArrowLeft": + event.preventDefault(); + send({ type: "PAGE.PREV" }); + break; + case "Home": + event.preventDefault(); + send({ type: "PAGE.SET", index: 0 }); + break; + case "End": + event.preventDefault(); + send({ type: "PAGE.SET", index: context.get("pageSnapPoints").length - 1 }); + } + }; + return addDomEvent(win, "keydown", onKeyDown, { capture: true }); + } + }, + actions: { + clearScrollEndTimer({ refs }) { + if (refs.get("timeoutRef") == null) return; + clearTimeout(refs.get("timeoutRef")); + refs.set("timeoutRef", void 0); + }, + scrollToPage({ context, event, scope, computed, flush }) { + const behavior = event.instant ? "instant" : "smooth"; + const index = clampValue(event.index ?? context.get("page"), 0, context.get("pageSnapPoints").length - 1); + const el = getItemGroupEl(scope); + if (!el) return; + const axis = computed("isHorizontal") ? "left" : "top"; + flush(() => { + el.scrollTo({ [axis]: context.get("pageSnapPoints")[index], behavior }); + }); + }, + setClosestPage({ context, scope, computed }) { + const el = getItemGroupEl(scope); + if (!el) return; + const scrollPosition = computed("isHorizontal") ? el.scrollLeft : el.scrollTop; + const page = context.get("pageSnapPoints").findIndex((point) => Math.abs(point - scrollPosition) < 1); + if (page === -1) return; + context.set("page", page); + }, + setNextPage({ context, prop, state }) { + const loop = state.matches("autoplay") || prop("loop"); + const page = nextIndex(context.get("pageSnapPoints"), context.get("page"), { loop }); + context.set("page", page); + }, + setPrevPage({ context, prop, state }) { + const loop = state.matches("autoplay") || prop("loop"); + const page = prevIndex(context.get("pageSnapPoints"), context.get("page"), { loop }); + context.set("page", page); + }, + setMatchingPage({ context, event, computed, scope }) { + const el = getItemGroupEl(scope); + if (!el) return; + const snapPoint = findSnapPoint( + el, + computed("isHorizontal") ? "x" : "y", + (node) => node.dataset.index === event.index.toString() + ); + if (snapPoint == null) return; + const page = context.get("pageSnapPoints").findIndex((point) => Math.abs(point - snapPoint) < 1); + context.set("page", page); + }, + setPage({ context, event }) { + const page = event.index ?? context.get("page"); + context.set("page", page); + }, + clampPage({ context }) { + const index = clampValue(context.get("page"), 0, context.get("pageSnapPoints").length - 1); + context.set("page", index); + }, + setSnapPoints({ context, computed, scope }) { + const el = getItemGroupEl(scope); + if (!el) return; + const scrollSnapPoints = getScrollSnapPositions(el); + context.set("pageSnapPoints", computed("isHorizontal") ? scrollSnapPoints.x : scrollSnapPoints.y); + }, + disableScrollSnap({ scope }) { + const el = getItemGroupEl(scope); + if (!el) return; + const styles = getComputedStyle(el); + el.dataset.scrollSnapType = styles.getPropertyValue("scroll-snap-type"); + el.style.setProperty("scroll-snap-type", "none"); + }, + scrollSlides({ scope, event }) { + const el = getItemGroupEl(scope); + el?.scrollBy({ left: event.left, top: event.top, behavior: "instant" }); + }, + endDragging({ scope, context, computed }) { + const el = getItemGroupEl(scope); + if (!el) return; + const isHorizontal = computed("isHorizontal"); + const scrollPos = isHorizontal ? el.scrollLeft : el.scrollTop; + const snapPoints = context.get("pageSnapPoints"); + const closest = snapPoints.reduce((closest2, curr) => { + return Math.abs(curr - scrollPos) < Math.abs(closest2 - scrollPos) ? curr : closest2; + }, snapPoints[0]); + raf(() => { + el.scrollTo({ + left: isHorizontal ? closest : el.scrollLeft, + top: isHorizontal ? el.scrollTop : closest, + behavior: "smooth" + }); + context.set("page", snapPoints.indexOf(closest)); + const scrollSnapType = el.dataset.scrollSnapType; + if (scrollSnapType) { + el.style.setProperty("scroll-snap-type", scrollSnapType); + delete el.dataset.scrollSnapType; + } + }); + }, + focusIndicatorEl({ context, event, scope }) { + if (event.src !== "indicator") return; + const el = getIndicatorEl(scope, context.get("page")); + if (!el) return; + raf(() => el.focus({ preventScroll: true })); + }, + invokeDragStart({ context, prop }) { + prop("onDragStatusChange")?.({ type: "dragging.start", isDragging: true, page: context.get("page") }); + }, + invokeDragging({ context, prop }) { + prop("onDragStatusChange")?.({ type: "dragging", isDragging: true, page: context.get("page") }); + }, + invokeDraggingEnd({ context, prop }) { + prop("onDragStatusChange")?.({ type: "dragging.end", isDragging: false, page: context.get("page") }); + }, + invokeAutoplay({ context, prop }) { + prop("onAutoplayStatusChange")?.({ type: "autoplay", isPlaying: true, page: context.get("page") }); + }, + invokeAutoplayStart({ context, prop }) { + prop("onAutoplayStatusChange")?.({ type: "autoplay.start", isPlaying: true, page: context.get("page") }); + }, + invokeAutoplayEnd({ context, prop }) { + prop("onAutoplayStatusChange")?.({ type: "autoplay.stop", isPlaying: false, page: context.get("page") }); + } + } + } +}); +function getPageSnapPoints(totalSlides, slidesPerMove, slidesPerPage) { + if (totalSlides == null || slidesPerPage <= 0) { + return []; + } + const snapPoints = []; + const perMove = slidesPerMove === "auto" ? Math.floor(slidesPerPage) : slidesPerMove; + if (perMove <= 0) { + return []; + } + for (let i = 0; i < totalSlides; i += perMove) { + if (i + slidesPerPage > totalSlides) break; + snapPoints.push(i); + } + return snapPoints; +} +var props = createProps()([ + "dir", + "getRootNode", + "id", + "ids", + "loop", + "page", + "defaultPage", + "onPageChange", + "orientation", + "slideCount", + "slidesPerPage", + "slidesPerMove", + "spacing", + "padding", + "autoplay", + "allowMouseDrag", + "inViewThreshold", + "translations", + "snapType", + "autoSize", + "onDragStatusChange", + "onAutoplayStatusChange" +]); +var splitProps = createSplitProps(props); +var indicatorProps = createProps()(["index", "readOnly"]); +var splitIndicatorProps = createSplitProps(indicatorProps); +var itemProps = createProps()(["index", "snapAlign"]); +var splitItemProps = createSplitProps(itemProps); + +// components/carousel.ts +var Carousel = class extends Component { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initMachine(props2) { + return new VanillaMachine(machine, props2); + } + initApi() { + return connect(this.machine.service, normalizeProps); + } + render() { + const rootEl = this.el.querySelector('[data-scope="carousel"][data-part="root"]') ?? this.el; + this.spreadProps(rootEl, this.api.getRootProps()); + const controlEl = this.el.querySelector( + '[data-scope="carousel"][data-part="control"]' + ); + if (controlEl) this.spreadProps(controlEl, this.api.getControlProps()); + const itemGroupEl = this.el.querySelector( + '[data-scope="carousel"][data-part="item-group"]' + ); + if (itemGroupEl) this.spreadProps(itemGroupEl, this.api.getItemGroupProps()); + const slideCount = Number(this.el.dataset.slideCount) || 0; + for (let i = 0; i < slideCount; i++) { + const itemEl = this.el.querySelector( + `[data-scope="carousel"][data-part="item"][data-index="${i}"]` + ); + if (itemEl) this.spreadProps(itemEl, this.api.getItemProps({ index: i })); + } + const prevTriggerEl = this.el.querySelector( + '[data-scope="carousel"][data-part="prev-trigger"]' + ); + if (prevTriggerEl) this.spreadProps(prevTriggerEl, this.api.getPrevTriggerProps()); + const nextTriggerEl = this.el.querySelector( + '[data-scope="carousel"][data-part="next-trigger"]' + ); + if (nextTriggerEl) this.spreadProps(nextTriggerEl, this.api.getNextTriggerProps()); + const autoplayTriggerEl = this.el.querySelector( + '[data-scope="carousel"][data-part="autoplay-trigger"]' + ); + if (autoplayTriggerEl) this.spreadProps(autoplayTriggerEl, this.api.getAutoplayTriggerProps()); + const indicatorGroupEl = this.el.querySelector( + '[data-scope="carousel"][data-part="indicator-group"]' + ); + if (indicatorGroupEl) this.spreadProps(indicatorGroupEl, this.api.getIndicatorGroupProps()); + const indicatorCount = this.api.pageSnapPoints.length; + for (let i = 0; i < indicatorCount; i++) { + const indicatorEl = this.el.querySelector( + `[data-scope="carousel"][data-part="indicator"][data-index="${i}"]` + ); + if (indicatorEl) + this.spreadProps(indicatorEl, this.api.getIndicatorProps({ index: i })); + } + const progressTextEl = this.el.querySelector( + '[data-scope="carousel"][data-part="progress-text"]' + ); + if (progressTextEl) this.spreadProps(progressTextEl, this.api.getProgressTextProps()); + } +}; + +// hooks/carousel.ts +var CarouselHook = { + mounted() { + const el = this.el; + const page = getNumber(el, "page"); + const defaultPage = getNumber(el, "defaultPage"); + const controlled = getBoolean(el, "controlled"); + const slideCount = getNumber(el, "slideCount"); + if (slideCount == null || slideCount < 1) { + return; + } + const zag = new Carousel(el, { + id: el.id, + slideCount, + ...controlled && page !== void 0 ? { page } : { defaultPage: defaultPage ?? 0 }, + dir: getDir(el), + orientation: getString(el, "orientation", [ + "horizontal", + "vertical" + ]), + slidesPerPage: getNumber(el, "slidesPerPage") ?? 1, + slidesPerMove: getString(el, "slidesPerMove") === "auto" ? "auto" : getNumber(el, "slidesPerMove"), + loop: getBoolean(el, "loop"), + autoplay: getBoolean(el, "autoplay") ? { delay: getNumber(el, "autoplayDelay") ?? 4e3 } : false, + allowMouseDrag: getBoolean(el, "allowMouseDrag"), + spacing: getString(el, "spacing") ?? "0px", + padding: getString(el, "padding"), + inViewThreshold: getNumber(el, "inViewThreshold") ?? 0.6, + snapType: getString(el, "snapType", ["proximity", "mandatory"]), + autoSize: getBoolean(el, "autoSize"), + onPageChange: (details) => { + const eventName = getString(el, "onPageChange"); + if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) { + this.pushEvent(eventName, { + page: details.page, + pageSnapPoint: details.pageSnapPoint, + id: el.id + }); + } + const clientName = getString(el, "onPageChangeClient"); + if (clientName) { + el.dispatchEvent( + new CustomEvent(clientName, { + bubbles: true, + detail: { value: details, id: el.id } + }) + ); + } + } + }); + zag.init(); + this.carousel = zag; + this.handlers = []; + }, + updated() { + const slideCount = getNumber(this.el, "slideCount"); + if (slideCount == null || slideCount < 1) return; + const page = getNumber(this.el, "page"); + const controlled = getBoolean(this.el, "controlled"); + this.carousel?.updateProps({ + id: this.el.id, + slideCount, + ...controlled && page !== void 0 ? { page } : {}, + dir: getDir(this.el), + orientation: getString(this.el, "orientation", [ + "horizontal", + "vertical" + ]), + slidesPerPage: getNumber(this.el, "slidesPerPage") ?? 1, + loop: getBoolean(this.el, "loop"), + allowMouseDrag: getBoolean(this.el, "allowMouseDrag") + }); + }, + destroyed() { + if (this.handlers) { + for (const h of this.handlers) this.removeHandleEvent(h); + } + this.carousel?.destroy(); + } +}; +export { + CarouselHook as Carousel +}; diff --git a/priv/static/checkbox.mjs b/priv/static/checkbox.mjs index 74942be..30be8a2 100644 --- a/priv/static/checkbox.mjs +++ b/priv/static/checkbox.mjs @@ -1,7 +1,7 @@ import { isFocusVisible, trackFocusVisible -} from "./chunk-EAMC7PNF.mjs"; +} from "./chunk-TEV2GE3U.mjs"; import { Component, VanillaMachine, @@ -21,7 +21,7 @@ import { trackFormControl, trackPress, visuallyHiddenStyle -} from "./chunk-GFGFZBBD.mjs"; +} from "./chunk-IXOYOLUJ.mjs"; // ../node_modules/.pnpm/@zag-js+checkbox@1.33.1/node_modules/@zag-js/checkbox/dist/index.mjs var anatomy = createAnatomy("checkbox").parts("root", "label", "control", "indicator"); diff --git a/priv/static/chunk-2DWEYSRA.mjs b/priv/static/chunk-2PO3TGCF.mjs similarity index 82% rename from priv/static/chunk-2DWEYSRA.mjs rename to priv/static/chunk-2PO3TGCF.mjs index 43cc6c3..fc89379 100644 --- a/priv/static/chunk-2DWEYSRA.mjs +++ b/priv/static/chunk-2PO3TGCF.mjs @@ -1,8 +1,11 @@ import { + chunk, hasProp, isEqual, - isObject -} from "./chunk-GFGFZBBD.mjs"; + isObject, + nextIndex, + prevIndex +} from "./chunk-IXOYOLUJ.mjs"; // ../node_modules/.pnpm/@zag-js+collection@1.33.1/node_modules/@zag-js/collection/dist/index.mjs var __defProp = Object.defineProperty; @@ -376,6 +379,247 @@ function move(items, indices, toIndex) { toIndex = Math.max(0, toIndex - indices.filter((i) => i < toIndex).length); return [...items.slice(0, toIndex), ...itemsToMove, ...items.slice(toIndex)]; } +var GridCollection = class extends ListCollection { + constructor(options) { + const { columnCount } = options; + super(options); + __publicField(this, "columnCount"); + __publicField(this, "rows", null); + __publicField(this, "getRows", () => { + if (!this.rows) { + this.rows = chunk([...this.items], this.columnCount); + } + return this.rows; + }); + __publicField(this, "getRowCount", () => { + return Math.ceil(this.items.length / this.columnCount); + }); + __publicField(this, "getCellIndex", (row, column) => { + return row * this.columnCount + column; + }); + __publicField(this, "getCell", (row, column) => { + return this.at(this.getCellIndex(row, column)); + }); + __publicField(this, "getValueCell", (value) => { + const index = this.indexOf(value); + if (index === -1) return null; + const row = Math.floor(index / this.columnCount); + const column = index % this.columnCount; + return { row, column }; + }); + __publicField(this, "getLastEnabledColumnIndex", (row) => { + for (let col = this.columnCount - 1; col >= 0; col--) { + const cell = this.getCell(row, col); + if (cell && !this.getItemDisabled(cell)) { + return col; + } + } + return null; + }); + __publicField(this, "getFirstEnabledColumnIndex", (row) => { + for (let col = 0; col < this.columnCount; col++) { + const cell = this.getCell(row, col); + if (cell && !this.getItemDisabled(cell)) { + return col; + } + } + return null; + }); + __publicField(this, "getPreviousRowValue", (value, loop = false) => { + const currentCell = this.getValueCell(value); + if (currentCell === null) return null; + const rows = this.getRows(); + const rowCount = rows.length; + let prevRowIndex = currentCell.row; + let prevColumnIndex = currentCell.column; + for (let i = 1; i <= rowCount; i++) { + prevRowIndex = prevIndex(rows, prevRowIndex, { loop }); + const prevRow = rows[prevRowIndex]; + if (!prevRow) continue; + const prevCell = prevRow[prevColumnIndex]; + if (!prevCell) { + const lastColumnIndex = this.getLastEnabledColumnIndex(prevRowIndex); + if (lastColumnIndex != null) { + prevColumnIndex = lastColumnIndex; + } + } + const cell = this.getCell(prevRowIndex, prevColumnIndex); + if (!this.getItemDisabled(cell)) { + return this.getItemValue(cell); + } + } + return this.firstValue; + }); + __publicField(this, "getNextRowValue", (value, loop = false) => { + const currentCell = this.getValueCell(value); + if (currentCell === null) return null; + const rows = this.getRows(); + const rowCount = rows.length; + let nextRowIndex = currentCell.row; + let nextColumnIndex = currentCell.column; + for (let i = 1; i <= rowCount; i++) { + nextRowIndex = nextIndex(rows, nextRowIndex, { loop }); + const nextRow = rows[nextRowIndex]; + if (!nextRow) continue; + const nextCell = nextRow[nextColumnIndex]; + if (!nextCell) { + const lastColumnIndex = this.getLastEnabledColumnIndex(nextRowIndex); + if (lastColumnIndex != null) { + nextColumnIndex = lastColumnIndex; + } + } + const cell = this.getCell(nextRowIndex, nextColumnIndex); + if (!this.getItemDisabled(cell)) { + return this.getItemValue(cell); + } + } + return this.lastValue; + }); + this.columnCount = columnCount; + } +}; +function isGridCollection(v) { + return hasProp(v, "columnCount") && hasProp(v, "getRows"); +} +var Selection = class _Selection extends Set { + constructor(values = []) { + super(values); + __publicField(this, "selectionMode", "single"); + __publicField(this, "deselectable", true); + __publicField(this, "copy", () => { + const clone = new _Selection([...this]); + return this.sync(clone); + }); + __publicField(this, "sync", (other) => { + other.selectionMode = this.selectionMode; + other.deselectable = this.deselectable; + return other; + }); + __publicField(this, "isEmpty", () => { + return this.size === 0; + }); + __publicField(this, "isSelected", (value) => { + if (this.selectionMode === "none" || value == null) { + return false; + } + return this.has(value); + }); + __publicField(this, "canSelect", (collection, value) => { + return this.selectionMode !== "none" || !collection.getItemDisabled(collection.find(value)); + }); + __publicField(this, "firstSelectedValue", (collection) => { + let firstValue = null; + for (let value of this) { + if (!firstValue || collection.compareValue(value, firstValue) < 0) { + firstValue = value; + } + } + return firstValue; + }); + __publicField(this, "lastSelectedValue", (collection) => { + let lastValue = null; + for (let value of this) { + if (!lastValue || collection.compareValue(value, lastValue) > 0) { + lastValue = value; + } + } + return lastValue; + }); + __publicField(this, "extendSelection", (collection, anchorValue, targetValue) => { + if (this.selectionMode === "none") { + return this; + } + if (this.selectionMode === "single") { + return this.replaceSelection(collection, targetValue); + } + const selection = this.copy(); + const lastSelected = Array.from(this).pop(); + for (let key of collection.getValueRange(anchorValue, lastSelected ?? targetValue)) { + selection.delete(key); + } + for (let key of collection.getValueRange(targetValue, anchorValue)) { + if (this.canSelect(collection, key)) { + selection.add(key); + } + } + return selection; + }); + __publicField(this, "toggleSelection", (collection, value) => { + if (this.selectionMode === "none") { + return this; + } + if (this.selectionMode === "single" && !this.isSelected(value)) { + return this.replaceSelection(collection, value); + } + const selection = this.copy(); + if (selection.has(value)) { + selection.delete(value); + } else if (selection.canSelect(collection, value)) { + selection.add(value); + } + return selection; + }); + __publicField(this, "replaceSelection", (collection, value) => { + if (this.selectionMode === "none") { + return this; + } + if (value == null) { + return this; + } + if (!this.canSelect(collection, value)) { + return this; + } + const selection = new _Selection([value]); + return this.sync(selection); + }); + __publicField(this, "setSelection", (values2) => { + if (this.selectionMode === "none") { + return this; + } + let selection = new _Selection(); + for (let value of values2) { + if (value != null) { + selection.add(value); + if (this.selectionMode === "single") { + break; + } + } + } + return this.sync(selection); + }); + __publicField(this, "clearSelection", () => { + const selection = this.copy(); + if (selection.deselectable && selection.size > 0) { + selection.clear(); + } + return selection; + }); + __publicField(this, "select", (collection, value, forceToggle) => { + if (this.selectionMode === "none") { + return this; + } + if (this.selectionMode === "single") { + if (this.isSelected(value) && this.deselectable) { + return this.toggleSelection(collection, value); + } else { + return this.replaceSelection(collection, value); + } + } else if (this.selectionMode === "multiple" || forceToggle) { + return this.toggleSelection(collection, value); + } else { + return this.replaceSelection(collection, value); + } + }); + __publicField(this, "deselect", (value) => { + const selection = this.copy(); + selection.delete(value); + return selection; + }); + __publicField(this, "isEqual", (other) => { + return isEqual(Array.from(this), Array.from(other)); + }); + } +}; function access(node, indexPath, options) { for (let i = 0; i < indexPath.length; i++) node = options.getChildren(node, indexPath.slice(i + 1))[indexPath[i]]; return node; @@ -1071,5 +1315,8 @@ var fallbackMethods = { export { ListCollection, + GridCollection, + isGridCollection, + Selection, TreeCollection }; diff --git a/priv/static/chunk-BMVNROAE.mjs b/priv/static/chunk-BMVNROAE.mjs new file mode 100644 index 0000000..199581d --- /dev/null +++ b/priv/static/chunk-BMVNROAE.mjs @@ -0,0 +1,432 @@ +// ../node_modules/.pnpm/@zag-js+rect-utils@1.33.1/node_modules/@zag-js/rect-utils/dist/index.mjs +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +var AffineTransform = class _AffineTransform { + constructor([m00, m01, m02, m10, m11, m12] = [0, 0, 0, 0, 0, 0]) { + __publicField(this, "m00"); + __publicField(this, "m01"); + __publicField(this, "m02"); + __publicField(this, "m10"); + __publicField(this, "m11"); + __publicField(this, "m12"); + __publicField(this, "rotate", (...args) => { + return this.prepend(_AffineTransform.rotate(...args)); + }); + __publicField(this, "scale", (...args) => { + return this.prepend(_AffineTransform.scale(...args)); + }); + __publicField(this, "translate", (...args) => { + return this.prepend(_AffineTransform.translate(...args)); + }); + this.m00 = m00; + this.m01 = m01; + this.m02 = m02; + this.m10 = m10; + this.m11 = m11; + this.m12 = m12; + } + applyTo(point) { + const { x, y } = point; + const { m00, m01, m02, m10, m11, m12 } = this; + return { + x: m00 * x + m01 * y + m02, + y: m10 * x + m11 * y + m12 + }; + } + prepend(other) { + return new _AffineTransform([ + this.m00 * other.m00 + this.m01 * other.m10, + // m00 + this.m00 * other.m01 + this.m01 * other.m11, + // m01 + this.m00 * other.m02 + this.m01 * other.m12 + this.m02, + // m02 + this.m10 * other.m00 + this.m11 * other.m10, + // m10 + this.m10 * other.m01 + this.m11 * other.m11, + // m11 + this.m10 * other.m02 + this.m11 * other.m12 + this.m12 + // m12 + ]); + } + append(other) { + return new _AffineTransform([ + other.m00 * this.m00 + other.m01 * this.m10, + // m00 + other.m00 * this.m01 + other.m01 * this.m11, + // m01 + other.m00 * this.m02 + other.m01 * this.m12 + other.m02, + // m02 + other.m10 * this.m00 + other.m11 * this.m10, + // m10 + other.m10 * this.m01 + other.m11 * this.m11, + // m11 + other.m10 * this.m02 + other.m11 * this.m12 + other.m12 + // m12 + ]); + } + get determinant() { + return this.m00 * this.m11 - this.m01 * this.m10; + } + get isInvertible() { + const det = this.determinant; + return isFinite(det) && isFinite(this.m02) && isFinite(this.m12) && det !== 0; + } + invert() { + const det = this.determinant; + return new _AffineTransform([ + this.m11 / det, + // m00 + -this.m01 / det, + // m01 + (this.m01 * this.m12 - this.m11 * this.m02) / det, + // m02 + -this.m10 / det, + // m10 + this.m00 / det, + // m11 + (this.m10 * this.m02 - this.m00 * this.m12) / det + // m12 + ]); + } + get array() { + return [this.m00, this.m01, this.m02, this.m10, this.m11, this.m12, 0, 0, 1]; + } + get float32Array() { + return new Float32Array(this.array); + } + // Static + static get identity() { + return new _AffineTransform([1, 0, 0, 0, 1, 0]); + } + static rotate(theta, origin) { + const rotation = new _AffineTransform([Math.cos(theta), -Math.sin(theta), 0, Math.sin(theta), Math.cos(theta), 0]); + if (origin && (origin.x !== 0 || origin.y !== 0)) { + return _AffineTransform.multiply( + _AffineTransform.translate(origin.x, origin.y), + rotation, + _AffineTransform.translate(-origin.x, -origin.y) + ); + } + return rotation; + } + static scale(sx, sy = sx, origin = { x: 0, y: 0 }) { + const scale = new _AffineTransform([sx, 0, 0, 0, sy, 0]); + if (origin.x !== 0 || origin.y !== 0) { + return _AffineTransform.multiply( + _AffineTransform.translate(origin.x, origin.y), + scale, + _AffineTransform.translate(-origin.x, -origin.y) + ); + } + return scale; + } + static translate(tx, ty) { + return new _AffineTransform([1, 0, tx, 0, 1, ty]); + } + static multiply(...[first, ...rest]) { + if (!first) return _AffineTransform.identity; + return rest.reduce((result, item) => result.prepend(item), first); + } + get a() { + return this.m00; + } + get b() { + return this.m10; + } + get c() { + return this.m01; + } + get d() { + return this.m11; + } + get tx() { + return this.m02; + } + get ty() { + return this.m12; + } + get scaleComponents() { + return { x: this.a, y: this.d }; + } + get translationComponents() { + return { x: this.tx, y: this.ty }; + } + get skewComponents() { + return { x: this.c, y: this.b }; + } + toString() { + return `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.tx}, ${this.ty})`; + } +}; +function getPointAngle(rect, point, reference = rect.center) { + const x = point.x - reference.x; + const y = point.y - reference.y; + const deg = Math.atan2(x, y) * (180 / Math.PI) + 180; + return 360 - deg; +} +var clamp = (value, min3, max2) => Math.min(Math.max(value, min3), max2); +var clampPoint = (position, size, boundaryRect) => { + const x = clamp(position.x, boundaryRect.x, boundaryRect.x + boundaryRect.width - size.width); + const y = clamp(position.y, boundaryRect.y, boundaryRect.y + boundaryRect.height - size.height); + return { x, y }; +}; +var defaultMinSize = { + width: 0, + height: 0 +}; +var defaultMaxSize = { + width: Infinity, + height: Infinity +}; +var clampSize = (size, minSize = defaultMinSize, maxSize = defaultMaxSize) => { + return { + width: Math.min(Math.max(size.width, minSize.width), maxSize.width), + height: Math.min(Math.max(size.height, minSize.height), maxSize.height) + }; +}; +var createPoint = (x, y) => ({ x, y }); +var subtractPoints = (a, b) => { + if (!b) return a; + return createPoint(a.x - b.x, a.y - b.y); +}; +var addPoints = (a, b) => createPoint(a.x + b.x, a.y + b.y); +function createRect(r) { + const { x, y, width, height } = r; + const midX = x + width / 2; + const midY = y + height / 2; + return { + x, + y, + width, + height, + minX: x, + minY: y, + maxX: x + width, + maxY: y + height, + midX, + midY, + center: createPoint(midX, midY) + }; +} +function getRectCorners(v) { + const top = createPoint(v.minX, v.minY); + const right = createPoint(v.maxX, v.minY); + const bottom = createPoint(v.maxX, v.maxY); + const left = createPoint(v.minX, v.maxY); + return { top, right, bottom, left }; +} +var constrainRect = (rect, boundary) => { + const left = Math.max(boundary.x, Math.min(rect.x, boundary.x + boundary.width - rect.width)); + const top = Math.max(boundary.y, Math.min(rect.y, boundary.y + boundary.height - rect.height)); + return { + x: left, + y: top, + width: Math.min(rect.width, boundary.width), + height: Math.min(rect.height, boundary.height) + }; +}; +var isSizeEqual = (a, b) => { + return a.width === b?.width && a.height === b?.height; +}; +var isPointEqual = (a, b) => { + return a.x === b?.x && a.y === b?.y; +}; +var styleCache = /* @__PURE__ */ new WeakMap(); +function getCacheComputedStyle(el) { + if (!styleCache.has(el)) { + const win = el.ownerDocument.defaultView || window; + styleCache.set(el, win.getComputedStyle(el)); + } + return styleCache.get(el); +} +function getElementRect(el, opts = {}) { + return createRect(getClientRect(el, opts)); +} +function getClientRect(el, opts = {}) { + const { excludeScrollbar = false, excludeBorders = false } = opts; + const { x, y, width, height } = el.getBoundingClientRect(); + const r = { x, y, width, height }; + const style = getCacheComputedStyle(el); + const { borderLeftWidth, borderTopWidth, borderRightWidth, borderBottomWidth } = style; + const borderXWidth = sum(borderLeftWidth, borderRightWidth); + const borderYWidth = sum(borderTopWidth, borderBottomWidth); + if (excludeBorders) { + r.width -= borderXWidth; + r.height -= borderYWidth; + r.x += px(borderLeftWidth); + r.y += px(borderTopWidth); + } + if (excludeScrollbar) { + const scrollbarWidth = el.offsetWidth - el.clientWidth - borderXWidth; + const scrollbarHeight = el.offsetHeight - el.clientHeight - borderYWidth; + r.width -= scrollbarWidth; + r.height -= scrollbarHeight; + } + return r; +} +var px = (v) => parseFloat(v.replace("px", "")); +var sum = (...vals) => vals.reduce((sum2, v) => sum2 + (v ? px(v) : 0), 0); +var { min, max } = Math; +function getWindowRect(win, opts = {}) { + return createRect(getViewportRect(win, opts)); +} +function getViewportRect(win, opts) { + const { excludeScrollbar = false } = opts; + const { innerWidth, innerHeight, document: doc, visualViewport } = win; + const width = visualViewport?.width || innerWidth; + const height = visualViewport?.height || innerHeight; + const rect = { x: 0, y: 0, width, height }; + if (excludeScrollbar) { + const scrollbarWidth = innerWidth - doc.documentElement.clientWidth; + const scrollbarHeight = innerHeight - doc.documentElement.clientHeight; + rect.width -= scrollbarWidth; + rect.height -= scrollbarHeight; + } + return rect; +} +function getElementPolygon(rectValue, placement) { + const rect = createRect(rectValue); + const { top, right, left, bottom } = getRectCorners(rect); + const [base] = placement.split("-"); + return { + top: [left, top, right, bottom], + right: [top, right, bottom, left], + bottom: [top, left, bottom, right], + left: [right, top, left, bottom] + }[base]; +} +function isPointInPolygon(polygon, point) { + const { x, y } = point; + let c = false; + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const xi = polygon[i].x; + const yi = polygon[i].y; + const xj = polygon[j].x; + const yj = polygon[j].y; + if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) { + c = !c; + } + } + return c; +} +var compassDirectionMap = { + n: { x: 0.5, y: 0 }, + ne: { x: 1, y: 0 }, + e: { x: 1, y: 0.5 }, + se: { x: 1, y: 1 }, + s: { x: 0.5, y: 1 }, + sw: { x: 0, y: 1 }, + w: { x: 0, y: 0.5 }, + nw: { x: 0, y: 0 } +}; +var oppositeDirectionMap = { + n: "s", + ne: "sw", + e: "w", + se: "nw", + s: "n", + sw: "ne", + w: "e", + nw: "se" +}; +var { sign, abs, min: min2 } = Math; +function getRectExtentPoint(rect, direction) { + const { minX, minY, maxX, maxY, midX, midY } = rect; + const x = direction.includes("w") ? minX : direction.includes("e") ? maxX : midX; + const y = direction.includes("n") ? minY : direction.includes("s") ? maxY : midY; + return { x, y }; +} +function getOppositeDirection(direction) { + return oppositeDirectionMap[direction]; +} +function resizeRect(rect, offset, direction, opts) { + const { scalingOriginMode, lockAspectRatio } = opts; + const extent = getRectExtentPoint(rect, direction); + const oppositeDirection = getOppositeDirection(direction); + const oppositeExtent = getRectExtentPoint(rect, oppositeDirection); + if (scalingOriginMode === "center") { + offset = { x: offset.x * 2, y: offset.y * 2 }; + } + const newExtent = { + x: extent.x + offset.x, + y: extent.y + offset.y + }; + const multiplier = { + x: compassDirectionMap[direction].x * 2 - 1, + y: compassDirectionMap[direction].y * 2 - 1 + }; + const newSize = { + width: newExtent.x - oppositeExtent.x, + height: newExtent.y - oppositeExtent.y + }; + const scaleX = multiplier.x * newSize.width / rect.width; + const scaleY = multiplier.y * newSize.height / rect.height; + const largestMagnitude = abs(scaleX) > abs(scaleY) ? scaleX : scaleY; + const scale = lockAspectRatio ? { x: largestMagnitude, y: largestMagnitude } : { + x: extent.x === oppositeExtent.x ? 1 : scaleX, + y: extent.y === oppositeExtent.y ? 1 : scaleY + }; + if (extent.y === oppositeExtent.y) { + scale.y = abs(scale.y); + } else if (sign(scale.y) !== sign(scaleY)) { + scale.y *= -1; + } + if (extent.x === oppositeExtent.x) { + scale.x = abs(scale.x); + } else if (sign(scale.x) !== sign(scaleX)) { + scale.x *= -1; + } + switch (scalingOriginMode) { + case "extent": + return transformRect(rect, AffineTransform.scale(scale.x, scale.y, oppositeExtent), false); + case "center": + return transformRect( + rect, + AffineTransform.scale(scale.x, scale.y, { + x: rect.midX, + y: rect.midY + }), + false + ); + } +} +function createRectFromPoints(initialPoint, finalPoint, normalized = true) { + if (normalized) { + return { + x: min2(finalPoint.x, initialPoint.x), + y: min2(finalPoint.y, initialPoint.y), + width: abs(finalPoint.x - initialPoint.x), + height: abs(finalPoint.y - initialPoint.y) + }; + } + return { + x: initialPoint.x, + y: initialPoint.y, + width: finalPoint.x - initialPoint.x, + height: finalPoint.y - initialPoint.y + }; +} +function transformRect(rect, transform, normalized = true) { + const p1 = transform.applyTo({ x: rect.minX, y: rect.minY }); + const p2 = transform.applyTo({ x: rect.maxX, y: rect.maxY }); + return createRectFromPoints(p1, p2, normalized); +} + +export { + getPointAngle, + clampPoint, + clampSize, + subtractPoints, + addPoints, + createRect, + constrainRect, + isSizeEqual, + isPointEqual, + getElementRect, + getWindowRect, + getElementPolygon, + isPointInPolygon, + resizeRect +}; diff --git a/priv/static/chunk-BPSX7Z7Y.mjs b/priv/static/chunk-BPSX7Z7Y.mjs deleted file mode 100644 index 73b6b56..0000000 --- a/priv/static/chunk-BPSX7Z7Y.mjs +++ /dev/null @@ -1,517 +0,0 @@ -import { - addDomEvent, - callAll, - contains, - getDocument, - getEventTarget, - getNearestOverflowAncestor, - getWindow, - isContextMenuEvent, - isControlledElement, - isFocusable, - isFunction, - isHTMLElement, - isShadowRoot, - isTouchDevice, - nextTick, - raf, - setStyle, - waitForElement, - warn -} from "./chunk-GFGFZBBD.mjs"; - -// ../node_modules/.pnpm/@zag-js+interact-outside@1.33.1/node_modules/@zag-js/interact-outside/dist/index.mjs -function getWindowFrames(win) { - const frames = { - each(cb) { - for (let i = 0; i < win.frames?.length; i += 1) { - const frame = win.frames[i]; - if (frame) cb(frame); - } - }, - addEventListener(event, listener, options) { - frames.each((frame) => { - try { - frame.document.addEventListener(event, listener, options); - } catch { - } - }); - return () => { - try { - frames.removeEventListener(event, listener, options); - } catch { - } - }; - }, - removeEventListener(event, listener, options) { - frames.each((frame) => { - try { - frame.document.removeEventListener(event, listener, options); - } catch { - } - }); - } - }; - return frames; -} -function getParentWindow(win) { - const parent = win.frameElement != null ? win.parent : null; - return { - addEventListener: (event, listener, options) => { - try { - parent?.addEventListener(event, listener, options); - } catch { - } - return () => { - try { - parent?.removeEventListener(event, listener, options); - } catch { - } - }; - }, - removeEventListener: (event, listener, options) => { - try { - parent?.removeEventListener(event, listener, options); - } catch { - } - } - }; -} -var POINTER_OUTSIDE_EVENT = "pointerdown.outside"; -var FOCUS_OUTSIDE_EVENT = "focus.outside"; -function isComposedPathFocusable(composedPath) { - for (const node of composedPath) { - if (isHTMLElement(node) && isFocusable(node)) return true; - } - return false; -} -var isPointerEvent = (event) => "clientY" in event; -function isEventPointWithin(node, event) { - if (!isPointerEvent(event) || !node) return false; - const rect = node.getBoundingClientRect(); - if (rect.width === 0 || rect.height === 0) return false; - return rect.top <= event.clientY && event.clientY <= rect.top + rect.height && rect.left <= event.clientX && event.clientX <= rect.left + rect.width; -} -function isPointInRect(rect, point) { - return rect.y <= point.y && point.y <= rect.y + rect.height && rect.x <= point.x && point.x <= rect.x + rect.width; -} -function isEventWithinScrollbar(event, ancestor) { - if (!ancestor || !isPointerEvent(event)) return false; - const isScrollableY = ancestor.scrollHeight > ancestor.clientHeight; - const onScrollbarY = isScrollableY && event.clientX > ancestor.offsetLeft + ancestor.clientWidth; - const isScrollableX = ancestor.scrollWidth > ancestor.clientWidth; - const onScrollbarX = isScrollableX && event.clientY > ancestor.offsetTop + ancestor.clientHeight; - const rect = { - x: ancestor.offsetLeft, - y: ancestor.offsetTop, - width: ancestor.clientWidth + (isScrollableY ? 16 : 0), - height: ancestor.clientHeight + (isScrollableX ? 16 : 0) - }; - const point = { - x: event.clientX, - y: event.clientY - }; - if (!isPointInRect(rect, point)) return false; - return onScrollbarY || onScrollbarX; -} -function trackInteractOutsideImpl(node, options) { - const { - exclude, - onFocusOutside, - onPointerDownOutside, - onInteractOutside, - defer, - followControlledElements = true - } = options; - if (!node) return; - const doc = getDocument(node); - const win = getWindow(node); - const frames = getWindowFrames(win); - const parentWin = getParentWindow(win); - function isEventOutside(event, target) { - if (!isHTMLElement(target)) return false; - if (!target.isConnected) return false; - if (contains(node, target)) return false; - if (isEventPointWithin(node, event)) return false; - if (followControlledElements && isControlledElement(node, target)) return false; - const triggerEl = doc.querySelector(`[aria-controls="${node.id}"]`); - if (triggerEl) { - const triggerAncestor = getNearestOverflowAncestor(triggerEl); - if (isEventWithinScrollbar(event, triggerAncestor)) return false; - } - const nodeAncestor = getNearestOverflowAncestor(node); - if (isEventWithinScrollbar(event, nodeAncestor)) return false; - return !exclude?.(target); - } - const pointerdownCleanups = /* @__PURE__ */ new Set(); - const isInShadowRoot = isShadowRoot(node?.getRootNode()); - function onPointerDown(event) { - function handler(clickEvent) { - const func = defer && !isTouchDevice() ? raf : (v) => v(); - const evt = clickEvent ?? event; - const composedPath = evt?.composedPath?.() ?? [evt?.target]; - func(() => { - const target = isInShadowRoot ? composedPath[0] : getEventTarget(event); - if (!node || !isEventOutside(event, target)) return; - if (onPointerDownOutside || onInteractOutside) { - const handler2 = callAll(onPointerDownOutside, onInteractOutside); - node.addEventListener(POINTER_OUTSIDE_EVENT, handler2, { once: true }); - } - fireCustomEvent(node, POINTER_OUTSIDE_EVENT, { - bubbles: false, - cancelable: true, - detail: { - originalEvent: evt, - contextmenu: isContextMenuEvent(evt), - focusable: isComposedPathFocusable(composedPath), - target - } - }); - }); - } - if (event.pointerType === "touch") { - pointerdownCleanups.forEach((fn) => fn()); - pointerdownCleanups.add(addDomEvent(doc, "click", handler, { once: true })); - pointerdownCleanups.add(parentWin.addEventListener("click", handler, { once: true })); - pointerdownCleanups.add(frames.addEventListener("click", handler, { once: true })); - } else { - handler(); - } - } - const cleanups = /* @__PURE__ */ new Set(); - const timer = setTimeout(() => { - cleanups.add(addDomEvent(doc, "pointerdown", onPointerDown, true)); - cleanups.add(parentWin.addEventListener("pointerdown", onPointerDown, true)); - cleanups.add(frames.addEventListener("pointerdown", onPointerDown, true)); - }, 0); - function onFocusin(event) { - const func = defer ? raf : (v) => v(); - func(() => { - const composedPath = event?.composedPath?.() ?? [event?.target]; - const target = isInShadowRoot ? composedPath[0] : getEventTarget(event); - if (!node || !isEventOutside(event, target)) return; - if (onFocusOutside || onInteractOutside) { - const handler = callAll(onFocusOutside, onInteractOutside); - node.addEventListener(FOCUS_OUTSIDE_EVENT, handler, { once: true }); - } - fireCustomEvent(node, FOCUS_OUTSIDE_EVENT, { - bubbles: false, - cancelable: true, - detail: { - originalEvent: event, - contextmenu: false, - focusable: isFocusable(target), - target - } - }); - }); - } - if (!isTouchDevice()) { - cleanups.add(addDomEvent(doc, "focusin", onFocusin, true)); - cleanups.add(parentWin.addEventListener("focusin", onFocusin, true)); - cleanups.add(frames.addEventListener("focusin", onFocusin, true)); - } - return () => { - clearTimeout(timer); - pointerdownCleanups.forEach((fn) => fn()); - cleanups.forEach((fn) => fn()); - }; -} -function trackInteractOutside(nodeOrFn, options) { - const { defer } = options; - const func = defer ? raf : (v) => v(); - const cleanups = []; - cleanups.push( - func(() => { - const node = typeof nodeOrFn === "function" ? nodeOrFn() : nodeOrFn; - cleanups.push(trackInteractOutsideImpl(node, options)); - }) - ); - return () => { - cleanups.forEach((fn) => fn?.()); - }; -} -function fireCustomEvent(el, type, init) { - const win = el.ownerDocument.defaultView || window; - const event = new win.CustomEvent(type, init); - return el.dispatchEvent(event); -} - -// ../node_modules/.pnpm/@zag-js+dismissable@1.33.1/node_modules/@zag-js/dismissable/dist/index.mjs -function trackEscapeKeydown(node, fn) { - const handleKeyDown = (event) => { - if (event.key !== "Escape") return; - if (event.isComposing) return; - fn?.(event); - }; - return addDomEvent(getDocument(node), "keydown", handleKeyDown, { capture: true }); -} -var LAYER_REQUEST_DISMISS_EVENT = "layer:request-dismiss"; -var layerStack = { - layers: [], - branches: [], - recentlyRemoved: /* @__PURE__ */ new Set(), - count() { - return this.layers.length; - }, - pointerBlockingLayers() { - return this.layers.filter((layer) => layer.pointerBlocking); - }, - topMostPointerBlockingLayer() { - return [...this.pointerBlockingLayers()].slice(-1)[0]; - }, - hasPointerBlockingLayer() { - return this.pointerBlockingLayers().length > 0; - }, - isBelowPointerBlockingLayer(node) { - const index = this.indexOf(node); - const highestBlockingIndex = this.topMostPointerBlockingLayer() ? this.indexOf(this.topMostPointerBlockingLayer()?.node) : -1; - return index < highestBlockingIndex; - }, - isTopMost(node) { - const layer = this.layers[this.count() - 1]; - return layer?.node === node; - }, - getNestedLayers(node) { - return Array.from(this.layers).slice(this.indexOf(node) + 1); - }, - getLayersByType(type) { - return this.layers.filter((layer) => layer.type === type); - }, - getNestedLayersByType(node, type) { - const index = this.indexOf(node); - if (index === -1) return []; - return this.layers.slice(index + 1).filter((layer) => layer.type === type); - }, - getParentLayerOfType(node, type) { - const index = this.indexOf(node); - if (index <= 0) return void 0; - return this.layers.slice(0, index).reverse().find((layer) => layer.type === type); - }, - countNestedLayersOfType(node, type) { - return this.getNestedLayersByType(node, type).length; - }, - isInNestedLayer(node, target) { - const inNested = this.getNestedLayers(node).some((layer) => contains(layer.node, target)); - if (inNested) return true; - if (this.recentlyRemoved.size > 0) return true; - return false; - }, - isInBranch(target) { - return Array.from(this.branches).some((branch) => contains(branch, target)); - }, - add(layer) { - this.layers.push(layer); - this.syncLayers(); - }, - addBranch(node) { - this.branches.push(node); - }, - remove(node) { - const index = this.indexOf(node); - if (index < 0) return; - this.recentlyRemoved.add(node); - nextTick(() => this.recentlyRemoved.delete(node)); - if (index < this.count() - 1) { - const _layers = this.getNestedLayers(node); - _layers.forEach((layer) => layerStack.dismiss(layer.node, node)); - } - this.layers.splice(index, 1); - this.syncLayers(); - }, - removeBranch(node) { - const index = this.branches.indexOf(node); - if (index >= 0) this.branches.splice(index, 1); - }, - syncLayers() { - this.layers.forEach((layer, index) => { - layer.node.style.setProperty("--layer-index", `${index}`); - layer.node.removeAttribute("data-nested"); - layer.node.removeAttribute("data-has-nested"); - const parentOfSameType = this.getParentLayerOfType(layer.node, layer.type); - if (parentOfSameType) { - layer.node.setAttribute("data-nested", layer.type); - } - const nestedCount = this.countNestedLayersOfType(layer.node, layer.type); - if (nestedCount > 0) { - layer.node.setAttribute("data-has-nested", layer.type); - } - layer.node.style.setProperty("--nested-layer-count", `${nestedCount}`); - }); - }, - indexOf(node) { - return this.layers.findIndex((layer) => layer.node === node); - }, - dismiss(node, parent) { - const index = this.indexOf(node); - if (index === -1) return; - const layer = this.layers[index]; - addListenerOnce(node, LAYER_REQUEST_DISMISS_EVENT, (event) => { - layer.requestDismiss?.(event); - if (!event.defaultPrevented) { - layer?.dismiss(); - } - }); - fireCustomEvent2(node, LAYER_REQUEST_DISMISS_EVENT, { - originalLayer: node, - targetLayer: parent, - originalIndex: index, - targetIndex: parent ? this.indexOf(parent) : -1 - }); - this.syncLayers(); - }, - clear() { - this.remove(this.layers[0].node); - } -}; -function fireCustomEvent2(el, type, detail) { - const win = el.ownerDocument.defaultView || window; - const event = new win.CustomEvent(type, { cancelable: true, bubbles: true, detail }); - return el.dispatchEvent(event); -} -function addListenerOnce(el, type, callback) { - el.addEventListener(type, callback, { once: true }); -} -var originalBodyPointerEvents; -function assignPointerEventToLayers() { - layerStack.layers.forEach(({ node }) => { - node.style.pointerEvents = layerStack.isBelowPointerBlockingLayer(node) ? "none" : "auto"; - }); -} -function clearPointerEvent(node) { - node.style.pointerEvents = ""; -} -function disablePointerEventsOutside(node, persistentElements) { - const doc = getDocument(node); - const cleanups = []; - if (layerStack.hasPointerBlockingLayer() && !doc.body.hasAttribute("data-inert")) { - originalBodyPointerEvents = document.body.style.pointerEvents; - queueMicrotask(() => { - doc.body.style.pointerEvents = "none"; - doc.body.setAttribute("data-inert", ""); - }); - } - persistentElements?.forEach((el) => { - const [promise, abort] = waitForElement( - () => { - const node2 = el(); - return isHTMLElement(node2) ? node2 : null; - }, - { timeout: 1e3 } - ); - promise.then((el2) => cleanups.push(setStyle(el2, { pointerEvents: "auto" }))); - cleanups.push(abort); - }); - return () => { - if (layerStack.hasPointerBlockingLayer()) return; - queueMicrotask(() => { - doc.body.style.pointerEvents = originalBodyPointerEvents; - doc.body.removeAttribute("data-inert"); - if (doc.body.style.length === 0) doc.body.removeAttribute("style"); - }); - cleanups.forEach((fn) => fn()); - }; -} -function trackDismissableElementImpl(node, options) { - const { warnOnMissingNode = true } = options; - if (warnOnMissingNode && !node) { - warn("[@zag-js/dismissable] node is `null` or `undefined`"); - return; - } - if (!node) { - return; - } - const { onDismiss, onRequestDismiss, pointerBlocking, exclude: excludeContainers, debug, type = "dialog" } = options; - const layer = { dismiss: onDismiss, node, type, pointerBlocking, requestDismiss: onRequestDismiss }; - layerStack.add(layer); - assignPointerEventToLayers(); - function onPointerDownOutside(event) { - const target = getEventTarget(event.detail.originalEvent); - if (layerStack.isBelowPointerBlockingLayer(node) || layerStack.isInBranch(target)) return; - options.onPointerDownOutside?.(event); - options.onInteractOutside?.(event); - if (event.defaultPrevented) return; - if (debug) { - console.log("onPointerDownOutside:", event.detail.originalEvent); - } - onDismiss?.(); - } - function onFocusOutside(event) { - const target = getEventTarget(event.detail.originalEvent); - if (layerStack.isInBranch(target)) return; - options.onFocusOutside?.(event); - options.onInteractOutside?.(event); - if (event.defaultPrevented) return; - if (debug) { - console.log("onFocusOutside:", event.detail.originalEvent); - } - onDismiss?.(); - } - function onEscapeKeyDown(event) { - if (!layerStack.isTopMost(node)) return; - options.onEscapeKeyDown?.(event); - if (!event.defaultPrevented && onDismiss) { - event.preventDefault(); - onDismiss(); - } - } - function exclude(target) { - if (!node) return false; - const containers = typeof excludeContainers === "function" ? excludeContainers() : excludeContainers; - const _containers = Array.isArray(containers) ? containers : [containers]; - const persistentElements = options.persistentElements?.map((fn) => fn()).filter(isHTMLElement); - if (persistentElements) _containers.push(...persistentElements); - return _containers.some((node2) => contains(node2, target)) || layerStack.isInNestedLayer(node, target); - } - const cleanups = [ - pointerBlocking ? disablePointerEventsOutside(node, options.persistentElements) : void 0, - trackEscapeKeydown(node, onEscapeKeyDown), - trackInteractOutside(node, { exclude, onFocusOutside, onPointerDownOutside, defer: options.defer }) - ]; - return () => { - layerStack.remove(node); - assignPointerEventToLayers(); - clearPointerEvent(node); - cleanups.forEach((fn) => fn?.()); - }; -} -function trackDismissableElement(nodeOrFn, options) { - const { defer } = options; - const func = defer ? raf : (v) => v(); - const cleanups = []; - cleanups.push( - func(() => { - const node = isFunction(nodeOrFn) ? nodeOrFn() : nodeOrFn; - cleanups.push(trackDismissableElementImpl(node, options)); - }) - ); - return () => { - cleanups.forEach((fn) => fn?.()); - }; -} -function trackDismissableBranch(nodeOrFn, options = {}) { - const { defer } = options; - const func = defer ? raf : (v) => v(); - const cleanups = []; - cleanups.push( - func(() => { - const node = isFunction(nodeOrFn) ? nodeOrFn() : nodeOrFn; - if (!node) { - warn("[@zag-js/dismissable] branch node is `null` or `undefined`"); - return; - } - layerStack.addBranch(node); - cleanups.push(() => { - layerStack.removeBranch(node); - }); - }) - ); - return () => { - cleanups.forEach((fn) => fn?.()); - }; -} - -export { - trackDismissableElement, - trackDismissableBranch -}; diff --git a/priv/static/chunk-EAMC7PNF.mjs b/priv/static/chunk-EAMC7PNF.mjs deleted file mode 100644 index 9ff17f6..0000000 --- a/priv/static/chunk-EAMC7PNF.mjs +++ /dev/null @@ -1,163 +0,0 @@ -import { - getDocument, - getEventTarget, - getWindow, - isMac, - isVirtualClick -} from "./chunk-GFGFZBBD.mjs"; - -// ../node_modules/.pnpm/@zag-js+focus-visible@1.33.1/node_modules/@zag-js/focus-visible/dist/index.mjs -function isValidKey(e) { - return !(e.metaKey || !isMac() && e.altKey || e.ctrlKey || e.key === "Control" || e.key === "Shift" || e.key === "Meta"); -} -var nonTextInputTypes = /* @__PURE__ */ new Set(["checkbox", "radio", "range", "color", "file", "image", "button", "submit", "reset"]); -function isKeyboardFocusEvent(isTextInput, modality, e) { - const target = e ? getEventTarget(e) : null; - const win = getWindow(target); - isTextInput = isTextInput || target instanceof win.HTMLInputElement && !nonTextInputTypes.has(target?.type) || target instanceof win.HTMLTextAreaElement || target instanceof win.HTMLElement && target.isContentEditable; - return !(isTextInput && modality === "keyboard" && e instanceof win.KeyboardEvent && !Reflect.has(FOCUS_VISIBLE_INPUT_KEYS, e.key)); -} -var currentModality = null; -var changeHandlers = /* @__PURE__ */ new Set(); -var listenerMap = /* @__PURE__ */ new Map(); -var hasEventBeforeFocus = false; -var hasBlurredWindowRecently = false; -var FOCUS_VISIBLE_INPUT_KEYS = { - Tab: true, - Escape: true -}; -function triggerChangeHandlers(modality, e) { - for (let handler of changeHandlers) { - handler(modality, e); - } -} -function handleKeyboardEvent(e) { - hasEventBeforeFocus = true; - if (isValidKey(e)) { - currentModality = "keyboard"; - triggerChangeHandlers("keyboard", e); - } -} -function handlePointerEvent(e) { - currentModality = "pointer"; - if (e.type === "mousedown" || e.type === "pointerdown") { - hasEventBeforeFocus = true; - triggerChangeHandlers("pointer", e); - } -} -function handleClickEvent(e) { - if (isVirtualClick(e)) { - hasEventBeforeFocus = true; - currentModality = "virtual"; - } -} -function handleFocusEvent(e) { - const target = getEventTarget(e); - if (target === getWindow(target) || target === getDocument(target)) { - return; - } - if (!hasEventBeforeFocus && !hasBlurredWindowRecently) { - currentModality = "virtual"; - triggerChangeHandlers("virtual", e); - } - hasEventBeforeFocus = false; - hasBlurredWindowRecently = false; -} -function handleWindowBlur() { - hasEventBeforeFocus = false; - hasBlurredWindowRecently = true; -} -function setupGlobalFocusEvents(root) { - if (typeof window === "undefined" || listenerMap.get(getWindow(root))) { - return; - } - const win = getWindow(root); - const doc = getDocument(root); - let focus = win.HTMLElement.prototype.focus; - function patchedFocus() { - currentModality = "virtual"; - triggerChangeHandlers("virtual", null); - hasEventBeforeFocus = true; - focus.apply(this, arguments); - } - try { - Object.defineProperty(win.HTMLElement.prototype, "focus", { - configurable: true, - value: patchedFocus - }); - } catch { - } - doc.addEventListener("keydown", handleKeyboardEvent, true); - doc.addEventListener("keyup", handleKeyboardEvent, true); - doc.addEventListener("click", handleClickEvent, true); - win.addEventListener("focus", handleFocusEvent, true); - win.addEventListener("blur", handleWindowBlur, false); - if (typeof win.PointerEvent !== "undefined") { - doc.addEventListener("pointerdown", handlePointerEvent, true); - doc.addEventListener("pointermove", handlePointerEvent, true); - doc.addEventListener("pointerup", handlePointerEvent, true); - } else { - doc.addEventListener("mousedown", handlePointerEvent, true); - doc.addEventListener("mousemove", handlePointerEvent, true); - doc.addEventListener("mouseup", handlePointerEvent, true); - } - win.addEventListener( - "beforeunload", - () => { - tearDownWindowFocusTracking(root); - }, - { once: true } - ); - listenerMap.set(win, { focus }); -} -var tearDownWindowFocusTracking = (root, loadListener) => { - const win = getWindow(root); - const doc = getDocument(root); - const listenerData = listenerMap.get(win); - if (!listenerData) { - return; - } - try { - Object.defineProperty(win.HTMLElement.prototype, "focus", { - configurable: true, - value: listenerData.focus - }); - } catch { - } - doc.removeEventListener("keydown", handleKeyboardEvent, true); - doc.removeEventListener("keyup", handleKeyboardEvent, true); - doc.removeEventListener("click", handleClickEvent, true); - win.removeEventListener("focus", handleFocusEvent, true); - win.removeEventListener("blur", handleWindowBlur, false); - if (typeof win.PointerEvent !== "undefined") { - doc.removeEventListener("pointerdown", handlePointerEvent, true); - doc.removeEventListener("pointermove", handlePointerEvent, true); - doc.removeEventListener("pointerup", handlePointerEvent, true); - } else { - doc.removeEventListener("mousedown", handlePointerEvent, true); - doc.removeEventListener("mousemove", handlePointerEvent, true); - doc.removeEventListener("mouseup", handlePointerEvent, true); - } - listenerMap.delete(win); -}; -function isFocusVisible() { - return currentModality === "keyboard"; -} -function trackFocusVisible(props = {}) { - const { isTextInput, autoFocus, onChange, root } = props; - setupGlobalFocusEvents(root); - onChange?.({ isFocusVisible: autoFocus || isFocusVisible(), modality: currentModality }); - const handler = (modality, e) => { - if (!isKeyboardFocusEvent(!!isTextInput, modality, e)) return; - onChange?.({ isFocusVisible: isFocusVisible(), modality }); - }; - changeHandlers.add(handler); - return () => { - changeHandlers.delete(handler); - }; -} - -export { - isFocusVisible, - trackFocusVisible -}; diff --git a/priv/static/chunk-GRHV6R4F.mjs b/priv/static/chunk-EENFWNGI.mjs similarity index 99% rename from priv/static/chunk-GRHV6R4F.mjs rename to priv/static/chunk-EENFWNGI.mjs index 020ce05..832e989 100644 --- a/priv/static/chunk-GRHV6R4F.mjs +++ b/priv/static/chunk-EENFWNGI.mjs @@ -6,7 +6,7 @@ import { isNull, noop, raf -} from "./chunk-GFGFZBBD.mjs"; +} from "./chunk-IXOYOLUJ.mjs"; // ../node_modules/.pnpm/@floating-ui+utils@0.2.10/node_modules/@floating-ui/utils/dist/floating-ui.utils.mjs var sides = ["top", "right", "bottom", "left"]; diff --git a/priv/static/chunk-ER3INIAI.mjs b/priv/static/chunk-ER3INIAI.mjs new file mode 100644 index 0000000..7ac0e66 --- /dev/null +++ b/priv/static/chunk-ER3INIAI.mjs @@ -0,0 +1,237 @@ +import { + addDomEvent, + callAll, + contains, + getDocument, + getEventTarget, + getNearestOverflowAncestor, + getWindow, + isContextMenuEvent, + isControlledElement, + isFocusable, + isHTMLElement, + isShadowRoot, + isTouchDevice, + raf +} from "./chunk-IXOYOLUJ.mjs"; + +// ../node_modules/.pnpm/@zag-js+interact-outside@1.33.1/node_modules/@zag-js/interact-outside/dist/index.mjs +function getWindowFrames(win) { + const frames = { + each(cb) { + for (let i = 0; i < win.frames?.length; i += 1) { + const frame = win.frames[i]; + if (frame) cb(frame); + } + }, + addEventListener(event, listener, options) { + frames.each((frame) => { + try { + frame.document.addEventListener(event, listener, options); + } catch { + } + }); + return () => { + try { + frames.removeEventListener(event, listener, options); + } catch { + } + }; + }, + removeEventListener(event, listener, options) { + frames.each((frame) => { + try { + frame.document.removeEventListener(event, listener, options); + } catch { + } + }); + } + }; + return frames; +} +function getParentWindow(win) { + const parent = win.frameElement != null ? win.parent : null; + return { + addEventListener: (event, listener, options) => { + try { + parent?.addEventListener(event, listener, options); + } catch { + } + return () => { + try { + parent?.removeEventListener(event, listener, options); + } catch { + } + }; + }, + removeEventListener: (event, listener, options) => { + try { + parent?.removeEventListener(event, listener, options); + } catch { + } + } + }; +} +var POINTER_OUTSIDE_EVENT = "pointerdown.outside"; +var FOCUS_OUTSIDE_EVENT = "focus.outside"; +function isComposedPathFocusable(composedPath) { + for (const node of composedPath) { + if (isHTMLElement(node) && isFocusable(node)) return true; + } + return false; +} +var isPointerEvent = (event) => "clientY" in event; +function isEventPointWithin(node, event) { + if (!isPointerEvent(event) || !node) return false; + const rect = node.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) return false; + return rect.top <= event.clientY && event.clientY <= rect.top + rect.height && rect.left <= event.clientX && event.clientX <= rect.left + rect.width; +} +function isPointInRect(rect, point) { + return rect.y <= point.y && point.y <= rect.y + rect.height && rect.x <= point.x && point.x <= rect.x + rect.width; +} +function isEventWithinScrollbar(event, ancestor) { + if (!ancestor || !isPointerEvent(event)) return false; + const isScrollableY = ancestor.scrollHeight > ancestor.clientHeight; + const onScrollbarY = isScrollableY && event.clientX > ancestor.offsetLeft + ancestor.clientWidth; + const isScrollableX = ancestor.scrollWidth > ancestor.clientWidth; + const onScrollbarX = isScrollableX && event.clientY > ancestor.offsetTop + ancestor.clientHeight; + const rect = { + x: ancestor.offsetLeft, + y: ancestor.offsetTop, + width: ancestor.clientWidth + (isScrollableY ? 16 : 0), + height: ancestor.clientHeight + (isScrollableX ? 16 : 0) + }; + const point = { + x: event.clientX, + y: event.clientY + }; + if (!isPointInRect(rect, point)) return false; + return onScrollbarY || onScrollbarX; +} +function trackInteractOutsideImpl(node, options) { + const { + exclude, + onFocusOutside, + onPointerDownOutside, + onInteractOutside, + defer, + followControlledElements = true + } = options; + if (!node) return; + const doc = getDocument(node); + const win = getWindow(node); + const frames = getWindowFrames(win); + const parentWin = getParentWindow(win); + function isEventOutside(event, target) { + if (!isHTMLElement(target)) return false; + if (!target.isConnected) return false; + if (contains(node, target)) return false; + if (isEventPointWithin(node, event)) return false; + if (followControlledElements && isControlledElement(node, target)) return false; + const triggerEl = doc.querySelector(`[aria-controls="${node.id}"]`); + if (triggerEl) { + const triggerAncestor = getNearestOverflowAncestor(triggerEl); + if (isEventWithinScrollbar(event, triggerAncestor)) return false; + } + const nodeAncestor = getNearestOverflowAncestor(node); + if (isEventWithinScrollbar(event, nodeAncestor)) return false; + return !exclude?.(target); + } + const pointerdownCleanups = /* @__PURE__ */ new Set(); + const isInShadowRoot = isShadowRoot(node?.getRootNode()); + function onPointerDown(event) { + function handler(clickEvent) { + const func = defer && !isTouchDevice() ? raf : (v) => v(); + const evt = clickEvent ?? event; + const composedPath = evt?.composedPath?.() ?? [evt?.target]; + func(() => { + const target = isInShadowRoot ? composedPath[0] : getEventTarget(event); + if (!node || !isEventOutside(event, target)) return; + if (onPointerDownOutside || onInteractOutside) { + const handler2 = callAll(onPointerDownOutside, onInteractOutside); + node.addEventListener(POINTER_OUTSIDE_EVENT, handler2, { once: true }); + } + fireCustomEvent(node, POINTER_OUTSIDE_EVENT, { + bubbles: false, + cancelable: true, + detail: { + originalEvent: evt, + contextmenu: isContextMenuEvent(evt), + focusable: isComposedPathFocusable(composedPath), + target + } + }); + }); + } + if (event.pointerType === "touch") { + pointerdownCleanups.forEach((fn) => fn()); + pointerdownCleanups.add(addDomEvent(doc, "click", handler, { once: true })); + pointerdownCleanups.add(parentWin.addEventListener("click", handler, { once: true })); + pointerdownCleanups.add(frames.addEventListener("click", handler, { once: true })); + } else { + handler(); + } + } + const cleanups = /* @__PURE__ */ new Set(); + const timer = setTimeout(() => { + cleanups.add(addDomEvent(doc, "pointerdown", onPointerDown, true)); + cleanups.add(parentWin.addEventListener("pointerdown", onPointerDown, true)); + cleanups.add(frames.addEventListener("pointerdown", onPointerDown, true)); + }, 0); + function onFocusin(event) { + const func = defer ? raf : (v) => v(); + func(() => { + const composedPath = event?.composedPath?.() ?? [event?.target]; + const target = isInShadowRoot ? composedPath[0] : getEventTarget(event); + if (!node || !isEventOutside(event, target)) return; + if (onFocusOutside || onInteractOutside) { + const handler = callAll(onFocusOutside, onInteractOutside); + node.addEventListener(FOCUS_OUTSIDE_EVENT, handler, { once: true }); + } + fireCustomEvent(node, FOCUS_OUTSIDE_EVENT, { + bubbles: false, + cancelable: true, + detail: { + originalEvent: event, + contextmenu: false, + focusable: isFocusable(target), + target + } + }); + }); + } + if (!isTouchDevice()) { + cleanups.add(addDomEvent(doc, "focusin", onFocusin, true)); + cleanups.add(parentWin.addEventListener("focusin", onFocusin, true)); + cleanups.add(frames.addEventListener("focusin", onFocusin, true)); + } + return () => { + clearTimeout(timer); + pointerdownCleanups.forEach((fn) => fn()); + cleanups.forEach((fn) => fn()); + }; +} +function trackInteractOutside(nodeOrFn, options) { + const { defer } = options; + const func = defer ? raf : (v) => v(); + const cleanups = []; + cleanups.push( + func(() => { + const node = typeof nodeOrFn === "function" ? nodeOrFn() : nodeOrFn; + cleanups.push(trackInteractOutsideImpl(node, options)); + }) + ); + return () => { + cleanups.forEach((fn) => fn?.()); + }; +} +function fireCustomEvent(el, type, init) { + const win = el.ownerDocument.defaultView || window; + const event = new win.CustomEvent(type, init); + return el.dispatchEvent(event); +} + +export { + trackInteractOutside +}; diff --git a/priv/static/chunk-GFGFZBBD.mjs b/priv/static/chunk-IXOYOLUJ.mjs similarity index 94% rename from priv/static/chunk-GFGFZBBD.mjs rename to priv/static/chunk-IXOYOLUJ.mjs index 253edf9..388efdc 100644 --- a/priv/static/chunk-GFGFZBBD.mjs +++ b/priv/static/chunk-IXOYOLUJ.mjs @@ -333,6 +333,11 @@ var isMac = () => pt(/^Mac/i); var isSafari = () => isApple() && vn(/apple/i); var isFirefox = () => ua(/Firefox/i); var isAndroid = () => ua(/Android/i); +function getBeforeInputValue(event) { + const { selectionStart, selectionEnd, value } = event.currentTarget; + const data = event.data; + return value.slice(0, selectionStart) + (data ?? "") + value.slice(selectionEnd); +} function getComposedPath(event) { return event.composedPath?.() ?? event.nativeEvent?.composedPath?.(); } @@ -406,6 +411,17 @@ function getEventKey(event, options = {}) { function getNativeEvent(event) { return event.nativeEvent ?? event; } +var pageKeys = /* @__PURE__ */ new Set(["PageUp", "PageDown"]); +var arrowKeys = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]); +function getEventStep(event) { + if (event.ctrlKey || event.metaKey) { + return 0.1; + } else { + const isPageKey = pageKeys.has(event.key); + const isSkipKey = isPageKey || event.shiftKey && arrowKeys.has(event.key); + return isSkipKey ? 10 : 1; + } +} function getEventPoint(event, type = "client") { const point = isTouchEvent(event) ? event.touches[0] || event.changedTouches[0] : event; return { x: point[`${type}X`], y: point[`${type}Y`] }; @@ -443,6 +459,14 @@ function setElementChecked(el, checked) { if (checked) el.setAttribute("checked", ""); else el.removeAttribute("checked"); } +function dispatchInputValueEvent(el, options) { + const { value, bubbles = true } = options; + if (!el) return; + const win = getWindow(el); + if (!(el instanceof win.HTMLInputElement)) return; + setElementValue(el, `${value}`); + el.dispatchEvent(new win.Event("input", { bubbles })); +} function dispatchInputCheckedEvent(el, options) { const { checked, bubbles = true } = options; if (!el) return; @@ -790,6 +814,32 @@ function getRelativePoint(point, element) { } return { offset, percent, getPercentValue }; } +function requestPointerLock(doc, fn) { + const body = doc.body; + const supported = "pointerLockElement" in doc || "mozPointerLockElement" in doc; + const isLocked = () => !!doc.pointerLockElement; + function onPointerChange() { + fn?.(isLocked()); + } + function onPointerError(event) { + if (isLocked()) fn?.(false); + console.error("PointerLock error occurred:", event); + doc.exitPointerLock(); + } + if (!supported) return; + try { + body.requestPointerLock(); + } catch { + } + const cleanup = [ + addDomEvent(doc, "pointerlockchange", onPointerChange, false), + addDomEvent(doc, "pointerlockerror", onPointerError, false) + ]; + return () => { + cleanup.forEach((cleanup2) => cleanup2()); + doc.exitPointerLock(); + }; +} var state = "default"; var userSelect = ""; var elementMap = /* @__PURE__ */ new WeakMap(); @@ -1337,9 +1387,34 @@ function match2(key, record, ...args) { Error.captureStackTrace?.(error, match2); throw error; } +function throttle(fn, wait = 0) { + let lastCall = 0; + let timeout = null; + return (...args) => { + const now = Date.now(); + const timeSinceLastCall = now - lastCall; + if (timeSinceLastCall >= wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + fn(...args); + lastCall = now; + } else if (!timeout) { + timeout = setTimeout(() => { + fn(...args); + lastCall = Date.now(); + timeout = null; + }, wait - timeSinceLastCall); + } + }; +} var { floor, abs, round, min, max, pow, sign } = Math; var isNaN = (v) => Number.isNaN(v); var nan = (v) => isNaN(v) ? 0 : v; +var wrap2 = (v, vmax) => (v % vmax + vmax) % vmax; +var isValueAtMax = (v, vmax) => nan(v) >= vmax; +var isValueAtMin = (v, vmin) => nan(v) <= vmin; var isValueWithinRange = (v, vmin, vmax) => { const value = nan(v); const minCheck = vmin == null || value >= vmin; @@ -1347,6 +1422,59 @@ var isValueWithinRange = (v, vmin, vmax) => { return minCheck && maxCheck; }; var clampValue = (v, vmin, vmax) => min(max(nan(v), vmin), vmax); +var roundToStepPrecision = (v, step) => { + let rv = v; + let ss = step.toString(); + let pi = ss.indexOf("."); + let p = pi >= 0 ? ss.length - pi : 0; + if (p > 0) { + let pw = pow(10, p); + rv = round(rv * pw) / pw; + } + return rv; +}; +var roundToDpr = (v, dpr) => typeof dpr === "number" ? floor(v * dpr + 0.5) / dpr : round(v); +var snapValueToStep = (v, vmin, vmax, step) => { + const min2 = vmin != null ? Number(vmin) : 0; + const max2 = Number(vmax); + const remainder = (v - min2) % step; + let snapped = abs(remainder) * 2 >= step ? v + sign(remainder) * (step - abs(remainder)) : v - remainder; + snapped = roundToStepPrecision(snapped, step); + if (!isNaN(min2) && snapped < min2) { + snapped = min2; + } else if (!isNaN(max2) && snapped > max2) { + const stepsInRange = floor((max2 - min2) / step); + const largestValidStep = min2 + stepsInRange * step; + snapped = stepsInRange <= 0 || largestValidStep < min2 ? max2 : largestValidStep; + } + return roundToStepPrecision(snapped, step); +}; +var setValueAtIndex = (vs, i, v) => { + if (vs[i] === v) return vs; + return [...vs.slice(0, i), v, ...vs.slice(i + 1)]; +}; +var countDecimals = (value) => { + if (!Number.isFinite(value)) return 0; + let e = 1, p = 0; + while (Math.round(value * e) / e !== value) { + e *= 10; + p += 1; + } + return p; +}; +var decimalOp = (a, op, b) => { + let result = op === "+" ? a + b : a - b; + if (a % 1 !== 0 || b % 1 !== 0) { + const multiplier = 10 ** Math.max(countDecimals(a), countDecimals(b)); + a = Math.round(a * multiplier); + b = Math.round(b * multiplier); + result = op === "+" ? a + b : a - b; + result /= multiplier; + } + return result; +}; +var incrementValue = (v, s) => decimalOp(nan(v), "+", s); +var decrementValue = (v, s) => decimalOp(nan(v), "-", s); var toPx = (v) => typeof v === "number" ? `${v}px` : v; function compact(obj) { if (!isPlainObject(obj) || obj === void 0) return obj; @@ -1360,6 +1488,16 @@ function compact(obj) { } return filtered; } +function pick(obj, keys) { + const filtered = {}; + for (const key of keys) { + const value = obj[key]; + if (value !== void 0) { + filtered[key] = value; + } + } + return filtered; +} function splitProps(props, keys) { const rest = {}; const result = {}; @@ -1436,6 +1574,17 @@ var Timer = class { } }; _tick = /* @__PURE__ */ new WeakMap(); +function setRafInterval(fn, intervalMs) { + const timer = new Timer(({ now, deltaMs }) => { + if (deltaMs >= intervalMs) { + const startMs = intervalMs > 0 ? now - deltaMs % intervalMs : now; + timer.setStartMs(startMs); + fn({ startMs, deltaMs }); + } + }); + timer.start(); + return () => timer.stop(); +} function setRafTimeout(fn, delayMs) { const timer = new Timer(({ deltaMs }) => { if (deltaMs >= delayMs) { @@ -1453,6 +1602,13 @@ function warn(...a) { console.warn(m); } } +function invariant(...a) { + const m = a.length === 1 ? a[0] : a[1]; + const c = a.length === 2 ? a[0] : true; + if (c && true) { + throw new Error(m); + } +} function ensure(c, m) { if (c == null) throw new Error(m()); } @@ -2383,12 +2539,15 @@ export { getDataUrl, isTouchDevice, isIos, + isApple, isMac, isSafari, + getBeforeInputValue, getEventTarget, isOpeningInNewTab, isDownloadingEvent, isComposingEvent, + isCtrlOrMetaKey, isPrintableKey, isVirtualClick, isLeftClick, @@ -2396,10 +2555,12 @@ export { isModifierKey, getEventKey, getNativeEvent, + getEventStep, getEventPoint, addDomEvent, setElementValue, setElementChecked, + dispatchInputValueEvent, dispatchInputCheckedEvent, trackFormControl, getFocusables, @@ -2418,6 +2579,7 @@ export { getNearestOverflowAncestor, scrollIntoView, getRelativePoint, + requestPointerLock, restoreTextSelection, disableTextSelection, trackPointerMove, @@ -2442,7 +2604,9 @@ export { uniq, diff, addOrRemove, + nextIndex, next, + prevIndex, prev, chunk, partition, @@ -2459,13 +2623,25 @@ export { callAll, uuid, match2 as match, + throttle, + wrap2 as wrap, + isValueAtMax, + isValueAtMin, isValueWithinRange, clampValue, + roundToDpr, + snapValueToStep, + setValueAtIndex, + incrementValue, + decrementValue, toPx, compact, + pick, createSplitProps, + setRafInterval, setRafTimeout, warn, + invariant, ensure, ensureProps, mergeProps, @@ -2474,6 +2650,8 @@ export { createMachine, setup, createProps, + proxy, + subscribe, normalizeProps, VanillaMachine, Component, diff --git a/priv/static/chunk-NYISECP7.mjs b/priv/static/chunk-NYISECP7.mjs deleted file mode 100644 index a10f7da..0000000 --- a/priv/static/chunk-NYISECP7.mjs +++ /dev/null @@ -1,1075 +0,0 @@ -import { - hasProp, - isEqual, - isObject -} from "./chunk-ZTKS6RKJ.mjs"; - -// ../node_modules/.pnpm/@zag-js+collection@1.33.1/node_modules/@zag-js/collection/dist/index.mjs -var __defProp = Object.defineProperty; -var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; -var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); -var fallback = { - itemToValue(item) { - if (typeof item === "string") return item; - if (isObject(item) && hasProp(item, "value")) return item.value; - return ""; - }, - itemToString(item) { - if (typeof item === "string") return item; - if (isObject(item) && hasProp(item, "label")) return item.label; - return fallback.itemToValue(item); - }, - isItemDisabled(item) { - if (isObject(item) && hasProp(item, "disabled")) return !!item.disabled; - return false; - } -}; -var ListCollection = class _ListCollection { - constructor(options) { - this.options = options; - __publicField(this, "items"); - __publicField(this, "indexMap", null); - __publicField(this, "copy", (items) => { - return new _ListCollection({ ...this.options, items: items ?? [...this.items] }); - }); - __publicField(this, "isEqual", (other) => { - return isEqual(this.items, other.items); - }); - __publicField(this, "setItems", (items) => { - return this.copy(items); - }); - __publicField(this, "getValues", (items = this.items) => { - const values = []; - for (const item of items) { - const value = this.getItemValue(item); - if (value != null) values.push(value); - } - return values; - }); - __publicField(this, "find", (value) => { - if (value == null) return null; - const index = this.indexOf(value); - return index !== -1 ? this.at(index) : null; - }); - __publicField(this, "findMany", (values) => { - const result = []; - for (const value of values) { - const item = this.find(value); - if (item != null) result.push(item); - } - return result; - }); - __publicField(this, "at", (index) => { - if (!this.options.groupBy && !this.options.groupSort) { - return this.items[index] ?? null; - } - let idx = 0; - const groups = this.group(); - for (const [, items] of groups) { - for (const item of items) { - if (idx === index) return item; - idx++; - } - } - return null; - }); - __publicField(this, "sortFn", (valueA, valueB) => { - const indexA = this.indexOf(valueA); - const indexB = this.indexOf(valueB); - return (indexA ?? 0) - (indexB ?? 0); - }); - __publicField(this, "sort", (values) => { - return [...values].sort(this.sortFn.bind(this)); - }); - __publicField(this, "getItemValue", (item) => { - if (item == null) return null; - return this.options.itemToValue?.(item) ?? fallback.itemToValue(item); - }); - __publicField(this, "getItemDisabled", (item) => { - if (item == null) return false; - return this.options.isItemDisabled?.(item) ?? fallback.isItemDisabled(item); - }); - __publicField(this, "stringifyItem", (item) => { - if (item == null) return null; - return this.options.itemToString?.(item) ?? fallback.itemToString(item); - }); - __publicField(this, "stringify", (value) => { - if (value == null) return null; - return this.stringifyItem(this.find(value)); - }); - __publicField(this, "stringifyItems", (items, separator = ", ") => { - const strs = []; - for (const item of items) { - const str = this.stringifyItem(item); - if (str != null) strs.push(str); - } - return strs.join(separator); - }); - __publicField(this, "stringifyMany", (value, separator) => { - return this.stringifyItems(this.findMany(value), separator); - }); - __publicField(this, "has", (value) => { - return this.indexOf(value) !== -1; - }); - __publicField(this, "hasItem", (item) => { - if (item == null) return false; - return this.has(this.getItemValue(item)); - }); - __publicField(this, "group", () => { - const { groupBy, groupSort } = this.options; - if (!groupBy) return [["", [...this.items]]]; - const groups = /* @__PURE__ */ new Map(); - this.items.forEach((item, index) => { - const groupKey = groupBy(item, index); - if (!groups.has(groupKey)) { - groups.set(groupKey, []); - } - groups.get(groupKey).push(item); - }); - let entries = Array.from(groups.entries()); - if (groupSort) { - entries.sort(([a], [b]) => { - if (typeof groupSort === "function") return groupSort(a, b); - if (Array.isArray(groupSort)) { - const indexA = groupSort.indexOf(a); - const indexB = groupSort.indexOf(b); - if (indexA === -1) return 1; - if (indexB === -1) return -1; - return indexA - indexB; - } - if (groupSort === "asc") return a.localeCompare(b); - if (groupSort === "desc") return b.localeCompare(a); - return 0; - }); - } - return entries; - }); - __publicField(this, "getNextValue", (value, step = 1, clamp = false) => { - let index = this.indexOf(value); - if (index === -1) return null; - index = clamp ? Math.min(index + step, this.size - 1) : index + step; - while (index <= this.size && this.getItemDisabled(this.at(index))) index++; - return this.getItemValue(this.at(index)); - }); - __publicField(this, "getPreviousValue", (value, step = 1, clamp = false) => { - let index = this.indexOf(value); - if (index === -1) return null; - index = clamp ? Math.max(index - step, 0) : index - step; - while (index >= 0 && this.getItemDisabled(this.at(index))) index--; - return this.getItemValue(this.at(index)); - }); - __publicField(this, "indexOf", (value) => { - if (value == null) return -1; - if (!this.options.groupBy && !this.options.groupSort) { - return this.items.findIndex((item) => this.getItemValue(item) === value); - } - if (!this.indexMap) { - this.indexMap = /* @__PURE__ */ new Map(); - let idx = 0; - const groups = this.group(); - for (const [, items] of groups) { - for (const item of items) { - const itemValue = this.getItemValue(item); - if (itemValue != null) { - this.indexMap.set(itemValue, idx); - } - idx++; - } - } - } - return this.indexMap.get(value) ?? -1; - }); - __publicField(this, "getByText", (text, current) => { - const currentIndex = current != null ? this.indexOf(current) : -1; - const isSingleKey = text.length === 1; - for (let i = 0; i < this.items.length; i++) { - const item = this.items[(currentIndex + i + 1) % this.items.length]; - if (isSingleKey && this.getItemValue(item) === current) continue; - if (this.getItemDisabled(item)) continue; - if (match(this.stringifyItem(item), text)) return item; - } - return void 0; - }); - __publicField(this, "search", (queryString, options2) => { - const { state, currentValue, timeout = 350 } = options2; - const search = state.keysSoFar + queryString; - const isRepeated = search.length > 1 && Array.from(search).every((char) => char === search[0]); - const query = isRepeated ? search[0] : search; - const item = this.getByText(query, currentValue); - const value = this.getItemValue(item); - function cleanup() { - clearTimeout(state.timer); - state.timer = -1; - } - function update(value2) { - state.keysSoFar = value2; - cleanup(); - if (value2 !== "") { - state.timer = +setTimeout(() => { - update(""); - cleanup(); - }, timeout); - } - } - update(search); - return value; - }); - __publicField(this, "update", (value, item) => { - let index = this.indexOf(value); - if (index === -1) return this; - return this.copy([...this.items.slice(0, index), item, ...this.items.slice(index + 1)]); - }); - __publicField(this, "upsert", (value, item, mode = "append") => { - let index = this.indexOf(value); - if (index === -1) { - const fn = mode === "append" ? this.append : this.prepend; - return fn(item); - } - return this.copy([...this.items.slice(0, index), item, ...this.items.slice(index + 1)]); - }); - __publicField(this, "insert", (index, ...items) => { - return this.copy(insert(this.items, index, ...items)); - }); - __publicField(this, "insertBefore", (value, ...items) => { - let toIndex = this.indexOf(value); - if (toIndex === -1) { - if (this.items.length === 0) toIndex = 0; - else return this; - } - return this.copy(insert(this.items, toIndex, ...items)); - }); - __publicField(this, "insertAfter", (value, ...items) => { - let toIndex = this.indexOf(value); - if (toIndex === -1) { - if (this.items.length === 0) toIndex = 0; - else return this; - } - return this.copy(insert(this.items, toIndex + 1, ...items)); - }); - __publicField(this, "prepend", (...items) => { - return this.copy(insert(this.items, 0, ...items)); - }); - __publicField(this, "append", (...items) => { - return this.copy(insert(this.items, this.items.length, ...items)); - }); - __publicField(this, "filter", (fn) => { - const filteredItems = this.items.filter((item, index) => fn(this.stringifyItem(item), index, item)); - return this.copy(filteredItems); - }); - __publicField(this, "remove", (...itemsOrValues) => { - const values = itemsOrValues.map( - (itemOrValue) => typeof itemOrValue === "string" ? itemOrValue : this.getItemValue(itemOrValue) - ); - return this.copy( - this.items.filter((item) => { - const value = this.getItemValue(item); - if (value == null) return false; - return !values.includes(value); - }) - ); - }); - __publicField(this, "move", (value, toIndex) => { - const fromIndex = this.indexOf(value); - if (fromIndex === -1) return this; - return this.copy(move(this.items, [fromIndex], toIndex)); - }); - __publicField(this, "moveBefore", (value, ...values) => { - let toIndex = this.items.findIndex((item) => this.getItemValue(item) === value); - if (toIndex === -1) return this; - let indices = values.map((value2) => this.items.findIndex((item) => this.getItemValue(item) === value2)).sort((a, b) => a - b); - return this.copy(move(this.items, indices, toIndex)); - }); - __publicField(this, "moveAfter", (value, ...values) => { - let toIndex = this.items.findIndex((item) => this.getItemValue(item) === value); - if (toIndex === -1) return this; - let indices = values.map((value2) => this.items.findIndex((item) => this.getItemValue(item) === value2)).sort((a, b) => a - b); - return this.copy(move(this.items, indices, toIndex + 1)); - }); - __publicField(this, "reorder", (fromIndex, toIndex) => { - return this.copy(move(this.items, [fromIndex], toIndex)); - }); - __publicField(this, "compareValue", (a, b) => { - const indexA = this.indexOf(a); - const indexB = this.indexOf(b); - if (indexA < indexB) return -1; - if (indexA > indexB) return 1; - return 0; - }); - __publicField(this, "range", (from, to) => { - let keys = []; - let key = from; - while (key != null) { - let item = this.find(key); - if (item) keys.push(key); - if (key === to) return keys; - key = this.getNextValue(key); - } - return []; - }); - __publicField(this, "getValueRange", (from, to) => { - if (from && to) { - if (this.compareValue(from, to) <= 0) { - return this.range(from, to); - } - return this.range(to, from); - } - return []; - }); - __publicField(this, "toString", () => { - let result = ""; - for (const item of this.items) { - const value = this.getItemValue(item); - const label = this.stringifyItem(item); - const disabled = this.getItemDisabled(item); - const itemString = [value, label, disabled].filter(Boolean).join(":"); - result += itemString + ","; - } - return result; - }); - __publicField(this, "toJSON", () => { - return { - size: this.size, - first: this.firstValue, - last: this.lastValue - }; - }); - this.items = [...options.items]; - } - /** - * Returns the number of items in the collection - */ - get size() { - return this.items.length; - } - /** - * Returns the first value in the collection - */ - get firstValue() { - let index = 0; - while (this.getItemDisabled(this.at(index))) index++; - return this.getItemValue(this.at(index)); - } - /** - * Returns the last value in the collection - */ - get lastValue() { - let index = this.size - 1; - while (this.getItemDisabled(this.at(index))) index--; - return this.getItemValue(this.at(index)); - } - *[Symbol.iterator]() { - yield* this.items; - } -}; -var match = (label, query) => { - return !!label?.toLowerCase().startsWith(query.toLowerCase()); -}; -function insert(items, index, ...values) { - return [...items.slice(0, index), ...values, ...items.slice(index)]; -} -function move(items, indices, toIndex) { - indices = [...indices].sort((a, b) => a - b); - const itemsToMove = indices.map((i) => items[i]); - for (let i = indices.length - 1; i >= 0; i--) { - items = [...items.slice(0, indices[i]), ...items.slice(indices[i] + 1)]; - } - toIndex = Math.max(0, toIndex - indices.filter((i) => i < toIndex).length); - return [...items.slice(0, toIndex), ...itemsToMove, ...items.slice(toIndex)]; -} -function access(node, indexPath, options) { - for (let i = 0; i < indexPath.length; i++) node = options.getChildren(node, indexPath.slice(i + 1))[indexPath[i]]; - return node; -} -function ancestorIndexPaths(indexPaths) { - const sortedPaths = sortIndexPaths(indexPaths); - const result = []; - const seen = /* @__PURE__ */ new Set(); - for (const indexPath of sortedPaths) { - const key = indexPath.join(); - if (!seen.has(key)) { - seen.add(key); - result.push(indexPath); - } - } - return result; -} -function compareIndexPaths(a, b) { - for (let i = 0; i < Math.min(a.length, b.length); i++) { - if (a[i] < b[i]) return -1; - if (a[i] > b[i]) return 1; - } - return a.length - b.length; -} -function sortIndexPaths(indexPaths) { - return indexPaths.sort(compareIndexPaths); -} -function find(node, options) { - let found; - visit(node, { - ...options, - onEnter: (child, indexPath) => { - if (options.predicate(child, indexPath)) { - found = child; - return "stop"; - } - } - }); - return found; -} -function findAll(node, options) { - const found = []; - visit(node, { - onEnter: (child, indexPath) => { - if (options.predicate(child, indexPath)) found.push(child); - }, - getChildren: options.getChildren - }); - return found; -} -function findIndexPath(node, options) { - let found; - visit(node, { - onEnter: (child, indexPath) => { - if (options.predicate(child, indexPath)) { - found = [...indexPath]; - return "stop"; - } - }, - getChildren: options.getChildren - }); - return found; -} -function reduce(node, options) { - let result = options.initialResult; - visit(node, { - ...options, - onEnter: (child, indexPath) => { - result = options.nextResult(result, child, indexPath); - } - }); - return result; -} -function flatMap(node, options) { - return reduce(node, { - ...options, - initialResult: [], - nextResult: (result, child, indexPath) => { - result.push(...options.transform(child, indexPath)); - return result; - } - }); -} -function filter(node, options) { - const { predicate, create, getChildren } = options; - const filterRecursive = (node2, indexPath) => { - const children = getChildren(node2, indexPath); - const filteredChildren = []; - children.forEach((child, index) => { - const childIndexPath = [...indexPath, index]; - const filteredChild = filterRecursive(child, childIndexPath); - if (filteredChild) filteredChildren.push(filteredChild); - }); - const isRoot = indexPath.length === 0; - const nodeMatches = predicate(node2, indexPath); - const hasFilteredChildren = filteredChildren.length > 0; - if (isRoot || nodeMatches || hasFilteredChildren) { - return create(node2, filteredChildren, indexPath); - } - return null; - }; - return filterRecursive(node, []) || create(node, [], []); -} -function flatten(rootNode, options) { - const nodes = []; - let idx = 0; - const idxMap = /* @__PURE__ */ new Map(); - const parentMap = /* @__PURE__ */ new Map(); - visit(rootNode, { - getChildren: options.getChildren, - onEnter: (node, indexPath) => { - if (!idxMap.has(node)) { - idxMap.set(node, idx++); - } - const children = options.getChildren(node, indexPath); - children.forEach((child) => { - if (!parentMap.has(child)) { - parentMap.set(child, node); - } - if (!idxMap.has(child)) { - idxMap.set(child, idx++); - } - }); - const _children = children.length > 0 ? children.map((child) => idxMap.get(child)) : void 0; - const parent = parentMap.get(node); - const _parent = parent ? idxMap.get(parent) : void 0; - const _index = idxMap.get(node); - nodes.push({ ...node, _children, _parent, _index }); - } - }); - return nodes; -} -function insertOperation(index, nodes) { - return { type: "insert", index, nodes }; -} -function removeOperation(indexes) { - return { type: "remove", indexes }; -} -function replaceOperation() { - return { type: "replace" }; -} -function splitIndexPath(indexPath) { - return [indexPath.slice(0, -1), indexPath[indexPath.length - 1]]; -} -function getInsertionOperations(indexPath, nodes, operations = /* @__PURE__ */ new Map()) { - const [parentIndexPath, index] = splitIndexPath(indexPath); - for (let i = parentIndexPath.length - 1; i >= 0; i--) { - const parentKey = parentIndexPath.slice(0, i).join(); - switch (operations.get(parentKey)?.type) { - case "remove": - continue; - } - operations.set(parentKey, replaceOperation()); - } - const operation = operations.get(parentIndexPath.join()); - switch (operation?.type) { - case "remove": - operations.set(parentIndexPath.join(), { - type: "removeThenInsert", - removeIndexes: operation.indexes, - insertIndex: index, - insertNodes: nodes - }); - break; - default: - operations.set(parentIndexPath.join(), insertOperation(index, nodes)); - } - return operations; -} -function getRemovalOperations(indexPaths) { - const operations = /* @__PURE__ */ new Map(); - const indexesToRemove = /* @__PURE__ */ new Map(); - for (const indexPath of indexPaths) { - const parentKey = indexPath.slice(0, -1).join(); - const value = indexesToRemove.get(parentKey) ?? []; - value.push(indexPath[indexPath.length - 1]); - indexesToRemove.set( - parentKey, - value.sort((a, b) => a - b) - ); - } - for (const indexPath of indexPaths) { - for (let i = indexPath.length - 2; i >= 0; i--) { - const parentKey = indexPath.slice(0, i).join(); - if (!operations.has(parentKey)) { - operations.set(parentKey, replaceOperation()); - } - } - } - for (const [parentKey, indexes] of indexesToRemove) { - operations.set(parentKey, removeOperation(indexes)); - } - return operations; -} -function getReplaceOperations(indexPath, node) { - const operations = /* @__PURE__ */ new Map(); - const [parentIndexPath, index] = splitIndexPath(indexPath); - for (let i = parentIndexPath.length - 1; i >= 0; i--) { - const parentKey = parentIndexPath.slice(0, i).join(); - operations.set(parentKey, replaceOperation()); - } - operations.set(parentIndexPath.join(), { - type: "removeThenInsert", - removeIndexes: [index], - insertIndex: index, - insertNodes: [node] - }); - return operations; -} -function mutate(node, operations, options) { - return map(node, { - ...options, - getChildren: (node2, indexPath) => { - const key = indexPath.join(); - const operation = operations.get(key); - switch (operation?.type) { - case "replace": - case "remove": - case "removeThenInsert": - case "insert": - return options.getChildren(node2, indexPath); - default: - return []; - } - }, - transform: (node2, children, indexPath) => { - const key = indexPath.join(); - const operation = operations.get(key); - switch (operation?.type) { - case "remove": - return options.create( - node2, - children.filter((_, index) => !operation.indexes.includes(index)), - indexPath - ); - case "removeThenInsert": - const updatedChildren = children.filter((_, index) => !operation.removeIndexes.includes(index)); - const adjustedIndex = operation.removeIndexes.reduce( - (index, removedIndex) => removedIndex < index ? index - 1 : index, - operation.insertIndex - ); - return options.create(node2, splice(updatedChildren, adjustedIndex, 0, ...operation.insertNodes), indexPath); - case "insert": - return options.create(node2, splice(children, operation.index, 0, ...operation.nodes), indexPath); - case "replace": - return options.create(node2, children, indexPath); - default: - return node2; - } - } - }); -} -function splice(array, start, deleteCount, ...items) { - return [...array.slice(0, start), ...items, ...array.slice(start + deleteCount)]; -} -function map(node, options) { - const childrenMap = {}; - visit(node, { - ...options, - onLeave: (child, indexPath) => { - const keyIndexPath = [0, ...indexPath]; - const key = keyIndexPath.join(); - const transformed = options.transform(child, childrenMap[key] ?? [], indexPath); - const parentKey = keyIndexPath.slice(0, -1).join(); - const parentChildren = childrenMap[parentKey] ?? []; - parentChildren.push(transformed); - childrenMap[parentKey] = parentChildren; - } - }); - return childrenMap[""][0]; -} -function insert2(node, options) { - const { nodes, at } = options; - if (at.length === 0) throw new Error(`Can't insert nodes at the root`); - const state = getInsertionOperations(at, nodes); - return mutate(node, state, options); -} -function replace(node, options) { - if (options.at.length === 0) return options.node; - const operations = getReplaceOperations(options.at, options.node); - return mutate(node, operations, options); -} -function remove(node, options) { - if (options.indexPaths.length === 0) return node; - for (const indexPath of options.indexPaths) { - if (indexPath.length === 0) throw new Error(`Can't remove the root node`); - } - const operations = getRemovalOperations(options.indexPaths); - return mutate(node, operations, options); -} -function move2(node, options) { - if (options.indexPaths.length === 0) return node; - for (const indexPath of options.indexPaths) { - if (indexPath.length === 0) throw new Error(`Can't move the root node`); - } - if (options.to.length === 0) throw new Error(`Can't move nodes to the root`); - const _ancestorIndexPaths = ancestorIndexPaths(options.indexPaths); - const nodesToInsert = _ancestorIndexPaths.map((indexPath) => access(node, indexPath, options)); - const operations = getInsertionOperations(options.to, nodesToInsert, getRemovalOperations(_ancestorIndexPaths)); - return mutate(node, operations, options); -} -function visit(node, options) { - const { onEnter, onLeave, getChildren } = options; - let indexPath = []; - let stack = [{ node }]; - const getIndexPath = options.reuseIndexPath ? () => indexPath : () => indexPath.slice(); - while (stack.length > 0) { - let wrapper = stack[stack.length - 1]; - if (wrapper.state === void 0) { - const enterResult = onEnter?.(wrapper.node, getIndexPath()); - if (enterResult === "stop") return; - wrapper.state = enterResult === "skip" ? -1 : 0; - } - const children = wrapper.children || getChildren(wrapper.node, getIndexPath()); - wrapper.children || (wrapper.children = children); - if (wrapper.state !== -1) { - if (wrapper.state < children.length) { - let currentIndex = wrapper.state; - indexPath.push(currentIndex); - stack.push({ node: children[currentIndex] }); - wrapper.state = currentIndex + 1; - continue; - } - const leaveResult = onLeave?.(wrapper.node, getIndexPath()); - if (leaveResult === "stop") return; - } - indexPath.pop(); - stack.pop(); - } -} -var TreeCollection = class _TreeCollection { - constructor(options) { - this.options = options; - __publicField(this, "rootNode"); - __publicField(this, "isEqual", (other) => { - return isEqual(this.rootNode, other.rootNode); - }); - __publicField(this, "getNodeChildren", (node) => { - return this.options.nodeToChildren?.(node) ?? fallbackMethods.nodeToChildren(node) ?? []; - }); - __publicField(this, "resolveIndexPath", (valueOrIndexPath) => { - return typeof valueOrIndexPath === "string" ? this.getIndexPath(valueOrIndexPath) : valueOrIndexPath; - }); - __publicField(this, "resolveNode", (valueOrIndexPath) => { - const indexPath = this.resolveIndexPath(valueOrIndexPath); - return indexPath ? this.at(indexPath) : void 0; - }); - __publicField(this, "getNodeChildrenCount", (node) => { - return this.options.nodeToChildrenCount?.(node) ?? fallbackMethods.nodeToChildrenCount(node); - }); - __publicField(this, "getNodeValue", (node) => { - return this.options.nodeToValue?.(node) ?? fallbackMethods.nodeToValue(node); - }); - __publicField(this, "getNodeDisabled", (node) => { - return this.options.isNodeDisabled?.(node) ?? fallbackMethods.isNodeDisabled(node); - }); - __publicField(this, "stringify", (value) => { - const node = this.findNode(value); - if (!node) return null; - return this.stringifyNode(node); - }); - __publicField(this, "stringifyNode", (node) => { - return this.options.nodeToString?.(node) ?? fallbackMethods.nodeToString(node); - }); - __publicField(this, "getFirstNode", (rootNode = this.rootNode, opts = {}) => { - let firstChild; - visit(rootNode, { - getChildren: this.getNodeChildren, - onEnter: (node, indexPath) => { - if (this.isSameNode(node, rootNode)) return; - if (opts.skip?.({ value: this.getNodeValue(node), node, indexPath })) return "skip"; - if (!firstChild && indexPath.length > 0 && !this.getNodeDisabled(node)) { - firstChild = node; - return "stop"; - } - } - }); - return firstChild; - }); - __publicField(this, "getLastNode", (rootNode = this.rootNode, opts = {}) => { - let lastChild; - visit(rootNode, { - getChildren: this.getNodeChildren, - onEnter: (node, indexPath) => { - if (this.isSameNode(node, rootNode)) return; - if (opts.skip?.({ value: this.getNodeValue(node), node, indexPath })) return "skip"; - if (indexPath.length > 0 && !this.getNodeDisabled(node)) { - lastChild = node; - } - } - }); - return lastChild; - }); - __publicField(this, "at", (indexPath) => { - return access(this.rootNode, indexPath, { - getChildren: this.getNodeChildren - }); - }); - __publicField(this, "findNode", (value, rootNode = this.rootNode) => { - return find(rootNode, { - getChildren: this.getNodeChildren, - predicate: (node) => this.getNodeValue(node) === value - }); - }); - __publicField(this, "findNodes", (values, rootNode = this.rootNode) => { - const v = new Set(values.filter((v2) => v2 != null)); - return findAll(rootNode, { - getChildren: this.getNodeChildren, - predicate: (node) => v.has(this.getNodeValue(node)) - }); - }); - __publicField(this, "sort", (values) => { - return values.reduce((acc, value) => { - const indexPath = this.getIndexPath(value); - if (indexPath) acc.push({ value, indexPath }); - return acc; - }, []).sort((a, b) => compareIndexPaths(a.indexPath, b.indexPath)).map(({ value }) => value); - }); - __publicField(this, "getIndexPath", (value) => { - return findIndexPath(this.rootNode, { - getChildren: this.getNodeChildren, - predicate: (node) => this.getNodeValue(node) === value - }); - }); - __publicField(this, "getValue", (indexPath) => { - const node = this.at(indexPath); - return node ? this.getNodeValue(node) : void 0; - }); - __publicField(this, "getValuePath", (indexPath) => { - if (!indexPath) return []; - const valuePath = []; - let currentPath = [...indexPath]; - while (currentPath.length > 0) { - const node = this.at(currentPath); - if (node) valuePath.unshift(this.getNodeValue(node)); - currentPath.pop(); - } - return valuePath; - }); - __publicField(this, "getDepth", (value) => { - const indexPath = findIndexPath(this.rootNode, { - getChildren: this.getNodeChildren, - predicate: (node) => this.getNodeValue(node) === value - }); - return indexPath?.length ?? 0; - }); - __publicField(this, "isSameNode", (node, other) => { - return this.getNodeValue(node) === this.getNodeValue(other); - }); - __publicField(this, "isRootNode", (node) => { - return this.isSameNode(node, this.rootNode); - }); - __publicField(this, "contains", (parentIndexPath, valueIndexPath) => { - if (!parentIndexPath || !valueIndexPath) return false; - return valueIndexPath.slice(0, parentIndexPath.length).every((_, i) => parentIndexPath[i] === valueIndexPath[i]); - }); - __publicField(this, "getNextNode", (value, opts = {}) => { - let found = false; - let nextNode; - visit(this.rootNode, { - getChildren: this.getNodeChildren, - onEnter: (node, indexPath) => { - if (this.isRootNode(node)) return; - const nodeValue = this.getNodeValue(node); - if (opts.skip?.({ value: nodeValue, node, indexPath })) { - if (nodeValue === value) { - found = true; - } - return "skip"; - } - if (found && !this.getNodeDisabled(node)) { - nextNode = node; - return "stop"; - } - if (nodeValue === value) { - found = true; - } - } - }); - return nextNode; - }); - __publicField(this, "getPreviousNode", (value, opts = {}) => { - let previousNode; - let found = false; - visit(this.rootNode, { - getChildren: this.getNodeChildren, - onEnter: (node, indexPath) => { - if (this.isRootNode(node)) return; - const nodeValue = this.getNodeValue(node); - if (opts.skip?.({ value: nodeValue, node, indexPath })) { - return "skip"; - } - if (nodeValue === value) { - found = true; - return "stop"; - } - if (!this.getNodeDisabled(node)) { - previousNode = node; - } - } - }); - return found ? previousNode : void 0; - }); - __publicField(this, "getParentNodes", (valueOrIndexPath) => { - const indexPath = this.resolveIndexPath(valueOrIndexPath)?.slice(); - if (!indexPath) return []; - const result = []; - while (indexPath.length > 0) { - indexPath.pop(); - const parentNode = this.at(indexPath); - if (parentNode && !this.isRootNode(parentNode)) { - result.unshift(parentNode); - } - } - return result; - }); - __publicField(this, "getDescendantNodes", (valueOrIndexPath, options2) => { - const parentNode = this.resolveNode(valueOrIndexPath); - if (!parentNode) return []; - const result = []; - visit(parentNode, { - getChildren: this.getNodeChildren, - onEnter: (node, nodeIndexPath) => { - if (nodeIndexPath.length === 0) return; - if (!options2?.withBranch && this.isBranchNode(node)) return; - result.push(node); - } - }); - return result; - }); - __publicField(this, "getDescendantValues", (valueOrIndexPath, options2) => { - const children = this.getDescendantNodes(valueOrIndexPath, options2); - return children.map((child) => this.getNodeValue(child)); - }); - __publicField(this, "getParentIndexPath", (indexPath) => { - return indexPath.slice(0, -1); - }); - __publicField(this, "getParentNode", (valueOrIndexPath) => { - const indexPath = this.resolveIndexPath(valueOrIndexPath); - return indexPath ? this.at(this.getParentIndexPath(indexPath)) : void 0; - }); - __publicField(this, "visit", (opts) => { - const { skip, ...rest } = opts; - visit(this.rootNode, { - ...rest, - getChildren: this.getNodeChildren, - onEnter: (node, indexPath) => { - if (this.isRootNode(node)) return; - if (skip?.({ value: this.getNodeValue(node), node, indexPath })) return "skip"; - return rest.onEnter?.(node, indexPath); - } - }); - }); - __publicField(this, "getPreviousSibling", (indexPath) => { - const parentNode = this.getParentNode(indexPath); - if (!parentNode) return; - const siblings = this.getNodeChildren(parentNode); - let idx = indexPath[indexPath.length - 1]; - while (--idx >= 0) { - const sibling = siblings[idx]; - if (!this.getNodeDisabled(sibling)) return sibling; - } - return; - }); - __publicField(this, "getNextSibling", (indexPath) => { - const parentNode = this.getParentNode(indexPath); - if (!parentNode) return; - const siblings = this.getNodeChildren(parentNode); - let idx = indexPath[indexPath.length - 1]; - while (++idx < siblings.length) { - const sibling = siblings[idx]; - if (!this.getNodeDisabled(sibling)) return sibling; - } - return; - }); - __publicField(this, "getSiblingNodes", (indexPath) => { - const parentNode = this.getParentNode(indexPath); - return parentNode ? this.getNodeChildren(parentNode) : []; - }); - __publicField(this, "getValues", (rootNode = this.rootNode) => { - const values = flatMap(rootNode, { - getChildren: this.getNodeChildren, - transform: (node) => [this.getNodeValue(node)] - }); - return values.slice(1); - }); - __publicField(this, "isValidDepth", (indexPath, depth) => { - if (depth == null) return true; - if (typeof depth === "function") return depth(indexPath.length); - return indexPath.length === depth; - }); - __publicField(this, "isBranchNode", (node) => { - return this.getNodeChildren(node).length > 0 || this.getNodeChildrenCount(node) != null; - }); - __publicField(this, "getBranchValues", (rootNode = this.rootNode, opts = {}) => { - let values = []; - visit(rootNode, { - getChildren: this.getNodeChildren, - onEnter: (node, indexPath) => { - if (indexPath.length === 0) return; - const nodeValue = this.getNodeValue(node); - if (opts.skip?.({ value: nodeValue, node, indexPath })) return "skip"; - if (this.isBranchNode(node) && this.isValidDepth(indexPath, opts.depth)) { - values.push(this.getNodeValue(node)); - } - } - }); - return values; - }); - __publicField(this, "flatten", (rootNode = this.rootNode) => { - return flatten(rootNode, { getChildren: this.getNodeChildren }); - }); - __publicField(this, "_create", (node, children) => { - if (this.getNodeChildren(node).length > 0 || children.length > 0) { - return { ...node, children }; - } - return { ...node }; - }); - __publicField(this, "_insert", (rootNode, indexPath, nodes) => { - return this.copy( - insert2(rootNode, { at: indexPath, nodes, getChildren: this.getNodeChildren, create: this._create }) - ); - }); - __publicField(this, "copy", (rootNode) => { - return new _TreeCollection({ ...this.options, rootNode }); - }); - __publicField(this, "_replace", (rootNode, indexPath, node) => { - return this.copy( - replace(rootNode, { at: indexPath, node, getChildren: this.getNodeChildren, create: this._create }) - ); - }); - __publicField(this, "_move", (rootNode, indexPaths, to) => { - return this.copy(move2(rootNode, { indexPaths, to, getChildren: this.getNodeChildren, create: this._create })); - }); - __publicField(this, "_remove", (rootNode, indexPaths) => { - return this.copy(remove(rootNode, { indexPaths, getChildren: this.getNodeChildren, create: this._create })); - }); - __publicField(this, "replace", (indexPath, node) => { - return this._replace(this.rootNode, indexPath, node); - }); - __publicField(this, "remove", (indexPaths) => { - return this._remove(this.rootNode, indexPaths); - }); - __publicField(this, "insertBefore", (indexPath, nodes) => { - const parentNode = this.getParentNode(indexPath); - return parentNode ? this._insert(this.rootNode, indexPath, nodes) : void 0; - }); - __publicField(this, "insertAfter", (indexPath, nodes) => { - const parentNode = this.getParentNode(indexPath); - if (!parentNode) return; - const nextIndex2 = [...indexPath.slice(0, -1), indexPath[indexPath.length - 1] + 1]; - return this._insert(this.rootNode, nextIndex2, nodes); - }); - __publicField(this, "move", (fromIndexPaths, toIndexPath) => { - return this._move(this.rootNode, fromIndexPaths, toIndexPath); - }); - __publicField(this, "filter", (predicate) => { - const filteredRoot = filter(this.rootNode, { - predicate, - getChildren: this.getNodeChildren, - create: this._create - }); - return this.copy(filteredRoot); - }); - __publicField(this, "toJSON", () => { - return this.getValues(this.rootNode); - }); - this.rootNode = options.rootNode; - } -}; -var fallbackMethods = { - nodeToValue(node) { - if (typeof node === "string") return node; - if (isObject(node) && hasProp(node, "value")) return node.value; - return ""; - }, - nodeToString(node) { - if (typeof node === "string") return node; - if (isObject(node) && hasProp(node, "label")) return node.label; - return fallbackMethods.nodeToValue(node); - }, - isNodeDisabled(node) { - if (isObject(node) && hasProp(node, "disabled")) return !!node.disabled; - return false; - }, - nodeToChildren(node) { - return node.children; - }, - nodeToChildrenCount(node) { - if (isObject(node) && hasProp(node, "childrenCount")) return node.childrenCount; - } -}; - -export { - ListCollection, - TreeCollection -}; diff --git a/priv/static/chunk-GYNMIQP2.mjs b/priv/static/chunk-RR7TJIQ5.mjs similarity index 54% rename from priv/static/chunk-GYNMIQP2.mjs rename to priv/static/chunk-RR7TJIQ5.mjs index 1778365..9cafe34 100644 --- a/priv/static/chunk-GYNMIQP2.mjs +++ b/priv/static/chunk-RR7TJIQ5.mjs @@ -1,241 +1,19 @@ +import { + trackInteractOutside +} from "./chunk-ER3INIAI.mjs"; import { addDomEvent, - callAll, contains, getDocument, getEventTarget, - getNearestOverflowAncestor, - getWindow, - isContextMenuEvent, - isControlledElement, - isFocusable, isFunction, isHTMLElement, - isShadowRoot, - isTouchDevice, nextTick, raf, setStyle, waitForElement, warn -} from "./chunk-ZTKS6RKJ.mjs"; - -// ../node_modules/.pnpm/@zag-js+interact-outside@1.33.1/node_modules/@zag-js/interact-outside/dist/index.mjs -function getWindowFrames(win) { - const frames = { - each(cb) { - for (let i = 0; i < win.frames?.length; i += 1) { - const frame = win.frames[i]; - if (frame) cb(frame); - } - }, - addEventListener(event, listener, options) { - frames.each((frame) => { - try { - frame.document.addEventListener(event, listener, options); - } catch { - } - }); - return () => { - try { - frames.removeEventListener(event, listener, options); - } catch { - } - }; - }, - removeEventListener(event, listener, options) { - frames.each((frame) => { - try { - frame.document.removeEventListener(event, listener, options); - } catch { - } - }); - } - }; - return frames; -} -function getParentWindow(win) { - const parent = win.frameElement != null ? win.parent : null; - return { - addEventListener: (event, listener, options) => { - try { - parent?.addEventListener(event, listener, options); - } catch { - } - return () => { - try { - parent?.removeEventListener(event, listener, options); - } catch { - } - }; - }, - removeEventListener: (event, listener, options) => { - try { - parent?.removeEventListener(event, listener, options); - } catch { - } - } - }; -} -var POINTER_OUTSIDE_EVENT = "pointerdown.outside"; -var FOCUS_OUTSIDE_EVENT = "focus.outside"; -function isComposedPathFocusable(composedPath) { - for (const node of composedPath) { - if (isHTMLElement(node) && isFocusable(node)) return true; - } - return false; -} -var isPointerEvent = (event) => "clientY" in event; -function isEventPointWithin(node, event) { - if (!isPointerEvent(event) || !node) return false; - const rect = node.getBoundingClientRect(); - if (rect.width === 0 || rect.height === 0) return false; - return rect.top <= event.clientY && event.clientY <= rect.top + rect.height && rect.left <= event.clientX && event.clientX <= rect.left + rect.width; -} -function isPointInRect(rect, point) { - return rect.y <= point.y && point.y <= rect.y + rect.height && rect.x <= point.x && point.x <= rect.x + rect.width; -} -function isEventWithinScrollbar(event, ancestor) { - if (!ancestor || !isPointerEvent(event)) return false; - const isScrollableY = ancestor.scrollHeight > ancestor.clientHeight; - const onScrollbarY = isScrollableY && event.clientX > ancestor.offsetLeft + ancestor.clientWidth; - const isScrollableX = ancestor.scrollWidth > ancestor.clientWidth; - const onScrollbarX = isScrollableX && event.clientY > ancestor.offsetTop + ancestor.clientHeight; - const rect = { - x: ancestor.offsetLeft, - y: ancestor.offsetTop, - width: ancestor.clientWidth + (isScrollableY ? 16 : 0), - height: ancestor.clientHeight + (isScrollableX ? 16 : 0) - }; - const point = { - x: event.clientX, - y: event.clientY - }; - if (!isPointInRect(rect, point)) return false; - return onScrollbarY || onScrollbarX; -} -function trackInteractOutsideImpl(node, options) { - const { - exclude, - onFocusOutside, - onPointerDownOutside, - onInteractOutside, - defer, - followControlledElements = true - } = options; - if (!node) return; - const doc = getDocument(node); - const win = getWindow(node); - const frames = getWindowFrames(win); - const parentWin = getParentWindow(win); - function isEventOutside(event, target) { - if (!isHTMLElement(target)) return false; - if (!target.isConnected) return false; - if (contains(node, target)) return false; - if (isEventPointWithin(node, event)) return false; - if (followControlledElements && isControlledElement(node, target)) return false; - const triggerEl = doc.querySelector(`[aria-controls="${node.id}"]`); - if (triggerEl) { - const triggerAncestor = getNearestOverflowAncestor(triggerEl); - if (isEventWithinScrollbar(event, triggerAncestor)) return false; - } - const nodeAncestor = getNearestOverflowAncestor(node); - if (isEventWithinScrollbar(event, nodeAncestor)) return false; - return !exclude?.(target); - } - const pointerdownCleanups = /* @__PURE__ */ new Set(); - const isInShadowRoot = isShadowRoot(node?.getRootNode()); - function onPointerDown(event) { - function handler(clickEvent) { - const func = defer && !isTouchDevice() ? raf : (v) => v(); - const evt = clickEvent ?? event; - const composedPath = evt?.composedPath?.() ?? [evt?.target]; - func(() => { - const target = isInShadowRoot ? composedPath[0] : getEventTarget(event); - if (!node || !isEventOutside(event, target)) return; - if (onPointerDownOutside || onInteractOutside) { - const handler2 = callAll(onPointerDownOutside, onInteractOutside); - node.addEventListener(POINTER_OUTSIDE_EVENT, handler2, { once: true }); - } - fireCustomEvent(node, POINTER_OUTSIDE_EVENT, { - bubbles: false, - cancelable: true, - detail: { - originalEvent: evt, - contextmenu: isContextMenuEvent(evt), - focusable: isComposedPathFocusable(composedPath), - target - } - }); - }); - } - if (event.pointerType === "touch") { - pointerdownCleanups.forEach((fn) => fn()); - pointerdownCleanups.add(addDomEvent(doc, "click", handler, { once: true })); - pointerdownCleanups.add(parentWin.addEventListener("click", handler, { once: true })); - pointerdownCleanups.add(frames.addEventListener("click", handler, { once: true })); - } else { - handler(); - } - } - const cleanups = /* @__PURE__ */ new Set(); - const timer = setTimeout(() => { - cleanups.add(addDomEvent(doc, "pointerdown", onPointerDown, true)); - cleanups.add(parentWin.addEventListener("pointerdown", onPointerDown, true)); - cleanups.add(frames.addEventListener("pointerdown", onPointerDown, true)); - }, 0); - function onFocusin(event) { - const func = defer ? raf : (v) => v(); - func(() => { - const composedPath = event?.composedPath?.() ?? [event?.target]; - const target = isInShadowRoot ? composedPath[0] : getEventTarget(event); - if (!node || !isEventOutside(event, target)) return; - if (onFocusOutside || onInteractOutside) { - const handler = callAll(onFocusOutside, onInteractOutside); - node.addEventListener(FOCUS_OUTSIDE_EVENT, handler, { once: true }); - } - fireCustomEvent(node, FOCUS_OUTSIDE_EVENT, { - bubbles: false, - cancelable: true, - detail: { - originalEvent: event, - contextmenu: false, - focusable: isFocusable(target), - target - } - }); - }); - } - if (!isTouchDevice()) { - cleanups.add(addDomEvent(doc, "focusin", onFocusin, true)); - cleanups.add(parentWin.addEventListener("focusin", onFocusin, true)); - cleanups.add(frames.addEventListener("focusin", onFocusin, true)); - } - return () => { - clearTimeout(timer); - pointerdownCleanups.forEach((fn) => fn()); - cleanups.forEach((fn) => fn()); - }; -} -function trackInteractOutside(nodeOrFn, options) { - const { defer } = options; - const func = defer ? raf : (v) => v(); - const cleanups = []; - cleanups.push( - func(() => { - const node = typeof nodeOrFn === "function" ? nodeOrFn() : nodeOrFn; - cleanups.push(trackInteractOutsideImpl(node, options)); - }) - ); - return () => { - cleanups.forEach((fn) => fn?.()); - }; -} -function fireCustomEvent(el, type, init) { - const win = el.ownerDocument.defaultView || window; - const event = new win.CustomEvent(type, init); - return el.dispatchEvent(event); -} +} from "./chunk-IXOYOLUJ.mjs"; // ../node_modules/.pnpm/@zag-js+dismissable@1.33.1/node_modules/@zag-js/dismissable/dist/index.mjs function trackEscapeKeydown(node, fn) { @@ -352,7 +130,7 @@ var layerStack = { layer?.dismiss(); } }); - fireCustomEvent2(node, LAYER_REQUEST_DISMISS_EVENT, { + fireCustomEvent(node, LAYER_REQUEST_DISMISS_EVENT, { originalLayer: node, targetLayer: parent, originalIndex: index, @@ -364,7 +142,7 @@ var layerStack = { this.remove(this.layers[0].node); } }; -function fireCustomEvent2(el, type, detail) { +function fireCustomEvent(el, type, detail) { const win = el.ownerDocument.defaultView || window; const event = new win.CustomEvent(type, { cancelable: true, bubbles: true, detail }); return el.dispatchEvent(event); diff --git a/priv/static/chunk-T4BCXOJK.mjs b/priv/static/chunk-T4BCXOJK.mjs deleted file mode 100644 index 634578d..0000000 --- a/priv/static/chunk-T4BCXOJK.mjs +++ /dev/null @@ -1,1953 +0,0 @@ -import { - compact, - getComputedStyle as getComputedStyle2, - getWindow, - isHTMLElement, - isNull, - noop, - raf -} from "./chunk-ZTKS6RKJ.mjs"; - -// ../node_modules/.pnpm/@floating-ui+utils@0.2.10/node_modules/@floating-ui/utils/dist/floating-ui.utils.mjs -var sides = ["top", "right", "bottom", "left"]; -var min = Math.min; -var max = Math.max; -var round = Math.round; -var floor = Math.floor; -var createCoords = (v) => ({ - x: v, - y: v -}); -var oppositeSideMap = { - left: "right", - right: "left", - bottom: "top", - top: "bottom" -}; -var oppositeAlignmentMap = { - start: "end", - end: "start" -}; -function clamp(start, value, end) { - return max(start, min(value, end)); -} -function evaluate(value, param) { - return typeof value === "function" ? value(param) : value; -} -function getSide(placement) { - return placement.split("-")[0]; -} -function getAlignment(placement) { - return placement.split("-")[1]; -} -function getOppositeAxis(axis) { - return axis === "x" ? "y" : "x"; -} -function getAxisLength(axis) { - return axis === "y" ? "height" : "width"; -} -var yAxisSides = /* @__PURE__ */ new Set(["top", "bottom"]); -function getSideAxis(placement) { - return yAxisSides.has(getSide(placement)) ? "y" : "x"; -} -function getAlignmentAxis(placement) { - return getOppositeAxis(getSideAxis(placement)); -} -function getAlignmentSides(placement, rects, rtl) { - if (rtl === void 0) { - rtl = false; - } - const alignment = getAlignment(placement); - const alignmentAxis = getAlignmentAxis(placement); - const length = getAxisLength(alignmentAxis); - let mainAlignmentSide = alignmentAxis === "x" ? alignment === (rtl ? "end" : "start") ? "right" : "left" : alignment === "start" ? "bottom" : "top"; - if (rects.reference[length] > rects.floating[length]) { - mainAlignmentSide = getOppositePlacement(mainAlignmentSide); - } - return [mainAlignmentSide, getOppositePlacement(mainAlignmentSide)]; -} -function getExpandedPlacements(placement) { - const oppositePlacement = getOppositePlacement(placement); - return [getOppositeAlignmentPlacement(placement), oppositePlacement, getOppositeAlignmentPlacement(oppositePlacement)]; -} -function getOppositeAlignmentPlacement(placement) { - return placement.replace(/start|end/g, (alignment) => oppositeAlignmentMap[alignment]); -} -var lrPlacement = ["left", "right"]; -var rlPlacement = ["right", "left"]; -var tbPlacement = ["top", "bottom"]; -var btPlacement = ["bottom", "top"]; -function getSideList(side, isStart, rtl) { - switch (side) { - case "top": - case "bottom": - if (rtl) return isStart ? rlPlacement : lrPlacement; - return isStart ? lrPlacement : rlPlacement; - case "left": - case "right": - return isStart ? tbPlacement : btPlacement; - default: - return []; - } -} -function getOppositeAxisPlacements(placement, flipAlignment, direction, rtl) { - const alignment = getAlignment(placement); - let list = getSideList(getSide(placement), direction === "start", rtl); - if (alignment) { - list = list.map((side) => side + "-" + alignment); - if (flipAlignment) { - list = list.concat(list.map(getOppositeAlignmentPlacement)); - } - } - return list; -} -function getOppositePlacement(placement) { - return placement.replace(/left|right|bottom|top/g, (side) => oppositeSideMap[side]); -} -function expandPaddingObject(padding) { - return { - top: 0, - right: 0, - bottom: 0, - left: 0, - ...padding - }; -} -function getPaddingObject(padding) { - return typeof padding !== "number" ? expandPaddingObject(padding) : { - top: padding, - right: padding, - bottom: padding, - left: padding - }; -} -function rectToClientRect(rect) { - const { - x, - y, - width, - height - } = rect; - return { - width, - height, - top: y, - left: x, - right: x + width, - bottom: y + height, - x, - y - }; -} - -// ../node_modules/.pnpm/@floating-ui+core@1.7.4/node_modules/@floating-ui/core/dist/floating-ui.core.mjs -function computeCoordsFromPlacement(_ref, placement, rtl) { - let { - reference, - floating - } = _ref; - const sideAxis = getSideAxis(placement); - const alignmentAxis = getAlignmentAxis(placement); - const alignLength = getAxisLength(alignmentAxis); - const side = getSide(placement); - const isVertical = sideAxis === "y"; - const commonX = reference.x + reference.width / 2 - floating.width / 2; - const commonY = reference.y + reference.height / 2 - floating.height / 2; - const commonAlign = reference[alignLength] / 2 - floating[alignLength] / 2; - let coords; - switch (side) { - case "top": - coords = { - x: commonX, - y: reference.y - floating.height - }; - break; - case "bottom": - coords = { - x: commonX, - y: reference.y + reference.height - }; - break; - case "right": - coords = { - x: reference.x + reference.width, - y: commonY - }; - break; - case "left": - coords = { - x: reference.x - floating.width, - y: commonY - }; - break; - default: - coords = { - x: reference.x, - y: reference.y - }; - } - switch (getAlignment(placement)) { - case "start": - coords[alignmentAxis] -= commonAlign * (rtl && isVertical ? -1 : 1); - break; - case "end": - coords[alignmentAxis] += commonAlign * (rtl && isVertical ? -1 : 1); - break; - } - return coords; -} -async function detectOverflow(state, options) { - var _await$platform$isEle; - if (options === void 0) { - options = {}; - } - const { - x, - y, - platform: platform2, - rects, - elements, - strategy - } = state; - const { - boundary = "clippingAncestors", - rootBoundary = "viewport", - elementContext = "floating", - altBoundary = false, - padding = 0 - } = evaluate(options, state); - const paddingObject = getPaddingObject(padding); - const altContext = elementContext === "floating" ? "reference" : "floating"; - const element = elements[altBoundary ? altContext : elementContext]; - const clippingClientRect = rectToClientRect(await platform2.getClippingRect({ - element: ((_await$platform$isEle = await (platform2.isElement == null ? void 0 : platform2.isElement(element))) != null ? _await$platform$isEle : true) ? element : element.contextElement || await (platform2.getDocumentElement == null ? void 0 : platform2.getDocumentElement(elements.floating)), - boundary, - rootBoundary, - strategy - })); - const rect = elementContext === "floating" ? { - x, - y, - width: rects.floating.width, - height: rects.floating.height - } : rects.reference; - const offsetParent = await (platform2.getOffsetParent == null ? void 0 : platform2.getOffsetParent(elements.floating)); - const offsetScale = await (platform2.isElement == null ? void 0 : platform2.isElement(offsetParent)) ? await (platform2.getScale == null ? void 0 : platform2.getScale(offsetParent)) || { - x: 1, - y: 1 - } : { - x: 1, - y: 1 - }; - const elementClientRect = rectToClientRect(platform2.convertOffsetParentRelativeRectToViewportRelativeRect ? await platform2.convertOffsetParentRelativeRectToViewportRelativeRect({ - elements, - rect, - offsetParent, - strategy - }) : rect); - return { - top: (clippingClientRect.top - elementClientRect.top + paddingObject.top) / offsetScale.y, - bottom: (elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom) / offsetScale.y, - left: (clippingClientRect.left - elementClientRect.left + paddingObject.left) / offsetScale.x, - right: (elementClientRect.right - clippingClientRect.right + paddingObject.right) / offsetScale.x - }; -} -var computePosition = async (reference, floating, config) => { - const { - placement = "bottom", - strategy = "absolute", - middleware = [], - platform: platform2 - } = config; - const validMiddleware = middleware.filter(Boolean); - const rtl = await (platform2.isRTL == null ? void 0 : platform2.isRTL(floating)); - let rects = await platform2.getElementRects({ - reference, - floating, - strategy - }); - let { - x, - y - } = computeCoordsFromPlacement(rects, placement, rtl); - let statefulPlacement = placement; - let middlewareData = {}; - let resetCount = 0; - for (let i = 0; i < validMiddleware.length; i++) { - var _platform$detectOverf; - const { - name, - fn - } = validMiddleware[i]; - const { - x: nextX, - y: nextY, - data, - reset - } = await fn({ - x, - y, - initialPlacement: placement, - placement: statefulPlacement, - strategy, - middlewareData, - rects, - platform: { - ...platform2, - detectOverflow: (_platform$detectOverf = platform2.detectOverflow) != null ? _platform$detectOverf : detectOverflow - }, - elements: { - reference, - floating - } - }); - x = nextX != null ? nextX : x; - y = nextY != null ? nextY : y; - middlewareData = { - ...middlewareData, - [name]: { - ...middlewareData[name], - ...data - } - }; - if (reset && resetCount <= 50) { - resetCount++; - if (typeof reset === "object") { - if (reset.placement) { - statefulPlacement = reset.placement; - } - if (reset.rects) { - rects = reset.rects === true ? await platform2.getElementRects({ - reference, - floating, - strategy - }) : reset.rects; - } - ({ - x, - y - } = computeCoordsFromPlacement(rects, statefulPlacement, rtl)); - } - i = -1; - } - } - return { - x, - y, - placement: statefulPlacement, - strategy, - middlewareData - }; -}; -var arrow = (options) => ({ - name: "arrow", - options, - async fn(state) { - const { - x, - y, - placement, - rects, - platform: platform2, - elements, - middlewareData - } = state; - const { - element, - padding = 0 - } = evaluate(options, state) || {}; - if (element == null) { - return {}; - } - const paddingObject = getPaddingObject(padding); - const coords = { - x, - y - }; - const axis = getAlignmentAxis(placement); - const length = getAxisLength(axis); - const arrowDimensions = await platform2.getDimensions(element); - const isYAxis = axis === "y"; - const minProp = isYAxis ? "top" : "left"; - const maxProp = isYAxis ? "bottom" : "right"; - const clientProp = isYAxis ? "clientHeight" : "clientWidth"; - const endDiff = rects.reference[length] + rects.reference[axis] - coords[axis] - rects.floating[length]; - const startDiff = coords[axis] - rects.reference[axis]; - const arrowOffsetParent = await (platform2.getOffsetParent == null ? void 0 : platform2.getOffsetParent(element)); - let clientSize = arrowOffsetParent ? arrowOffsetParent[clientProp] : 0; - if (!clientSize || !await (platform2.isElement == null ? void 0 : platform2.isElement(arrowOffsetParent))) { - clientSize = elements.floating[clientProp] || rects.floating[length]; - } - const centerToReference = endDiff / 2 - startDiff / 2; - const largestPossiblePadding = clientSize / 2 - arrowDimensions[length] / 2 - 1; - const minPadding = min(paddingObject[minProp], largestPossiblePadding); - const maxPadding = min(paddingObject[maxProp], largestPossiblePadding); - const min$1 = minPadding; - const max2 = clientSize - arrowDimensions[length] - maxPadding; - const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference; - const offset3 = clamp(min$1, center, max2); - const shouldAddOffset = !middlewareData.arrow && getAlignment(placement) != null && center !== offset3 && rects.reference[length] / 2 - (center < min$1 ? minPadding : maxPadding) - arrowDimensions[length] / 2 < 0; - const alignmentOffset = shouldAddOffset ? center < min$1 ? center - min$1 : center - max2 : 0; - return { - [axis]: coords[axis] + alignmentOffset, - data: { - [axis]: offset3, - centerOffset: center - offset3 - alignmentOffset, - ...shouldAddOffset && { - alignmentOffset - } - }, - reset: shouldAddOffset - }; - } -}); -var flip = function(options) { - if (options === void 0) { - options = {}; - } - return { - name: "flip", - options, - async fn(state) { - var _middlewareData$arrow, _middlewareData$flip; - const { - placement, - middlewareData, - rects, - initialPlacement, - platform: platform2, - elements - } = state; - const { - mainAxis: checkMainAxis = true, - crossAxis: checkCrossAxis = true, - fallbackPlacements: specifiedFallbackPlacements, - fallbackStrategy = "bestFit", - fallbackAxisSideDirection = "none", - flipAlignment = true, - ...detectOverflowOptions - } = evaluate(options, state); - if ((_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) { - return {}; - } - const side = getSide(placement); - const initialSideAxis = getSideAxis(initialPlacement); - const isBasePlacement = getSide(initialPlacement) === initialPlacement; - const rtl = await (platform2.isRTL == null ? void 0 : platform2.isRTL(elements.floating)); - const fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipAlignment ? [getOppositePlacement(initialPlacement)] : getExpandedPlacements(initialPlacement)); - const hasFallbackAxisSideDirection = fallbackAxisSideDirection !== "none"; - if (!specifiedFallbackPlacements && hasFallbackAxisSideDirection) { - fallbackPlacements.push(...getOppositeAxisPlacements(initialPlacement, flipAlignment, fallbackAxisSideDirection, rtl)); - } - const placements2 = [initialPlacement, ...fallbackPlacements]; - const overflow = await platform2.detectOverflow(state, detectOverflowOptions); - const overflows = []; - let overflowsData = ((_middlewareData$flip = middlewareData.flip) == null ? void 0 : _middlewareData$flip.overflows) || []; - if (checkMainAxis) { - overflows.push(overflow[side]); - } - if (checkCrossAxis) { - const sides2 = getAlignmentSides(placement, rects, rtl); - overflows.push(overflow[sides2[0]], overflow[sides2[1]]); - } - overflowsData = [...overflowsData, { - placement, - overflows - }]; - if (!overflows.every((side2) => side2 <= 0)) { - var _middlewareData$flip2, _overflowsData$filter; - const nextIndex = (((_middlewareData$flip2 = middlewareData.flip) == null ? void 0 : _middlewareData$flip2.index) || 0) + 1; - const nextPlacement = placements2[nextIndex]; - if (nextPlacement) { - const ignoreCrossAxisOverflow = checkCrossAxis === "alignment" ? initialSideAxis !== getSideAxis(nextPlacement) : false; - if (!ignoreCrossAxisOverflow || // We leave the current main axis only if every placement on that axis - // overflows the main axis. - overflowsData.every((d) => getSideAxis(d.placement) === initialSideAxis ? d.overflows[0] > 0 : true)) { - return { - data: { - index: nextIndex, - overflows: overflowsData - }, - reset: { - placement: nextPlacement - } - }; - } - } - let resetPlacement = (_overflowsData$filter = overflowsData.filter((d) => d.overflows[0] <= 0).sort((a, b) => a.overflows[1] - b.overflows[1])[0]) == null ? void 0 : _overflowsData$filter.placement; - if (!resetPlacement) { - switch (fallbackStrategy) { - case "bestFit": { - var _overflowsData$filter2; - const placement2 = (_overflowsData$filter2 = overflowsData.filter((d) => { - if (hasFallbackAxisSideDirection) { - const currentSideAxis = getSideAxis(d.placement); - return currentSideAxis === initialSideAxis || // Create a bias to the `y` side axis due to horizontal - // reading directions favoring greater width. - currentSideAxis === "y"; - } - return true; - }).map((d) => [d.placement, d.overflows.filter((overflow2) => overflow2 > 0).reduce((acc, overflow2) => acc + overflow2, 0)]).sort((a, b) => a[1] - b[1])[0]) == null ? void 0 : _overflowsData$filter2[0]; - if (placement2) { - resetPlacement = placement2; - } - break; - } - case "initialPlacement": - resetPlacement = initialPlacement; - break; - } - } - if (placement !== resetPlacement) { - return { - reset: { - placement: resetPlacement - } - }; - } - } - return {}; - } - }; -}; -function getSideOffsets(overflow, rect) { - return { - top: overflow.top - rect.height, - right: overflow.right - rect.width, - bottom: overflow.bottom - rect.height, - left: overflow.left - rect.width - }; -} -function isAnySideFullyClipped(overflow) { - return sides.some((side) => overflow[side] >= 0); -} -var hide = function(options) { - if (options === void 0) { - options = {}; - } - return { - name: "hide", - options, - async fn(state) { - const { - rects, - platform: platform2 - } = state; - const { - strategy = "referenceHidden", - ...detectOverflowOptions - } = evaluate(options, state); - switch (strategy) { - case "referenceHidden": { - const overflow = await platform2.detectOverflow(state, { - ...detectOverflowOptions, - elementContext: "reference" - }); - const offsets = getSideOffsets(overflow, rects.reference); - return { - data: { - referenceHiddenOffsets: offsets, - referenceHidden: isAnySideFullyClipped(offsets) - } - }; - } - case "escaped": { - const overflow = await platform2.detectOverflow(state, { - ...detectOverflowOptions, - altBoundary: true - }); - const offsets = getSideOffsets(overflow, rects.floating); - return { - data: { - escapedOffsets: offsets, - escaped: isAnySideFullyClipped(offsets) - } - }; - } - default: { - return {}; - } - } - } - }; -}; -var originSides = /* @__PURE__ */ new Set(["left", "top"]); -async function convertValueToCoords(state, options) { - const { - placement, - platform: platform2, - elements - } = state; - const rtl = await (platform2.isRTL == null ? void 0 : platform2.isRTL(elements.floating)); - const side = getSide(placement); - const alignment = getAlignment(placement); - const isVertical = getSideAxis(placement) === "y"; - const mainAxisMulti = originSides.has(side) ? -1 : 1; - const crossAxisMulti = rtl && isVertical ? -1 : 1; - const rawValue = evaluate(options, state); - let { - mainAxis, - crossAxis, - alignmentAxis - } = typeof rawValue === "number" ? { - mainAxis: rawValue, - crossAxis: 0, - alignmentAxis: null - } : { - mainAxis: rawValue.mainAxis || 0, - crossAxis: rawValue.crossAxis || 0, - alignmentAxis: rawValue.alignmentAxis - }; - if (alignment && typeof alignmentAxis === "number") { - crossAxis = alignment === "end" ? alignmentAxis * -1 : alignmentAxis; - } - return isVertical ? { - x: crossAxis * crossAxisMulti, - y: mainAxis * mainAxisMulti - } : { - x: mainAxis * mainAxisMulti, - y: crossAxis * crossAxisMulti - }; -} -var offset = function(options) { - if (options === void 0) { - options = 0; - } - return { - name: "offset", - options, - async fn(state) { - var _middlewareData$offse, _middlewareData$arrow; - const { - x, - y, - placement, - middlewareData - } = state; - const diffCoords = await convertValueToCoords(state, options); - if (placement === ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse.placement) && (_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) { - return {}; - } - return { - x: x + diffCoords.x, - y: y + diffCoords.y, - data: { - ...diffCoords, - placement - } - }; - } - }; -}; -var shift = function(options) { - if (options === void 0) { - options = {}; - } - return { - name: "shift", - options, - async fn(state) { - const { - x, - y, - placement, - platform: platform2 - } = state; - const { - mainAxis: checkMainAxis = true, - crossAxis: checkCrossAxis = false, - limiter = { - fn: (_ref) => { - let { - x: x2, - y: y2 - } = _ref; - return { - x: x2, - y: y2 - }; - } - }, - ...detectOverflowOptions - } = evaluate(options, state); - const coords = { - x, - y - }; - const overflow = await platform2.detectOverflow(state, detectOverflowOptions); - const crossAxis = getSideAxis(getSide(placement)); - const mainAxis = getOppositeAxis(crossAxis); - let mainAxisCoord = coords[mainAxis]; - let crossAxisCoord = coords[crossAxis]; - if (checkMainAxis) { - const minSide = mainAxis === "y" ? "top" : "left"; - const maxSide = mainAxis === "y" ? "bottom" : "right"; - const min2 = mainAxisCoord + overflow[minSide]; - const max2 = mainAxisCoord - overflow[maxSide]; - mainAxisCoord = clamp(min2, mainAxisCoord, max2); - } - if (checkCrossAxis) { - const minSide = crossAxis === "y" ? "top" : "left"; - const maxSide = crossAxis === "y" ? "bottom" : "right"; - const min2 = crossAxisCoord + overflow[minSide]; - const max2 = crossAxisCoord - overflow[maxSide]; - crossAxisCoord = clamp(min2, crossAxisCoord, max2); - } - const limitedCoords = limiter.fn({ - ...state, - [mainAxis]: mainAxisCoord, - [crossAxis]: crossAxisCoord - }); - return { - ...limitedCoords, - data: { - x: limitedCoords.x - x, - y: limitedCoords.y - y, - enabled: { - [mainAxis]: checkMainAxis, - [crossAxis]: checkCrossAxis - } - } - }; - } - }; -}; -var limitShift = function(options) { - if (options === void 0) { - options = {}; - } - return { - options, - fn(state) { - const { - x, - y, - placement, - rects, - middlewareData - } = state; - const { - offset: offset3 = 0, - mainAxis: checkMainAxis = true, - crossAxis: checkCrossAxis = true - } = evaluate(options, state); - const coords = { - x, - y - }; - const crossAxis = getSideAxis(placement); - const mainAxis = getOppositeAxis(crossAxis); - let mainAxisCoord = coords[mainAxis]; - let crossAxisCoord = coords[crossAxis]; - const rawOffset = evaluate(offset3, state); - const computedOffset = typeof rawOffset === "number" ? { - mainAxis: rawOffset, - crossAxis: 0 - } : { - mainAxis: 0, - crossAxis: 0, - ...rawOffset - }; - if (checkMainAxis) { - const len = mainAxis === "y" ? "height" : "width"; - const limitMin = rects.reference[mainAxis] - rects.floating[len] + computedOffset.mainAxis; - const limitMax = rects.reference[mainAxis] + rects.reference[len] - computedOffset.mainAxis; - if (mainAxisCoord < limitMin) { - mainAxisCoord = limitMin; - } else if (mainAxisCoord > limitMax) { - mainAxisCoord = limitMax; - } - } - if (checkCrossAxis) { - var _middlewareData$offse, _middlewareData$offse2; - const len = mainAxis === "y" ? "width" : "height"; - const isOriginSide = originSides.has(getSide(placement)); - const limitMin = rects.reference[crossAxis] - rects.floating[len] + (isOriginSide ? ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse[crossAxis]) || 0 : 0) + (isOriginSide ? 0 : computedOffset.crossAxis); - const limitMax = rects.reference[crossAxis] + rects.reference[len] + (isOriginSide ? 0 : ((_middlewareData$offse2 = middlewareData.offset) == null ? void 0 : _middlewareData$offse2[crossAxis]) || 0) - (isOriginSide ? computedOffset.crossAxis : 0); - if (crossAxisCoord < limitMin) { - crossAxisCoord = limitMin; - } else if (crossAxisCoord > limitMax) { - crossAxisCoord = limitMax; - } - } - return { - [mainAxis]: mainAxisCoord, - [crossAxis]: crossAxisCoord - }; - } - }; -}; -var size = function(options) { - if (options === void 0) { - options = {}; - } - return { - name: "size", - options, - async fn(state) { - var _state$middlewareData, _state$middlewareData2; - const { - placement, - rects, - platform: platform2, - elements - } = state; - const { - apply = () => { - }, - ...detectOverflowOptions - } = evaluate(options, state); - const overflow = await platform2.detectOverflow(state, detectOverflowOptions); - const side = getSide(placement); - const alignment = getAlignment(placement); - const isYAxis = getSideAxis(placement) === "y"; - const { - width, - height - } = rects.floating; - let heightSide; - let widthSide; - if (side === "top" || side === "bottom") { - heightSide = side; - widthSide = alignment === (await (platform2.isRTL == null ? void 0 : platform2.isRTL(elements.floating)) ? "start" : "end") ? "left" : "right"; - } else { - widthSide = side; - heightSide = alignment === "end" ? "top" : "bottom"; - } - const maximumClippingHeight = height - overflow.top - overflow.bottom; - const maximumClippingWidth = width - overflow.left - overflow.right; - const overflowAvailableHeight = min(height - overflow[heightSide], maximumClippingHeight); - const overflowAvailableWidth = min(width - overflow[widthSide], maximumClippingWidth); - const noShift = !state.middlewareData.shift; - let availableHeight = overflowAvailableHeight; - let availableWidth = overflowAvailableWidth; - if ((_state$middlewareData = state.middlewareData.shift) != null && _state$middlewareData.enabled.x) { - availableWidth = maximumClippingWidth; - } - if ((_state$middlewareData2 = state.middlewareData.shift) != null && _state$middlewareData2.enabled.y) { - availableHeight = maximumClippingHeight; - } - if (noShift && !alignment) { - const xMin = max(overflow.left, 0); - const xMax = max(overflow.right, 0); - const yMin = max(overflow.top, 0); - const yMax = max(overflow.bottom, 0); - if (isYAxis) { - availableWidth = width - 2 * (xMin !== 0 || xMax !== 0 ? xMin + xMax : max(overflow.left, overflow.right)); - } else { - availableHeight = height - 2 * (yMin !== 0 || yMax !== 0 ? yMin + yMax : max(overflow.top, overflow.bottom)); - } - } - await apply({ - ...state, - availableWidth, - availableHeight - }); - const nextDimensions = await platform2.getDimensions(elements.floating); - if (width !== nextDimensions.width || height !== nextDimensions.height) { - return { - reset: { - rects: true - } - }; - } - return {}; - } - }; -}; - -// ../node_modules/.pnpm/@floating-ui+utils@0.2.10/node_modules/@floating-ui/utils/dist/floating-ui.utils.dom.mjs -function hasWindow() { - return typeof window !== "undefined"; -} -function getNodeName(node) { - if (isNode(node)) { - return (node.nodeName || "").toLowerCase(); - } - return "#document"; -} -function getWindow2(node) { - var _node$ownerDocument; - return (node == null || (_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.defaultView) || window; -} -function getDocumentElement(node) { - var _ref; - return (_ref = (isNode(node) ? node.ownerDocument : node.document) || window.document) == null ? void 0 : _ref.documentElement; -} -function isNode(value) { - if (!hasWindow()) { - return false; - } - return value instanceof Node || value instanceof getWindow2(value).Node; -} -function isElement(value) { - if (!hasWindow()) { - return false; - } - return value instanceof Element || value instanceof getWindow2(value).Element; -} -function isHTMLElement2(value) { - if (!hasWindow()) { - return false; - } - return value instanceof HTMLElement || value instanceof getWindow2(value).HTMLElement; -} -function isShadowRoot(value) { - if (!hasWindow() || typeof ShadowRoot === "undefined") { - return false; - } - return value instanceof ShadowRoot || value instanceof getWindow2(value).ShadowRoot; -} -var invalidOverflowDisplayValues = /* @__PURE__ */ new Set(["inline", "contents"]); -function isOverflowElement(element) { - const { - overflow, - overflowX, - overflowY, - display - } = getComputedStyle3(element); - return /auto|scroll|overlay|hidden|clip/.test(overflow + overflowY + overflowX) && !invalidOverflowDisplayValues.has(display); -} -var tableElements = /* @__PURE__ */ new Set(["table", "td", "th"]); -function isTableElement(element) { - return tableElements.has(getNodeName(element)); -} -var topLayerSelectors = [":popover-open", ":modal"]; -function isTopLayer(element) { - return topLayerSelectors.some((selector) => { - try { - return element.matches(selector); - } catch (_e) { - return false; - } - }); -} -var transformProperties = ["transform", "translate", "scale", "rotate", "perspective"]; -var willChangeValues = ["transform", "translate", "scale", "rotate", "perspective", "filter"]; -var containValues = ["paint", "layout", "strict", "content"]; -function isContainingBlock(elementOrCss) { - const webkit = isWebKit(); - const css = isElement(elementOrCss) ? getComputedStyle3(elementOrCss) : elementOrCss; - return transformProperties.some((value) => css[value] ? css[value] !== "none" : false) || (css.containerType ? css.containerType !== "normal" : false) || !webkit && (css.backdropFilter ? css.backdropFilter !== "none" : false) || !webkit && (css.filter ? css.filter !== "none" : false) || willChangeValues.some((value) => (css.willChange || "").includes(value)) || containValues.some((value) => (css.contain || "").includes(value)); -} -function getContainingBlock(element) { - let currentNode = getParentNode(element); - while (isHTMLElement2(currentNode) && !isLastTraversableNode(currentNode)) { - if (isContainingBlock(currentNode)) { - return currentNode; - } else if (isTopLayer(currentNode)) { - return null; - } - currentNode = getParentNode(currentNode); - } - return null; -} -function isWebKit() { - if (typeof CSS === "undefined" || !CSS.supports) return false; - return CSS.supports("-webkit-backdrop-filter", "none"); -} -var lastTraversableNodeNames = /* @__PURE__ */ new Set(["html", "body", "#document"]); -function isLastTraversableNode(node) { - return lastTraversableNodeNames.has(getNodeName(node)); -} -function getComputedStyle3(element) { - return getWindow2(element).getComputedStyle(element); -} -function getNodeScroll(element) { - if (isElement(element)) { - return { - scrollLeft: element.scrollLeft, - scrollTop: element.scrollTop - }; - } - return { - scrollLeft: element.scrollX, - scrollTop: element.scrollY - }; -} -function getParentNode(node) { - if (getNodeName(node) === "html") { - return node; - } - const result = ( - // Step into the shadow DOM of the parent of a slotted node. - node.assignedSlot || // DOM Element detected. - node.parentNode || // ShadowRoot detected. - isShadowRoot(node) && node.host || // Fallback. - getDocumentElement(node) - ); - return isShadowRoot(result) ? result.host : result; -} -function getNearestOverflowAncestor(node) { - const parentNode = getParentNode(node); - if (isLastTraversableNode(parentNode)) { - return node.ownerDocument ? node.ownerDocument.body : node.body; - } - if (isHTMLElement2(parentNode) && isOverflowElement(parentNode)) { - return parentNode; - } - return getNearestOverflowAncestor(parentNode); -} -function getOverflowAncestors(node, list, traverseIframes) { - var _node$ownerDocument2; - if (list === void 0) { - list = []; - } - if (traverseIframes === void 0) { - traverseIframes = true; - } - const scrollableAncestor = getNearestOverflowAncestor(node); - const isBody = scrollableAncestor === ((_node$ownerDocument2 = node.ownerDocument) == null ? void 0 : _node$ownerDocument2.body); - const win = getWindow2(scrollableAncestor); - if (isBody) { - const frameElement = getFrameElement(win); - return list.concat(win, win.visualViewport || [], isOverflowElement(scrollableAncestor) ? scrollableAncestor : [], frameElement && traverseIframes ? getOverflowAncestors(frameElement) : []); - } - return list.concat(scrollableAncestor, getOverflowAncestors(scrollableAncestor, [], traverseIframes)); -} -function getFrameElement(win) { - return win.parent && Object.getPrototypeOf(win.parent) ? win.frameElement : null; -} - -// ../node_modules/.pnpm/@floating-ui+dom@1.7.5/node_modules/@floating-ui/dom/dist/floating-ui.dom.mjs -function getCssDimensions(element) { - const css = getComputedStyle3(element); - let width = parseFloat(css.width) || 0; - let height = parseFloat(css.height) || 0; - const hasOffset = isHTMLElement2(element); - const offsetWidth = hasOffset ? element.offsetWidth : width; - const offsetHeight = hasOffset ? element.offsetHeight : height; - const shouldFallback = round(width) !== offsetWidth || round(height) !== offsetHeight; - if (shouldFallback) { - width = offsetWidth; - height = offsetHeight; - } - return { - width, - height, - $: shouldFallback - }; -} -function unwrapElement(element) { - return !isElement(element) ? element.contextElement : element; -} -function getScale(element) { - const domElement = unwrapElement(element); - if (!isHTMLElement2(domElement)) { - return createCoords(1); - } - const rect = domElement.getBoundingClientRect(); - const { - width, - height, - $ - } = getCssDimensions(domElement); - let x = ($ ? round(rect.width) : rect.width) / width; - let y = ($ ? round(rect.height) : rect.height) / height; - if (!x || !Number.isFinite(x)) { - x = 1; - } - if (!y || !Number.isFinite(y)) { - y = 1; - } - return { - x, - y - }; -} -var noOffsets = /* @__PURE__ */ createCoords(0); -function getVisualOffsets(element) { - const win = getWindow2(element); - if (!isWebKit() || !win.visualViewport) { - return noOffsets; - } - return { - x: win.visualViewport.offsetLeft, - y: win.visualViewport.offsetTop - }; -} -function shouldAddVisualOffsets(element, isFixed, floatingOffsetParent) { - if (isFixed === void 0) { - isFixed = false; - } - if (!floatingOffsetParent || isFixed && floatingOffsetParent !== getWindow2(element)) { - return false; - } - return isFixed; -} -function getBoundingClientRect(element, includeScale, isFixedStrategy, offsetParent) { - if (includeScale === void 0) { - includeScale = false; - } - if (isFixedStrategy === void 0) { - isFixedStrategy = false; - } - const clientRect = element.getBoundingClientRect(); - const domElement = unwrapElement(element); - let scale = createCoords(1); - if (includeScale) { - if (offsetParent) { - if (isElement(offsetParent)) { - scale = getScale(offsetParent); - } - } else { - scale = getScale(element); - } - } - const visualOffsets = shouldAddVisualOffsets(domElement, isFixedStrategy, offsetParent) ? getVisualOffsets(domElement) : createCoords(0); - let x = (clientRect.left + visualOffsets.x) / scale.x; - let y = (clientRect.top + visualOffsets.y) / scale.y; - let width = clientRect.width / scale.x; - let height = clientRect.height / scale.y; - if (domElement) { - const win = getWindow2(domElement); - const offsetWin = offsetParent && isElement(offsetParent) ? getWindow2(offsetParent) : offsetParent; - let currentWin = win; - let currentIFrame = getFrameElement(currentWin); - while (currentIFrame && offsetParent && offsetWin !== currentWin) { - const iframeScale = getScale(currentIFrame); - const iframeRect = currentIFrame.getBoundingClientRect(); - const css = getComputedStyle3(currentIFrame); - const left = iframeRect.left + (currentIFrame.clientLeft + parseFloat(css.paddingLeft)) * iframeScale.x; - const top = iframeRect.top + (currentIFrame.clientTop + parseFloat(css.paddingTop)) * iframeScale.y; - x *= iframeScale.x; - y *= iframeScale.y; - width *= iframeScale.x; - height *= iframeScale.y; - x += left; - y += top; - currentWin = getWindow2(currentIFrame); - currentIFrame = getFrameElement(currentWin); - } - } - return rectToClientRect({ - width, - height, - x, - y - }); -} -function getWindowScrollBarX(element, rect) { - const leftScroll = getNodeScroll(element).scrollLeft; - if (!rect) { - return getBoundingClientRect(getDocumentElement(element)).left + leftScroll; - } - return rect.left + leftScroll; -} -function getHTMLOffset(documentElement, scroll) { - const htmlRect = documentElement.getBoundingClientRect(); - const x = htmlRect.left + scroll.scrollLeft - getWindowScrollBarX(documentElement, htmlRect); - const y = htmlRect.top + scroll.scrollTop; - return { - x, - y - }; -} -function convertOffsetParentRelativeRectToViewportRelativeRect(_ref) { - let { - elements, - rect, - offsetParent, - strategy - } = _ref; - const isFixed = strategy === "fixed"; - const documentElement = getDocumentElement(offsetParent); - const topLayer = elements ? isTopLayer(elements.floating) : false; - if (offsetParent === documentElement || topLayer && isFixed) { - return rect; - } - let scroll = { - scrollLeft: 0, - scrollTop: 0 - }; - let scale = createCoords(1); - const offsets = createCoords(0); - const isOffsetParentAnElement = isHTMLElement2(offsetParent); - if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { - if (getNodeName(offsetParent) !== "body" || isOverflowElement(documentElement)) { - scroll = getNodeScroll(offsetParent); - } - if (isHTMLElement2(offsetParent)) { - const offsetRect = getBoundingClientRect(offsetParent); - scale = getScale(offsetParent); - offsets.x = offsetRect.x + offsetParent.clientLeft; - offsets.y = offsetRect.y + offsetParent.clientTop; - } - } - const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll) : createCoords(0); - return { - width: rect.width * scale.x, - height: rect.height * scale.y, - x: rect.x * scale.x - scroll.scrollLeft * scale.x + offsets.x + htmlOffset.x, - y: rect.y * scale.y - scroll.scrollTop * scale.y + offsets.y + htmlOffset.y - }; -} -function getClientRects(element) { - return Array.from(element.getClientRects()); -} -function getDocumentRect(element) { - const html = getDocumentElement(element); - const scroll = getNodeScroll(element); - const body = element.ownerDocument.body; - const width = max(html.scrollWidth, html.clientWidth, body.scrollWidth, body.clientWidth); - const height = max(html.scrollHeight, html.clientHeight, body.scrollHeight, body.clientHeight); - let x = -scroll.scrollLeft + getWindowScrollBarX(element); - const y = -scroll.scrollTop; - if (getComputedStyle3(body).direction === "rtl") { - x += max(html.clientWidth, body.clientWidth) - width; - } - return { - width, - height, - x, - y - }; -} -var SCROLLBAR_MAX = 25; -function getViewportRect(element, strategy) { - const win = getWindow2(element); - const html = getDocumentElement(element); - const visualViewport = win.visualViewport; - let width = html.clientWidth; - let height = html.clientHeight; - let x = 0; - let y = 0; - if (visualViewport) { - width = visualViewport.width; - height = visualViewport.height; - const visualViewportBased = isWebKit(); - if (!visualViewportBased || visualViewportBased && strategy === "fixed") { - x = visualViewport.offsetLeft; - y = visualViewport.offsetTop; - } - } - const windowScrollbarX = getWindowScrollBarX(html); - if (windowScrollbarX <= 0) { - const doc = html.ownerDocument; - const body = doc.body; - const bodyStyles = getComputedStyle(body); - const bodyMarginInline = doc.compatMode === "CSS1Compat" ? parseFloat(bodyStyles.marginLeft) + parseFloat(bodyStyles.marginRight) || 0 : 0; - const clippingStableScrollbarWidth = Math.abs(html.clientWidth - body.clientWidth - bodyMarginInline); - if (clippingStableScrollbarWidth <= SCROLLBAR_MAX) { - width -= clippingStableScrollbarWidth; - } - } else if (windowScrollbarX <= SCROLLBAR_MAX) { - width += windowScrollbarX; - } - return { - width, - height, - x, - y - }; -} -var absoluteOrFixed = /* @__PURE__ */ new Set(["absolute", "fixed"]); -function getInnerBoundingClientRect(element, strategy) { - const clientRect = getBoundingClientRect(element, true, strategy === "fixed"); - const top = clientRect.top + element.clientTop; - const left = clientRect.left + element.clientLeft; - const scale = isHTMLElement2(element) ? getScale(element) : createCoords(1); - const width = element.clientWidth * scale.x; - const height = element.clientHeight * scale.y; - const x = left * scale.x; - const y = top * scale.y; - return { - width, - height, - x, - y - }; -} -function getClientRectFromClippingAncestor(element, clippingAncestor, strategy) { - let rect; - if (clippingAncestor === "viewport") { - rect = getViewportRect(element, strategy); - } else if (clippingAncestor === "document") { - rect = getDocumentRect(getDocumentElement(element)); - } else if (isElement(clippingAncestor)) { - rect = getInnerBoundingClientRect(clippingAncestor, strategy); - } else { - const visualOffsets = getVisualOffsets(element); - rect = { - x: clippingAncestor.x - visualOffsets.x, - y: clippingAncestor.y - visualOffsets.y, - width: clippingAncestor.width, - height: clippingAncestor.height - }; - } - return rectToClientRect(rect); -} -function hasFixedPositionAncestor(element, stopNode) { - const parentNode = getParentNode(element); - if (parentNode === stopNode || !isElement(parentNode) || isLastTraversableNode(parentNode)) { - return false; - } - return getComputedStyle3(parentNode).position === "fixed" || hasFixedPositionAncestor(parentNode, stopNode); -} -function getClippingElementAncestors(element, cache) { - const cachedResult = cache.get(element); - if (cachedResult) { - return cachedResult; - } - let result = getOverflowAncestors(element, [], false).filter((el) => isElement(el) && getNodeName(el) !== "body"); - let currentContainingBlockComputedStyle = null; - const elementIsFixed = getComputedStyle3(element).position === "fixed"; - let currentNode = elementIsFixed ? getParentNode(element) : element; - while (isElement(currentNode) && !isLastTraversableNode(currentNode)) { - const computedStyle = getComputedStyle3(currentNode); - const currentNodeIsContaining = isContainingBlock(currentNode); - if (!currentNodeIsContaining && computedStyle.position === "fixed") { - currentContainingBlockComputedStyle = null; - } - const shouldDropCurrentNode = elementIsFixed ? !currentNodeIsContaining && !currentContainingBlockComputedStyle : !currentNodeIsContaining && computedStyle.position === "static" && !!currentContainingBlockComputedStyle && absoluteOrFixed.has(currentContainingBlockComputedStyle.position) || isOverflowElement(currentNode) && !currentNodeIsContaining && hasFixedPositionAncestor(element, currentNode); - if (shouldDropCurrentNode) { - result = result.filter((ancestor) => ancestor !== currentNode); - } else { - currentContainingBlockComputedStyle = computedStyle; - } - currentNode = getParentNode(currentNode); - } - cache.set(element, result); - return result; -} -function getClippingRect(_ref) { - let { - element, - boundary, - rootBoundary, - strategy - } = _ref; - const elementClippingAncestors = boundary === "clippingAncestors" ? isTopLayer(element) ? [] : getClippingElementAncestors(element, this._c) : [].concat(boundary); - const clippingAncestors = [...elementClippingAncestors, rootBoundary]; - const firstClippingAncestor = clippingAncestors[0]; - const clippingRect = clippingAncestors.reduce((accRect, clippingAncestor) => { - const rect = getClientRectFromClippingAncestor(element, clippingAncestor, strategy); - accRect.top = max(rect.top, accRect.top); - accRect.right = min(rect.right, accRect.right); - accRect.bottom = min(rect.bottom, accRect.bottom); - accRect.left = max(rect.left, accRect.left); - return accRect; - }, getClientRectFromClippingAncestor(element, firstClippingAncestor, strategy)); - return { - width: clippingRect.right - clippingRect.left, - height: clippingRect.bottom - clippingRect.top, - x: clippingRect.left, - y: clippingRect.top - }; -} -function getDimensions(element) { - const { - width, - height - } = getCssDimensions(element); - return { - width, - height - }; -} -function getRectRelativeToOffsetParent(element, offsetParent, strategy) { - const isOffsetParentAnElement = isHTMLElement2(offsetParent); - const documentElement = getDocumentElement(offsetParent); - const isFixed = strategy === "fixed"; - const rect = getBoundingClientRect(element, true, isFixed, offsetParent); - let scroll = { - scrollLeft: 0, - scrollTop: 0 - }; - const offsets = createCoords(0); - function setLeftRTLScrollbarOffset() { - offsets.x = getWindowScrollBarX(documentElement); - } - if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { - if (getNodeName(offsetParent) !== "body" || isOverflowElement(documentElement)) { - scroll = getNodeScroll(offsetParent); - } - if (isOffsetParentAnElement) { - const offsetRect = getBoundingClientRect(offsetParent, true, isFixed, offsetParent); - offsets.x = offsetRect.x + offsetParent.clientLeft; - offsets.y = offsetRect.y + offsetParent.clientTop; - } else if (documentElement) { - setLeftRTLScrollbarOffset(); - } - } - if (isFixed && !isOffsetParentAnElement && documentElement) { - setLeftRTLScrollbarOffset(); - } - const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll) : createCoords(0); - const x = rect.left + scroll.scrollLeft - offsets.x - htmlOffset.x; - const y = rect.top + scroll.scrollTop - offsets.y - htmlOffset.y; - return { - x, - y, - width: rect.width, - height: rect.height - }; -} -function isStaticPositioned(element) { - return getComputedStyle3(element).position === "static"; -} -function getTrueOffsetParent(element, polyfill) { - if (!isHTMLElement2(element) || getComputedStyle3(element).position === "fixed") { - return null; - } - if (polyfill) { - return polyfill(element); - } - let rawOffsetParent = element.offsetParent; - if (getDocumentElement(element) === rawOffsetParent) { - rawOffsetParent = rawOffsetParent.ownerDocument.body; - } - return rawOffsetParent; -} -function getOffsetParent(element, polyfill) { - const win = getWindow2(element); - if (isTopLayer(element)) { - return win; - } - if (!isHTMLElement2(element)) { - let svgOffsetParent = getParentNode(element); - while (svgOffsetParent && !isLastTraversableNode(svgOffsetParent)) { - if (isElement(svgOffsetParent) && !isStaticPositioned(svgOffsetParent)) { - return svgOffsetParent; - } - svgOffsetParent = getParentNode(svgOffsetParent); - } - return win; - } - let offsetParent = getTrueOffsetParent(element, polyfill); - while (offsetParent && isTableElement(offsetParent) && isStaticPositioned(offsetParent)) { - offsetParent = getTrueOffsetParent(offsetParent, polyfill); - } - if (offsetParent && isLastTraversableNode(offsetParent) && isStaticPositioned(offsetParent) && !isContainingBlock(offsetParent)) { - return win; - } - return offsetParent || getContainingBlock(element) || win; -} -var getElementRects = async function(data) { - const getOffsetParentFn = this.getOffsetParent || getOffsetParent; - const getDimensionsFn = this.getDimensions; - const floatingDimensions = await getDimensionsFn(data.floating); - return { - reference: getRectRelativeToOffsetParent(data.reference, await getOffsetParentFn(data.floating), data.strategy), - floating: { - x: 0, - y: 0, - width: floatingDimensions.width, - height: floatingDimensions.height - } - }; -}; -function isRTL(element) { - return getComputedStyle3(element).direction === "rtl"; -} -var platform = { - convertOffsetParentRelativeRectToViewportRelativeRect, - getDocumentElement, - getClippingRect, - getOffsetParent, - getElementRects, - getClientRects, - getDimensions, - getScale, - isElement, - isRTL -}; -function rectsAreEqual(a, b) { - return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height; -} -function observeMove(element, onMove) { - let io = null; - let timeoutId; - const root = getDocumentElement(element); - function cleanup() { - var _io; - clearTimeout(timeoutId); - (_io = io) == null || _io.disconnect(); - io = null; - } - function refresh(skip, threshold) { - if (skip === void 0) { - skip = false; - } - if (threshold === void 0) { - threshold = 1; - } - cleanup(); - const elementRectForRootMargin = element.getBoundingClientRect(); - const { - left, - top, - width, - height - } = elementRectForRootMargin; - if (!skip) { - onMove(); - } - if (!width || !height) { - return; - } - const insetTop = floor(top); - const insetRight = floor(root.clientWidth - (left + width)); - const insetBottom = floor(root.clientHeight - (top + height)); - const insetLeft = floor(left); - const rootMargin = -insetTop + "px " + -insetRight + "px " + -insetBottom + "px " + -insetLeft + "px"; - const options = { - rootMargin, - threshold: max(0, min(1, threshold)) || 1 - }; - let isFirstUpdate = true; - function handleObserve(entries) { - const ratio = entries[0].intersectionRatio; - if (ratio !== threshold) { - if (!isFirstUpdate) { - return refresh(); - } - if (!ratio) { - timeoutId = setTimeout(() => { - refresh(false, 1e-7); - }, 1e3); - } else { - refresh(false, ratio); - } - } - if (ratio === 1 && !rectsAreEqual(elementRectForRootMargin, element.getBoundingClientRect())) { - refresh(); - } - isFirstUpdate = false; - } - try { - io = new IntersectionObserver(handleObserve, { - ...options, - // Handle