diff --git a/packages/core/src/animations.css b/packages/core/src/animations.css index 0c2a2b8..ab6ca42 100644 --- a/packages/core/src/animations.css +++ b/packages/core/src/animations.css @@ -3,43 +3,43 @@ .petty-stagger-hidden { opacity: 0; } .petty-stagger-fade-up { - animation: pettyFadeUp 0.5s ease both; + animation: pettyFadeUp 0.5s cubic-bezier(0.2, 0, 0, 1) both; } .petty-stagger-fade-down { - animation: pettyFadeDown 0.5s ease both; + animation: pettyFadeDown 0.5s cubic-bezier(0.2, 0, 0, 1) both; } .petty-stagger-fade-left { - animation: pettyFadeLeft 0.5s ease both; + animation: pettyFadeLeft 0.5s cubic-bezier(0.2, 0, 0, 1) both; } .petty-stagger-fade-right { - animation: pettyFadeRight 0.5s ease both; + animation: pettyFadeRight 0.5s cubic-bezier(0.2, 0, 0, 1) both; } .petty-stagger-scale { - animation: pettyScale 0.5s ease both; + animation: pettyScale 0.5s cubic-bezier(0.2, 0, 0, 1) both; } .petty-stagger-blur { - animation: pettyBlur 0.6s ease both; + animation: pettyBlur 0.5s cubic-bezier(0.2, 0, 0, 1) both; } .petty-reveal-hidden { opacity: 0; } .petty-reveal-fade-up { - animation: pettyFadeUp 0.6s ease both; + animation: pettyFadeUp 0.5s cubic-bezier(0.2, 0, 0, 1) both; } .petty-reveal-fade-down { - animation: pettyFadeDown 0.6s ease both; + animation: pettyFadeDown 0.5s cubic-bezier(0.2, 0, 0, 1) both; } .petty-reveal-fade-left { - animation: pettyFadeLeft 0.6s ease both; + animation: pettyFadeLeft 0.5s cubic-bezier(0.2, 0, 0, 1) both; } .petty-reveal-fade-right { - animation: pettyFadeRight 0.6s ease both; + animation: pettyFadeRight 0.5s cubic-bezier(0.2, 0, 0, 1) both; } .petty-reveal-scale { - animation: pettyScale 0.6s ease both; + animation: pettyScale 0.5s cubic-bezier(0.2, 0, 0, 1) both; } .petty-reveal-blur { - animation: pettyBlur 0.7s ease both; + animation: pettyBlur 0.5s cubic-bezier(0.2, 0, 0, 1) both; } petty-typewriter.petty-typewriter-cursor::after { @@ -82,7 +82,7 @@ petty-counter { font-variant-numeric: tabular-nums; } } dialog[open] { - animation: pettyDialogIn 0.2s ease; + animation: pettyDialogIn 0.2s cubic-bezier(0.2, 0, 0, 1); } @keyframes pettyDialogIn { from { opacity: 0; transform: translateY(8px) scale(0.98); } @@ -90,7 +90,7 @@ dialog[open] { } [popover]:popover-open { - animation: pettyPopoverIn 0.15s ease; + animation: pettyPopoverIn 0.15s cubic-bezier(0.2, 0, 0, 1); } @keyframes pettyPopoverIn { from { opacity: 0; transform: translateY(-4px); } @@ -98,7 +98,7 @@ dialog[open] { } [data-part="toast"] { - animation: pettySlideIn 0.3s ease; + animation: pettySlideIn 0.3s cubic-bezier(0.2, 0, 0, 1); } @keyframes pettySlideIn { from { opacity: 0; transform: translateX(100%); } @@ -113,25 +113,10 @@ petty-loading-indicator [data-part="indicator"] { border: 3px solid rgba(54, 192, 241, 0.12); border-top-color: #36c0f1; border-right-color: rgba(54, 192, 241, 0.4); - animation: pettyLoadSpin 1.1s linear infinite, pettyLoadPulse 2.2s ease-in-out infinite; -} - -petty-loading-indicator [data-part="container"]::before { - content: ""; - position: absolute; - inset: -2px; - border-radius: 50%; - border: 2px solid transparent; - border-bottom-color: rgba(54, 192, 241, 0.2); - border-left-color: rgba(54, 192, 241, 0.1); - animation: pettyLoadSpin 2.4s linear infinite reverse; + animation: pettyLoadSpin 0.9s linear infinite; } @keyframes pettyLoadSpin { to { rotate: 360deg; } } -@keyframes pettyLoadPulse { - 0%, 100% { box-shadow: 0 0 8px rgba(54, 192, 241, 0.15); } - 50% { box-shadow: 0 0 20px rgba(54, 192, 241, 0.35); } -} diff --git a/packages/core/src/components/accordion/accordion-item.ts b/packages/core/src/components/accordion/accordion-item.ts index e9f3e4b..7a5edcc 100644 --- a/packages/core/src/components/accordion/accordion-item.ts +++ b/packages/core/src/components/accordion/accordion-item.ts @@ -1,3 +1,5 @@ +import { listen } from "../../shared/helpers"; + /** * PettyAccordionItem — wraps a single `
` element within a PettyAccordion. * @@ -34,20 +36,18 @@ export class PettyAccordionItem extends HTMLElement { return this.querySelector("summary")?.textContent?.trim() ?? ""; } + #cleanup = (): void => {}; + /** @internal */ connectedCallback(): void { this.#syncState(); this.#applyDisabled(); - const details = this.detailsElement; - if (details) { - details.addEventListener("toggle", this.#handleToggle); - } + this.#cleanup = listen(this.detailsElement, [["toggle", this.#handleToggle]]); } /** @internal */ disconnectedCallback(): void { - const details = this.detailsElement; - details?.removeEventListener("toggle", this.#handleToggle); + this.#cleanup(); } /** @internal */ diff --git a/packages/core/src/components/avatar/avatar.ts b/packages/core/src/components/avatar/avatar.ts index 83811f9..6968af5 100644 --- a/packages/core/src/components/avatar/avatar.ts +++ b/packages/core/src/components/avatar/avatar.ts @@ -1,3 +1,5 @@ +import { listen } from "../../shared/helpers"; + /** PettyAvatar — image with automatic fallback on load error. */ export class PettyAvatar extends HTMLElement { connectedCallback(): void { diff --git a/packages/core/src/components/calendar/calendar.ts b/packages/core/src/components/calendar/calendar.ts index 4e75788..47fbe53 100644 --- a/packages/core/src/components/calendar/calendar.ts +++ b/packages/core/src/components/calendar/calendar.ts @@ -1,5 +1,5 @@ import { signal, effect } from "../../signals"; -import { emit } from "../../shared/helpers"; +import { emit, listen } from "../../shared/helpers"; /** PettyCalendar — month grid with day selection and month navigation. */ export class PettyCalendar extends HTMLElement { diff --git a/packages/core/src/components/combobox/combobox.ts b/packages/core/src/components/combobox/combobox.ts index a532453..832a764 100644 --- a/packages/core/src/components/combobox/combobox.ts +++ b/packages/core/src/components/combobox/combobox.ts @@ -1,5 +1,5 @@ import { uniqueId } from "../../shared/aria"; -import { emit } from "../../shared/helpers"; +import { emit, listen } from "../../shared/helpers"; /** PettyCombobox — searchable select with popover listbox and keyboard nav. */ export class PettyCombobox extends HTMLElement { diff --git a/packages/core/src/components/command-palette/command-palette.ts b/packages/core/src/components/command-palette/command-palette.ts index acdb430..62193d5 100644 --- a/packages/core/src/components/command-palette/command-palette.ts +++ b/packages/core/src/components/command-palette/command-palette.ts @@ -1,4 +1,4 @@ -import { emit } from "../../shared/helpers"; +import { emit, listen } from "../../shared/helpers"; /** PettyCommandPalette — search-driven command menu using native dialog. */ export class PettyCommandPalette extends HTMLElement { diff --git a/packages/core/src/components/context-menu/context-menu.ts b/packages/core/src/components/context-menu/context-menu.ts index e868a8b..04b05b6 100644 --- a/packages/core/src/components/context-menu/context-menu.ts +++ b/packages/core/src/components/context-menu/context-menu.ts @@ -1,4 +1,4 @@ -import { emit } from "../../shared/helpers"; +import { emit, listen } from "../../shared/helpers"; /** PettyContextMenu — right-click menu using Popover API with keyboard nav. */ export class PettyContextMenu extends HTMLElement { diff --git a/packages/core/src/components/date-picker/date-picker.ts b/packages/core/src/components/date-picker/date-picker.ts index c7315f4..7772724 100644 --- a/packages/core/src/components/date-picker/date-picker.ts +++ b/packages/core/src/components/date-picker/date-picker.ts @@ -1,4 +1,4 @@ -import { emit } from "../../shared/helpers"; +import { emit, listen } from "../../shared/helpers"; /** PettyDatePicker — date input with calendar popover integration. */ export class PettyDatePicker extends HTMLElement { diff --git a/packages/core/src/components/dropdown-menu/dropdown-menu.ts b/packages/core/src/components/dropdown-menu/dropdown-menu.ts index 325fec9..14ae1e8 100644 --- a/packages/core/src/components/dropdown-menu/dropdown-menu.ts +++ b/packages/core/src/components/dropdown-menu/dropdown-menu.ts @@ -1,4 +1,4 @@ -import { emit } from "../../shared/helpers"; +import { emit, listen } from "../../shared/helpers"; /** PettyDropdownMenu — action menu built on the Popover API. */ export class PettyDropdownMenu extends HTMLElement { diff --git a/packages/core/src/components/form/form.ts b/packages/core/src/components/form/form.ts index d3ddbc6..aa2b406 100644 --- a/packages/core/src/components/form/form.ts +++ b/packages/core/src/components/form/form.ts @@ -1,4 +1,4 @@ -import { emit } from "../../shared/helpers"; +import { emit, listen } from "../../shared/helpers"; interface SchemaLike { safeParse: (data: unknown) => { @@ -37,18 +37,19 @@ export class PettyForm extends HTMLElement { this.#schema = schema; } + #cleanup = (): void => {}; + /** @internal */ connectedCallback(): void { const form = this.querySelector("form"); if (!form) return; - form.addEventListener("submit", this.#handleSubmit); form.setAttribute("novalidate", ""); + this.#cleanup = listen(form, [["submit", this.#handleSubmit]]); } /** @internal */ disconnectedCallback(): void { - const form = this.querySelector("form"); - form?.removeEventListener("submit", this.#handleSubmit); + this.#cleanup(); } #handleSubmit = (e: Event): void => { diff --git a/packages/core/src/components/image/image.ts b/packages/core/src/components/image/image.ts index 060622d..7769166 100644 --- a/packages/core/src/components/image/image.ts +++ b/packages/core/src/components/image/image.ts @@ -1,3 +1,5 @@ +import { listen } from "../../shared/helpers"; + /** PettyImage — image element with fallback display on load failure. */ export class PettyImage extends HTMLElement { connectedCallback(): void { diff --git a/packages/core/src/components/link/link.ts b/packages/core/src/components/link/link.ts index 682d3cb..f9cbbd7 100644 --- a/packages/core/src/components/link/link.ts +++ b/packages/core/src/components/link/link.ts @@ -1,3 +1,5 @@ +import { listen } from "../../shared/helpers"; + /** PettyLink — headless anchor wrapper with disabled and external support. */ export class PettyLink extends HTMLElement { static observedAttributes = ["disabled", "external"]; @@ -6,13 +8,15 @@ export class PettyLink extends HTMLElement { return this.querySelector("a"); } + #cleanup = (): void => {}; + connectedCallback(): void { this.#sync(); - this.addEventListener("click", this.#handleClick); + this.#cleanup = listen(this, [["click", this.#handleClick]]); } disconnectedCallback(): void { - this.removeEventListener("click", this.#handleClick); + this.#cleanup(); } attributeChangedCallback(): void { diff --git a/packages/core/src/components/listbox/listbox.ts b/packages/core/src/components/listbox/listbox.ts index a53c63b..5e78eb1 100644 --- a/packages/core/src/components/listbox/listbox.ts +++ b/packages/core/src/components/listbox/listbox.ts @@ -1,5 +1,5 @@ import { signal, effect } from "../../signals"; -import { emit, initialValue } from "../../shared/helpers"; +import { emit, initialValue, listen } from "../../shared/helpers"; /** PettyListbox — inline selectable list with single or multiple selection. */ export class PettyListbox extends HTMLElement { @@ -27,18 +27,18 @@ export class PettyListbox extends HTMLElement { emit(this, "change", { value: this.#value.get() }); } + #cleanup = (): void => {}; + connectedCallback(): void { const init = initialValue(this); if (init) this.#value.set(init); this.#stopEffect = effect(() => this.#syncChildren()); - this.addEventListener("keydown", this.#onKeydown); - this.addEventListener("click", this.#onClick); + this.#cleanup = listen(this, [["keydown", this.#onKeydown], ["click", this.#onClick]]); } disconnectedCallback(): void { this.#stopEffect = null; - this.removeEventListener("keydown", this.#onKeydown); - this.removeEventListener("click", this.#onClick); + this.#cleanup(); } attributeChangedCallback(name: string, _old: string | null, next: string | null): void { @@ -59,18 +59,19 @@ export class PettyListbox extends HTMLElement { } } - #onClick = (e: MouseEvent): void => { + #onClick = (e: Event): void => { const opt = (e.target as HTMLElement).closest("petty-listbox-option"); if (!opt || opt.hasAttribute("disabled")) return; this.selectValue(opt.getAttribute("value") ?? ""); }; - #onKeydown = (e: KeyboardEvent): void => { + #onKeydown = (e: Event): void => { + const ke = e as KeyboardEvent; const items = this.#options(); const active = document.activeElement as HTMLElement; const idx = items.indexOf(active); - if (e.key === "ArrowDown") { e.preventDefault(); items[(idx + 1) % items.length]?.focus(); } - else if (e.key === "ArrowUp") { e.preventDefault(); items[(idx - 1 + items.length) % items.length]?.focus(); } - else if (e.key === "Enter" || e.key === " ") { e.preventDefault(); if (active) this.selectValue(active.getAttribute("value") ?? ""); } + if (ke.key === "ArrowDown") { ke.preventDefault(); items[(idx + 1) % items.length]?.focus(); } + else if (ke.key === "ArrowUp") { ke.preventDefault(); items[(idx - 1 + items.length) % items.length]?.focus(); } + else if (ke.key === "Enter" || ke.key === " ") { ke.preventDefault(); if (active) this.selectValue(active.getAttribute("value") ?? ""); } }; } diff --git a/packages/core/src/components/navigation-menu/navigation-menu-item.ts b/packages/core/src/components/navigation-menu/navigation-menu-item.ts index b34fa06..9769162 100644 --- a/packages/core/src/components/navigation-menu/navigation-menu-item.ts +++ b/packages/core/src/components/navigation-menu/navigation-menu-item.ts @@ -1,3 +1,5 @@ +import { listen } from "../../shared/helpers"; + /** PettyNavigationMenuItem — nav item with optional popover content on hover. */ export class PettyNavigationMenuItem extends HTMLElement { #showTimer: ReturnType | null = null; diff --git a/packages/core/src/components/number-field/number-field.ts b/packages/core/src/components/number-field/number-field.ts index 7318cab..c8cb061 100644 --- a/packages/core/src/components/number-field/number-field.ts +++ b/packages/core/src/components/number-field/number-field.ts @@ -1,5 +1,5 @@ import { signal } from "../../signals"; -import { emit } from "../../shared/helpers"; +import { emit, listen, part } from "../../shared/helpers"; import { uniqueId } from "../../shared/aria"; /** PettyNumberField — numeric input with increment/decrement buttons and clamping. */ @@ -7,6 +7,7 @@ export class PettyNumberField extends HTMLElement { static observedAttributes = ["min", "max", "step", "value", "disabled", "name"]; readonly #value = signal(0); + #cleanup = (): void => {}; get value(): number { return this.#value.get(); } set value(v: number) { this.#applyValue(v); } @@ -18,18 +19,14 @@ export class PettyNumberField extends HTMLElement { this.#syncInput(); this.#wireLabel(); - input?.addEventListener("input", this.#onInput); - input?.addEventListener("keydown", this.#onKeydown); - this.querySelector("[data-part=increment]")?.addEventListener("click", this.#onIncrement); - this.querySelector("[data-part=decrement]")?.addEventListener("click", this.#onDecrement); + const c1 = listen(input, [["input", this.#onInput], ["keydown", this.#onKeydown as EventListener]]); + const c2 = listen(part(this, "increment"), [["click", this.#onIncrement]]); + const c3 = listen(part(this, "decrement"), [["click", this.#onDecrement]]); + this.#cleanup = () => { c1(); c2(); c3(); }; } disconnectedCallback(): void { - const input = this.#input(); - input?.removeEventListener("input", this.#onInput); - input?.removeEventListener("keydown", this.#onKeydown); - this.querySelector("[data-part=increment]")?.removeEventListener("click", this.#onIncrement); - this.querySelector("[data-part=decrement]")?.removeEventListener("click", this.#onDecrement); + this.#cleanup(); } attributeChangedCallback(name: string, _old: string | null, next: string | null): void { diff --git a/packages/core/src/components/pagination/pagination-item.ts b/packages/core/src/components/pagination/pagination-item.ts index eaafe37..f09a4ad 100644 --- a/packages/core/src/components/pagination/pagination-item.ts +++ b/packages/core/src/components/pagination/pagination-item.ts @@ -1,17 +1,19 @@ +import { listen } from "../../shared/helpers"; + /** PettyPaginationItem — single page button within a pagination component. */ export class PettyPaginationItem extends HTMLElement { static observedAttributes = ["value", "type", "disabled"]; + #cleanup = (): void => {}; + connectedCallback(): void { this.setAttribute("role", "button"); this.setAttribute("tabindex", "0"); - this.addEventListener("click", this.#handleClick); - this.addEventListener("keydown", this.#handleKeydown); + this.#cleanup = listen(this, [["click", this.#handleClick], ["keydown", this.#handleKeydown]]); } disconnectedCallback(): void { - this.removeEventListener("click", this.#handleClick); - this.removeEventListener("keydown", this.#handleKeydown); + this.#cleanup(); } attributeChangedCallback(name: string): void { @@ -35,7 +37,8 @@ export class PettyPaginationItem extends HTMLElement { } #handleClick = (): void => { this.#activate(); }; - #handleKeydown = (e: KeyboardEvent): void => { - if (e.key === "Enter" || e.key === " ") { e.preventDefault(); this.#activate(); } + #handleKeydown = (e: Event): void => { + const ke = e as KeyboardEvent; + if (ke.key === "Enter" || ke.key === " ") { ke.preventDefault(); this.#activate(); } }; } diff --git a/packages/core/src/components/radio-group/radio-item.ts b/packages/core/src/components/radio-group/radio-item.ts index a95de5c..f469226 100644 --- a/packages/core/src/components/radio-group/radio-item.ts +++ b/packages/core/src/components/radio-group/radio-item.ts @@ -1,4 +1,5 @@ import { wrapIndex } from "../../shared/keyboard"; +import { listen } from "../../shared/helpers"; /** PettyRadioItem — single radio option within a petty-radio-group. */ export class PettyRadioItem extends HTMLElement { @@ -7,17 +8,17 @@ export class PettyRadioItem extends HTMLElement { get value(): string { return this.getAttribute("value") ?? ""; } get disabled(): boolean { return this.hasAttribute("disabled"); } + #cleanup = (): void => {}; + connectedCallback(): void { this.setAttribute("role", "radio"); this.setAttribute("tabindex", "-1"); if (this.disabled) this.setAttribute("aria-disabled", "true"); - this.addEventListener("click", this.#handleClick); - this.addEventListener("keydown", this.#handleKeydown); + this.#cleanup = listen(this, [["click", this.#handleClick], ["keydown", this.#handleKeydown]]); } disconnectedCallback(): void { - this.removeEventListener("click", this.#handleClick); - this.removeEventListener("keydown", this.#handleKeydown); + this.#cleanup(); } attributeChangedCallback(name: string): void { @@ -42,17 +43,18 @@ export class PettyRadioItem extends HTMLElement { this.focus(); }; - #handleKeydown = (e: KeyboardEvent): void => { + #handleKeydown = (e: Event): void => { + const ke = e as KeyboardEvent; const isVertical = this.closest("petty-radio-group")?.getAttribute("orientation") !== "horizontal"; const prev = isVertical ? "ArrowUp" : "ArrowLeft"; const next = isVertical ? "ArrowDown" : "ArrowRight"; - if (e.key !== prev && e.key !== next && e.key !== " ") return; - e.preventDefault(); - if (e.key === " ") { this.#handleClick(); return; } + if (ke.key !== prev && ke.key !== next && ke.key !== " ") return; + ke.preventDefault(); + if (ke.key === " ") { this.#handleClick(); return; } const items = this.#siblings(); const idx = items.indexOf(this); if (idx === -1) return; - const delta = e.key === next ? 1 : -1; + const delta = ke.key === next ? 1 : -1; const target = items[wrapIndex(idx, delta, items.length)]; if (target) { this.#group()?.selectValue(target.value); diff --git a/packages/core/src/components/slider/slider.ts b/packages/core/src/components/slider/slider.ts index 31f08d9..633d3d6 100644 --- a/packages/core/src/components/slider/slider.ts +++ b/packages/core/src/components/slider/slider.ts @@ -1,10 +1,11 @@ -import { emit } from "../../shared/helpers"; -import { uniqueId } from "../../shared/aria"; +import { emit, listen, part, wireLabel } from "../../shared/helpers"; /** PettySlider — range input wrapper with label, output, and change events. */ export class PettySlider extends HTMLElement { static observedAttributes = ["min", "max", "step", "value", "disabled", "name", "orientation"]; + #cleanup = (): void => {}; + get value(): number { return Number(this.#input()?.value ?? 0); } @@ -18,13 +19,13 @@ export class PettySlider extends HTMLElement { const input = this.#input(); if (!input) return; this.#syncAttrs(); - this.#wireLabel(); + wireLabel(input, part(this, "label"), "petty-slider"); this.#syncOutput(); - input.addEventListener("input", this.#handleInput); + this.#cleanup = listen(input, [["input", this.#handleInput]]); } disconnectedCallback(): void { - this.#input()?.removeEventListener("input", this.#handleInput); + this.#cleanup(); } attributeChangedCallback(): void { @@ -36,15 +37,6 @@ export class PettySlider extends HTMLElement { return this.querySelector("input[data-part=control]"); } - #wireLabel(): void { - const input = this.#input(); - const label = this.querySelector("[data-part=label]"); - if (input && label) { - if (!input.id) input.id = uniqueId("petty-slider"); - (label as HTMLLabelElement).htmlFor = input.id; - } - } - #syncAttrs(): void { const input = this.#input(); if (!input) return; diff --git a/packages/core/src/components/tabs/tab.ts b/packages/core/src/components/tabs/tab.ts index 25b3ad0..f2fe6c5 100644 --- a/packages/core/src/components/tabs/tab.ts +++ b/packages/core/src/components/tabs/tab.ts @@ -1,3 +1,5 @@ +import { listen } from "../../shared/helpers"; + /** * PettyTab — individual tab button within a petty-tabs component. * @@ -19,17 +21,17 @@ export class PettyTab extends HTMLElement { } /** @internal */ + #cleanup = (): void => {}; + connectedCallback(): void { this.setAttribute("role", "tab"); this.setAttribute("tabindex", "-1"); - this.addEventListener("click", this.#handleClick); - this.addEventListener("keydown", this.#handleKeydown); + this.#cleanup = listen(this, [["click", this.#handleClick], ["keydown", this.#handleKeydown]]); } /** @internal */ disconnectedCallback(): void { - this.removeEventListener("click", this.#handleClick); - this.removeEventListener("keydown", this.#handleKeydown); + this.#cleanup(); } /** @internal */ @@ -48,12 +50,13 @@ export class PettyTab extends HTMLElement { active?.focus(); }; - #handleKeydown = (e: KeyboardEvent): void => { - if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return; + #handleKeydown = (e: Event): void => { + const ke = e as KeyboardEvent; + if (ke.key !== "ArrowLeft" && ke.key !== "ArrowRight") return; const tabs = this.#siblingTabs().filter(t => !t.disabled); const idx = tabs.indexOf(this); if (idx === -1) return; - const nextIdx = e.key === "ArrowRight" + const nextIdx = ke.key === "ArrowRight" ? (idx + 1) % tabs.length : (idx - 1 + tabs.length) % tabs.length; const next = tabs[nextIdx]; diff --git a/packages/core/src/components/tags-input/tags-input.ts b/packages/core/src/components/tags-input/tags-input.ts index fa136ec..f69b603 100644 --- a/packages/core/src/components/tags-input/tags-input.ts +++ b/packages/core/src/components/tags-input/tags-input.ts @@ -1,5 +1,5 @@ import { signal, effect } from "../../signals"; -import { emit } from "../../shared/helpers"; +import { emit, listen } from "../../shared/helpers"; /** PettyTagsInput — multi-value text input for tags, emails, or tokens. */ export class PettyTagsInput extends HTMLElement { @@ -41,18 +41,21 @@ export class PettyTagsInput extends HTMLElement { this.#dispatch(); } + #cleanupInput = (): void => {}; + #cleanupSelf = (): void => {}; + connectedCallback(): void { const init = this.getAttribute("value"); if (init) this.#tags.set(init.split(",").map(s => s.trim()).filter(Boolean)); this.#stopEffect = effect(() => this.#render()); - this.#input()?.addEventListener("keydown", this.#onKeydown); - this.addEventListener("click", this.#onTagRemove); + this.#cleanupInput = listen(this.#input(), [["keydown", this.#onKeydown]]); + this.#cleanupSelf = listen(this, [["click", this.#onTagRemove]]); } disconnectedCallback(): void { this.#stopEffect = null; - this.#input()?.removeEventListener("keydown", this.#onKeydown); - this.removeEventListener("click", this.#onTagRemove); + this.#cleanupInput(); + this.#cleanupSelf(); } attributeChangedCallback(name: string, _old: string | null, next: string | null): void { @@ -73,14 +76,15 @@ export class PettyTagsInput extends HTMLElement { if (hidden instanceof HTMLInputElement) hidden.value = this.#tags.get().join(","); } - #onKeydown = (e: KeyboardEvent): void => { + #onKeydown = (e: Event): void => { + const ke = e as KeyboardEvent; if (this.hasAttribute("disabled")) return; const input = this.#input(); if (!input) return; - if (e.key === "Enter" || e.key === ",") { - e.preventDefault(); + if (ke.key === "Enter" || ke.key === ",") { + ke.preventDefault(); if (this.addTag(input.value)) input.value = ""; - } else if (e.key === "Backspace" && input.value === "") { + } else if (ke.key === "Backspace" && input.value === "") { const tags = this.#tags.get(); const last = tags[tags.length - 1]; if (last) this.removeTag(last); diff --git a/packages/core/src/components/text-field/text-field.ts b/packages/core/src/components/text-field/text-field.ts index 173c141..e9ec781 100644 --- a/packages/core/src/components/text-field/text-field.ts +++ b/packages/core/src/components/text-field/text-field.ts @@ -1,20 +1,22 @@ import { uniqueId } from "../../shared/aria"; -import { emit } from "../../shared/helpers"; +import { emit, listen, part } from "../../shared/helpers"; /** PettyTextField — labeled text input with description and error wiring. */ export class PettyTextField extends HTMLElement { static observedAttributes = ["name", "disabled", "required"]; + #cleanup = (): void => {}; + connectedCallback(): void { const name = this.getAttribute("name") ?? ""; const controlId = uniqueId(`petty-tf-${name}`); const errorId = `${controlId}-error`; const descId = `${controlId}-desc`; - const label = this.querySelector("[data-part=label]"); + const label = part(this, "label"); const control = this.#control(); - const desc = this.querySelector("[data-part=description]"); - const error = this.querySelector("[data-part=error]"); + const desc = part(this, "description"); + const error = part(this, "error"); if (control) { control.id = controlId; @@ -22,16 +24,16 @@ export class PettyTextField extends HTMLElement { const describedBy = [desc ? descId : "", error ? errorId : ""].filter(Boolean).join(" "); if (describedBy) control.setAttribute("aria-describedby", describedBy); } - if (label && control) (label as HTMLLabelElement).htmlFor = controlId; + if (label && control) label.htmlFor = controlId; if (desc) desc.id = descId; if (error) error.id = errorId; this.#syncAttrs(); - control?.addEventListener("input", this.#handleInput); + this.#cleanup = listen(control, [["input", this.#handleInput]]); } disconnectedCallback(): void { - this.#control()?.removeEventListener("input", this.#handleInput); + this.#cleanup(); } attributeChangedCallback(): void { @@ -40,7 +42,7 @@ export class PettyTextField extends HTMLElement { /** Display an error message on this field. */ setError(message: string): void { - const error = this.querySelector("[data-part=error]"); + const error = part(this, "error"); const control = this.#control(); if (error) error.textContent = message; if (control) control.setAttribute("aria-invalid", "true"); @@ -48,7 +50,7 @@ export class PettyTextField extends HTMLElement { /** Clear the error message on this field. */ clearError(): void { - const error = this.querySelector("[data-part=error]"); + const error = part(this, "error"); const control = this.#control(); if (error) error.textContent = ""; if (control) control.removeAttribute("aria-invalid"); diff --git a/packages/core/src/components/toggle-group/toggle-group-item.ts b/packages/core/src/components/toggle-group/toggle-group-item.ts index 594d9cc..4909569 100644 --- a/packages/core/src/components/toggle-group/toggle-group-item.ts +++ b/packages/core/src/components/toggle-group/toggle-group-item.ts @@ -1,4 +1,5 @@ import { wrapIndex } from "../../shared/keyboard"; +import { listen } from "../../shared/helpers"; /** PettyToggleGroupItem — single item within a toggle group. */ export class PettyToggleGroupItem extends HTMLElement { @@ -7,17 +8,17 @@ export class PettyToggleGroupItem extends HTMLElement { get value(): string { return this.getAttribute("value") ?? ""; } get disabled(): boolean { return this.hasAttribute("disabled"); } + #cleanup = (): void => {}; + connectedCallback(): void { this.setAttribute("role", "button"); this.setAttribute("tabindex", "0"); if (this.disabled) this.setAttribute("aria-disabled", "true"); - this.addEventListener("click", this.#handleClick); - this.addEventListener("keydown", this.#handleKeydown); + this.#cleanup = listen(this, [["click", this.#handleClick], ["keydown", this.#handleKeydown]]); } disconnectedCallback(): void { - this.removeEventListener("click", this.#handleClick); - this.removeEventListener("keydown", this.#handleKeydown); + this.#cleanup(); } attributeChangedCallback(name: string): void { @@ -39,21 +40,22 @@ export class PettyToggleGroupItem extends HTMLElement { this.#group()?.toggleValue(this.value); }; - #handleKeydown = (e: KeyboardEvent): void => { - if (e.key === " " || e.key === "Enter") { - e.preventDefault(); + #handleKeydown = (e: Event): void => { + const ke = e as KeyboardEvent; + if (ke.key === " " || ke.key === "Enter") { + ke.preventDefault(); this.#handleClick(); return; } const isHorizontal = this.closest("petty-toggle-group")?.getAttribute("orientation") !== "vertical"; const prev = isHorizontal ? "ArrowLeft" : "ArrowUp"; const next = isHorizontal ? "ArrowRight" : "ArrowDown"; - if (e.key !== prev && e.key !== next) return; - e.preventDefault(); + if (ke.key !== prev && ke.key !== next) return; + ke.preventDefault(); const items = this.#siblings(); const idx = items.indexOf(this); if (idx === -1) return; - const delta = e.key === next ? 1 : -1; + const delta = ke.key === next ? 1 : -1; items[wrapIndex(idx, delta, items.length)]?.focus(); }; } diff --git a/packages/core/src/components/virtual-list/virtual-list.ts b/packages/core/src/components/virtual-list/virtual-list.ts index 7725abf..7213960 100644 --- a/packages/core/src/components/virtual-list/virtual-list.ts +++ b/packages/core/src/components/virtual-list/virtual-list.ts @@ -1,4 +1,4 @@ -import { emit } from "../../shared/helpers"; +import { emit, listen } from "../../shared/helpers"; /** PettyVirtualList — windowed scroll rendering only visible items plus overscan. */ export class PettyVirtualList extends HTMLElement { diff --git a/packages/core/src/theme.css b/packages/core/src/theme.css index 593ae95..acc4149 100644 --- a/packages/core/src/theme.css +++ b/packages/core/src/theme.css @@ -46,7 +46,6 @@ dialog { dialog::backdrop { background: rgba(0 0 0 / 0.4); - backdrop-filter: blur(2px); } /* ── Select ─────────────────────────────────────────────────────────────── */ @@ -234,7 +233,13 @@ petty-toast-region { cursor: pointer; font-size: 1rem; line-height: 1; - padding: 0.125rem 0.25rem; + padding: 0.5rem; + min-width: 2.75rem; + min-height: 2.75rem; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: var(--petty-radius); } [data-part="toast-close"]:hover { color: var(--petty-text); } @@ -265,10 +270,11 @@ petty-form-field { width: 100%; } -[data-part="control"]:focus { +[data-part="control"]:focus-visible { border-color: var(--petty-border-focus); box-shadow: 0 0 0 3px rgba(37 99 235 / 0.15); - outline: none; + outline: 2px solid var(--petty-border-focus); + outline-offset: 2px; } [data-part="control"][aria-invalid="true"] { diff --git a/packages/showcase/index.html b/packages/showcase/index.html index 7e7972b..5f4b78a 100644 --- a/packages/showcase/index.html +++ b/packages/showcase/index.html @@ -7,7 +7,7 @@ PettyUI — Component Showcase - +