@aihu/primitives 0.0.12 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +14 -7
  2. package/dist/{button-C-8c-A17.js → button-Cha1gKlr.js} +2 -2
  3. package/dist/{button-C-8c-A17.js.map → button-Cha1gKlr.js.map} +1 -1
  4. package/dist/button.d.ts +1 -1
  5. package/dist/button.js +1 -1
  6. package/dist/checkbox.d.ts +45 -0
  7. package/dist/checkbox.d.ts.map +1 -0
  8. package/dist/checkbox.js +190 -0
  9. package/dist/checkbox.js.map +1 -0
  10. package/dist/{dialog-y7MHc6vf.js → dialog-VDL-W3Vy.js} +17 -7
  11. package/dist/{dialog-y7MHc6vf.js.map → dialog-VDL-W3Vy.js.map} +1 -1
  12. package/dist/dialog.d.ts +1 -1
  13. package/dist/dialog.js +1 -1
  14. package/dist/form-control-B_BO9j7_.js +223 -0
  15. package/dist/form-control-B_BO9j7_.js.map +1 -0
  16. package/dist/form-control.d.ts +2 -41
  17. package/dist/form-control.js +2 -145
  18. package/dist/index-BvFa1Y-Z.d.ts +60 -0
  19. package/dist/index-BvFa1Y-Z.d.ts.map +1 -0
  20. package/dist/{index-D9kf9rVU.d.ts → index-D7hJLfnb.d.ts} +10 -3
  21. package/dist/{index-D9kf9rVU.d.ts.map → index-D7hJLfnb.d.ts.map} +1 -1
  22. package/dist/{index-DPD4L6Nj.d.ts → index-yPv3StRL.d.ts} +1 -1
  23. package/dist/{index-DPD4L6Nj.d.ts.map → index-yPv3StRL.d.ts.map} +1 -1
  24. package/dist/index.d.ts +12 -4
  25. package/dist/index.js +12 -4
  26. package/dist/input.d.ts +13 -0
  27. package/dist/input.d.ts.map +1 -0
  28. package/dist/input.js +42 -0
  29. package/dist/input.js.map +1 -0
  30. package/dist/label.d.ts +24 -0
  31. package/dist/label.d.ts.map +1 -0
  32. package/dist/label.js +136 -0
  33. package/dist/label.js.map +1 -0
  34. package/dist/radio-group.d.ts +82 -0
  35. package/dist/radio-group.d.ts.map +1 -0
  36. package/dist/radio-group.js +298 -0
  37. package/dist/radio-group.js.map +1 -0
  38. package/dist/roving-focus.d.ts +4 -2
  39. package/dist/roving-focus.d.ts.map +1 -1
  40. package/dist/roving-focus.js +5 -3
  41. package/dist/roving-focus.js.map +1 -1
  42. package/dist/separator.d.ts +25 -0
  43. package/dist/separator.d.ts.map +1 -0
  44. package/dist/separator.js +85 -0
  45. package/dist/separator.js.map +1 -0
  46. package/dist/switch.d.ts +44 -0
  47. package/dist/switch.d.ts.map +1 -0
  48. package/dist/switch.js +176 -0
  49. package/dist/switch.js.map +1 -0
  50. package/dist/text-control-BBX7s8Oe.js +181 -0
  51. package/dist/text-control-BBX7s8Oe.js.map +1 -0
  52. package/dist/text-control-Brv5fUEX.d.ts +38 -0
  53. package/dist/text-control-Brv5fUEX.d.ts.map +1 -0
  54. package/dist/textarea.d.ts +13 -0
  55. package/dist/textarea.d.ts.map +1 -0
  56. package/dist/textarea.js +38 -0
  57. package/dist/textarea.js.map +1 -0
  58. package/dist/tooltip.d.ts +8 -2
  59. package/dist/tooltip.d.ts.map +1 -1
  60. package/dist/tooltip.js +15 -6
  61. package/dist/tooltip.js.map +1 -1
  62. package/package.json +33 -2
  63. package/dist/form-control.d.ts.map +0 -1
  64. package/dist/form-control.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"radio-group.js","names":[],"sources":["../src/radio-group/index.ts"],"sourcesContent":["/**\n * Headless radio group — `<aihu-radio-group-root>` (state owner, EXTENDS\n * `AihuRovingFocus`) + `<aihu-radio-group-item>` (role=\"radio\") +\n * presentational `<aihu-radio-group-indicator>`. Implements the WAI-ARIA APG\n * **Radio Group** pattern: `role=\"radiogroup\"` on the root, roving tabindex\n * across the items (exactly one `tabindex=\"0\"`), arrow keys move focus AND\n * select (orientation defaults to `\"both\"`, loop defaults ON for the APG\n * wrap), Space selects an unchecked item, Enter does NOT activate. Emits NO\n * CSS — each piece reflects `data-state=\"checked\" | \"unchecked\"`\n * (+ `data-disabled` presence) for the consumer to style.\n *\n * Form participation rides `attachHiddenInput` (ONE root-owned\n * visually-hidden native radio in the root's light DOM): submits\n * `name → value` only when a selection exists — no selection submits nothing\n * (native parity).\n *\n * `value-change` (detail `{ value: string }`, bubbles, composed) is dispatched\n * on USER-driven selection only (clicks/keys via the roving `setCurrent`\n * path) — programmatic `setValue()` / attribute writes do not emit (checkbox\n * precedent).\n */\n\nimport { effect, type Read, signal, untrack } from '@aihu/signals'\nimport { type CollectionContextValue, collectionContext } from '../collection/index.ts'\nimport { createDomContext, injectValue, provideContext } from '../dom-context.ts'\nimport {\n attachHiddenInput,\n type FormControlContextValue,\n formControlContext,\n} from '../form-control/index.ts'\nimport { AihuRovingFocus } from '../roving-focus/index.ts'\n\nexport interface RadioGroupContextValue {\n /** Currently selected item value; null when nothing is selected. */\n readonly value: Read<string | null>\n /** Group-effective disabled (own attribute ∥ inherited form-control). */\n readonly disabled: Read<boolean>\n readonly required: Read<boolean>\n /** Programmatic write: signal + reflected `value` attribute. NO event. */\n setValue(next: string | null): void\n /** USER-driven selection of `item` (click path): moves the tab stop to the\n * item WITHOUT stealing focus, selects its value, emits `value-change`. */\n selectItem(item: Element): void\n}\n\nexport const radioGroupContext = createDomContext<RadioGroupContextValue>('radio-group')\n\n/** Per-item context so `<aihu-radio-group-indicator>` mirrors its OWN item\n * (nearest-provider-wins walk lands on the enclosing item, not the root). */\nexport interface RadioGroupItemContextValue {\n readonly checked: Read<boolean>\n readonly disabled: Read<boolean>\n}\n\nexport const radioGroupItemContext =\n createDomContext<RadioGroupItemContextValue>('radio-group-item')\n\nexport class AihuRadioGroupRoot extends AihuRovingFocus {\n static override readonly observedAttributes = [\n ...AihuRovingFocus.observedAttributes,\n 'value',\n 'default-value',\n 'name',\n 'disabled',\n 'required',\n ]\n\n private readonly _value = signal<string | null>(null)\n private readonly _name = signal<string | null>(null)\n private readonly _groupDisabled = signal(false)\n private readonly _required = signal(false)\n private _fc: FormControlContextValue | null = null\n private _rgDisposers: Array<() => void> = []\n private _defaultSeeded = false\n\n constructor() {\n super()\n provideContext(this, radioGroupContext, {\n value: this._value[0],\n disabled: () => this._effectiveDisabled(),\n required: () => this._effectiveRequired(),\n setValue: (next) => this.setValue(next),\n selectItem: (item) => this._selectItem(item),\n })\n }\n\n get value(): Read<string | null> {\n return this._value[0]\n }\n\n /** Programmatic write: signal + reflected `value` attribute (two-way,\n * dialog open-attr pattern). Does NOT emit `value-change`. */\n setValue(next: string | null): void {\n if (next === this._value[0]()) return\n this._value[1](next)\n if (next === null) this.removeAttribute('value')\n else this.setAttribute('value', next)\n }\n\n /** Move the roving current index. With `focus = true` (the base keyboard\n * path — arrows/Home/End) this ALSO selects the landed item's value (APG:\n * moving focus in a radio group selects). `focus = false` moves the tab\n * stop silently and never selects. */\n override setCurrent(index: number, focus = true): void {\n super.setCurrent(index, focus)\n if (!focus) return\n const item = this.items()[this.currentIndex()]\n const v = item?.getAttribute('value') ?? null\n if (v === null) return // value-less items are focusable but unselectable\n this._userSelect(v)\n }\n\n override connectedCallback(): void {\n // APG defaults when the consumer set nothing: all four arrows work and\n // navigation wraps. Reflecting the attribute routes through the base's\n // attributeChangedCallback, so the base signals stay in sync.\n if (!this.hasAttribute('orientation')) this.setAttribute('orientation', 'both')\n if (!this.hasAttribute('loop')) this.setAttribute('loop', '')\n\n // Initial selection: the `value` attribute wins; otherwise `default-value`\n // seeds ONCE (signal only — never reflected). Checkbox precedent.\n if (this.hasAttribute('value')) {\n this._value[1](this.getAttribute('value'))\n } else if (!this._defaultSeeded && this.hasAttribute('default-value')) {\n this._value[1](this.getAttribute('default-value'))\n }\n this._defaultSeeded = true\n this._syncOwnAttrs()\n\n if (!this.hasAttribute('role')) this.setAttribute('role', 'radiogroup')\n // Discoverable by an ancestor <aihu-form-control> ([data-fc-control] is\n // first in its control selector, so the ROOT wins over the hidden input).\n this.setAttribute('data-fc-control', '')\n\n try {\n this._fc = injectValue(this, formControlContext)\n } catch {\n this._fc = null\n }\n\n super.connectedCallback()\n\n this._rgDisposers.push(\n // Group-level ARIA is root-owned. form-control also writes\n // aria-required/aria-disabled onto [data-fc-control], but this effect\n // tracks the SAME fc signals and re-asserts the effective (own ∥ fc)\n // superset — see accessibility.md.\n effect(() => {\n if (this._effectiveRequired()) this.setAttribute('aria-required', 'true')\n else this.removeAttribute('aria-required')\n if (this._effectiveDisabled()) {\n this.setAttribute('aria-disabled', 'true')\n this.setAttribute('data-disabled', '')\n } else {\n this.removeAttribute('aria-disabled')\n this.removeAttribute('data-disabled')\n }\n }),\n // Tab-stop-follows-checked: when a checked item exists and the current\n // tab stop is elsewhere, move the tab stop WITHOUT focusing (no focus\n // steal at mount). With nothing checked the base default (item 0) holds.\n effect(() => {\n const items = this.items()\n const v = this._value[0]()\n if (v === null) return\n const idx = items.findIndex((el) => el.getAttribute('value') === v)\n if (idx < 0) return\n // untrack: this effect WRITES currentIndex (via setCurrent), so its\n // read must not be a tracked dependency (circular-write guard).\n untrack(() => {\n if (idx !== this.currentIndex()) this.setCurrent(idx, false)\n })\n }),\n // ONE root-owned hidden radio: no selection ⇒ unchecked ⇒ submits\n // nothing (native parity).\n attachHiddenInput(this, {\n type: 'radio',\n name: this._name[0],\n value: () => this._value[0]() ?? '',\n checked: () => this._value[0]() !== null,\n required: () => this._effectiveRequired(),\n disabled: () => this._effectiveDisabled(),\n }),\n )\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n for (const d of this._rgDisposers) d()\n this._rgDisposers = []\n }\n\n override attributeChangedCallback(name: string, old: string | null, value: string | null): void {\n switch (name) {\n case 'value':\n this._value[1](value)\n break\n case 'name':\n this._name[1](value)\n break\n case 'disabled':\n this._groupDisabled[1](value !== null)\n break\n case 'required':\n this._required[1](value !== null)\n break\n case 'default-value':\n // Read once on connect — never reactive.\n break\n default:\n // orientation / loop / dir belong to the roving base.\n super.attributeChangedCallback(name, old, value)\n }\n }\n\n private _effectiveDisabled(): boolean {\n if (this._groupDisabled[0]()) return true\n return this._fc ? this._fc.disabled() : false\n }\n\n private _effectiveRequired(): boolean {\n if (this._required[0]()) return true\n return this._fc ? this._fc.required() : false\n }\n\n private _syncOwnAttrs(): void {\n this._groupDisabled[1](this.hasAttribute('disabled'))\n this._required[1](this.hasAttribute('required'))\n this._name[1](this.getAttribute('name'))\n }\n\n private _selectItem(item: Element): void {\n if (this._effectiveDisabled()) return\n const idx = this.items().indexOf(item)\n // Click already focuses the item naturally — move the tab stop only.\n if (idx >= 0) this.setCurrent(idx, false)\n const v = item.getAttribute('value')\n if (v !== null) this._userSelect(v)\n }\n\n private _userSelect(v: string): void {\n if (this._effectiveDisabled()) return\n if (v === this._value[0]()) return\n this.setValue(v)\n this.dispatchEvent(\n new CustomEvent<{ value: string }>('value-change', {\n detail: { value: v },\n bubbles: true,\n composed: true,\n }),\n )\n }\n}\n\nexport class AihuRadioGroupItem extends HTMLElement {\n static readonly observedAttributes = ['value', 'disabled']\n\n private readonly _itemValue = signal<string | null>(null)\n private readonly _disabled = signal(false)\n private _ctx: RadioGroupContextValue | null = null\n private _collection: CollectionContextValue | null = null\n private _unregister: (() => void) | null = null\n private _disposers: Array<() => void> = []\n\n constructor() {\n super()\n provideContext(this, radioGroupItemContext, {\n checked: () => this._checked(),\n disabled: () => this._effectiveDisabled(),\n })\n }\n\n connectedCallback(): void {\n this._itemValue[1](this.getAttribute('value'))\n this._disabled[1](this.hasAttribute('disabled'))\n if (!this.hasAttribute('role')) this.setAttribute('role', 'radio')\n\n try {\n this._ctx = injectValue(this, radioGroupContext)\n } catch {\n this._ctx = null\n }\n try {\n this._collection = injectValue(this, collectionContext)\n } catch {\n this._collection = null\n }\n\n this.addEventListener('keydown', this._onKeydown)\n // Capture so disabled suppression beats consumer handlers (checkbox/button\n // precedent).\n this.addEventListener('click', this._onClickCapture, true)\n\n this._disposers.push(\n effect(() => {\n const checked = this._checked()\n this.setAttribute('aria-checked', checked ? 'true' : 'false')\n this.setAttribute('data-state', checked ? 'checked' : 'unchecked')\n if (this._effectiveDisabled()) {\n // Item-level aria-disabled is item-owned: form-control targets the\n // ROOT ([data-fc-control]), never the items (see accessibility.md).\n this.setAttribute('aria-disabled', 'true')\n this.setAttribute('data-disabled', '')\n } else {\n this.removeAttribute('aria-disabled')\n this.removeAttribute('data-disabled')\n }\n }),\n // Roving registration tracks disabled: a disabled item leaves the\n // collection so arrows skip it entirely; re-enabling re-registers.\n effect(() => {\n if (this._effectiveDisabled()) {\n this._unregister?.()\n this._unregister = null\n this.setAttribute('tabindex', '-1')\n } else if (this._unregister === null && this._collection !== null) {\n this._unregister = this._collection.register(this)\n }\n }),\n )\n }\n\n disconnectedCallback(): void {\n this.removeEventListener('keydown', this._onKeydown)\n this.removeEventListener('click', this._onClickCapture, true)\n this._unregister?.()\n this._unregister = null\n for (const d of this._disposers) d()\n this._disposers = []\n }\n\n attributeChangedCallback(name: string, _old: string | null, value: string | null): void {\n if (name === 'value') this._itemValue[1](value)\n if (name === 'disabled') this._disabled[1](value !== null)\n }\n\n private _checked(): boolean {\n const v = this._itemValue[0]()\n return v !== null && this._ctx !== null && this._ctx.value() === v\n }\n\n private _effectiveDisabled(): boolean {\n if (this._disabled[0]()) return true\n return this._ctx ? this._ctx.disabled() : false\n }\n\n private readonly _onKeydown = (ev: KeyboardEvent): void => {\n if (ev.key === 'Enter') {\n // APG: Enter does NOT activate a radio (reserved for form submission).\n ev.preventDefault()\n return\n }\n if (ev.key === ' ' || ev.key === 'Spacebar') {\n ev.preventDefault() // stop page scroll\n if (this._effectiveDisabled()) return\n // APG: Space selects the focused item when unchecked (no toggle-off).\n if (!this._checked()) this.click()\n }\n }\n\n private readonly _onClickCapture = (ev: Event): void => {\n if (this._effectiveDisabled()) {\n ev.preventDefault()\n ev.stopImmediatePropagation()\n return\n }\n this._ctx?.selectItem(this)\n }\n}\n\n/** Presentational styling hook: mirrors its enclosing ITEM's state (nearest\n * `radioGroupItemContext` provider), hidden from AT. */\nexport class AihuRadioGroupIndicator extends HTMLElement {\n private _disposers: Array<() => void> = []\n\n connectedCallback(): void {\n const ctx = injectValue(this, radioGroupItemContext)\n this.setAttribute('aria-hidden', 'true')\n this._disposers.push(\n effect(() => {\n this.setAttribute('data-state', ctx.checked() ? 'checked' : 'unchecked')\n if (ctx.disabled()) this.setAttribute('data-disabled', '')\n else this.removeAttribute('data-disabled')\n }),\n )\n }\n\n disconnectedCallback(): void {\n for (const d of this._disposers) d()\n this._disposers = []\n }\n}\n\nconst REGISTRY: Array<[string, CustomElementConstructor]> = [\n ['aihu-radio-group-root', AihuRadioGroupRoot],\n ['aihu-radio-group-item', AihuRadioGroupItem],\n ['aihu-radio-group-indicator', AihuRadioGroupIndicator],\n]\n\nlet _defined = false\n/** Register all radio-group custom elements (idempotent). */\nexport function defineRadioGroup(): void {\n if (_defined) return\n for (const [tag, ctor] of REGISTRY) {\n if (!customElements.get(tag)) customElements.define(tag, ctor)\n }\n _defined = true\n}\n"],"mappings":"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"}
@@ -19,8 +19,10 @@ declare class AihuRovingFocus extends HTMLElement {
19
19
  attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
20
20
  /** Register a focusable descendant; returns an unregister disposer. */
21
21
  register(el: Element): () => void;
22
- /** Move the current index to `index` (clamped/looped) and focus the item. */
23
- setCurrent(index: number): void;
22
+ /** Move the current index to `index` (clamped/looped) and focus the item.
23
+ * Pass `focus = false` to move the tab stop (tabindex bookkeeping) without
24
+ * focusing the element. */
25
+ setCurrent(index: number, focus?: boolean): void;
24
26
  /** Resolve the effective direction (own attr, else nearest config-provider). */
25
27
  private _direction;
26
28
  private readonly _onKeydown;
@@ -1 +1 @@
1
- {"version":3,"file":"roving-focus.d.ts","names":[],"sources":["../src/roving-focus/index.ts"],"mappings":";;;KAwBY,WAAA;AAAA,cAEC,eAAA,SAAwB,WAAA;EAAA,gBACnB,kBAAA;EAAA,iBAEC,WAAA;EAAA,iBACA,aAAA;EAAA,iBACA,KAAA;EAAA,iBACA,YAAA;EAAA,iBACA,QAAA;EAAA,SAER,QAAA,EAAU,IAAA;EAAA,QAEX,UAAA;EAAA,IAEJ,YAAA,CAAA,GAAgB,IAAA;EAAA,IAGhB,KAAA,CAAA,GAAS,IAAA,CAAK,OAAA;;EAelB,iBAAA,CAAA;EAgBA,oBAAA,CAAA;EAMA,wBAAA,CAAyB,IAAA,UAAc,IAAA,iBAAqB,KAAA;EAtB5D;EAsCA,QAAA,CAAS,EAAA,EAAI,OAAA;EAhBb;EAqBA,UAAA,CAAW,KAAA;EArB4B;EAAA,QAmC/B,UAAA;EAAA,iBAUS,UAAA;EAAA,QAqCT,cAAA;AAAA;;iBAWM,iBAAA,CAAkB,GAAA"}
1
+ {"version":3,"file":"roving-focus.d.ts","names":[],"sources":["../src/roving-focus/index.ts"],"mappings":";;;KAwBY,WAAA;AAAA,cAEC,eAAA,SAAwB,WAAA;EAAA,gBACnB,kBAAA;EAAA,iBAEC,WAAA;EAAA,iBACA,aAAA;EAAA,iBACA,KAAA;EAAA,iBACA,YAAA;EAAA,iBACA,QAAA;EAAA,SAER,QAAA,EAAU,IAAA;EAAA,QAEX,UAAA;EAAA,IAEJ,YAAA,CAAA,GAAgB,IAAA;EAAA,IAGhB,KAAA,CAAA,GAAS,IAAA,CAAK,OAAA;;EAelB,iBAAA,CAAA;EAgBA,oBAAA,CAAA;EAMA,wBAAA,CAAyB,IAAA,UAAc,IAAA,iBAAqB,KAAA;EAtB5D;EAsCA,QAAA,CAAS,EAAA,EAAI,OAAA;EAhBb;;;EAuBA,UAAA,CAAW,KAAA,UAAe,KAAA;EAP1B;EAAA,QAqBQ,UAAA;EAAA,iBAUS,UAAA;EAAA,QAqCT,cAAA;AAAA;;iBAWM,iBAAA,CAAkB,GAAA"}
@@ -81,15 +81,17 @@ var AihuRovingFocus = class extends HTMLElement {
81
81
  register(el) {
82
82
  return this._collection.register(el);
83
83
  }
84
- /** Move the current index to `index` (clamped/looped) and focus the item. */
85
- setCurrent(index) {
84
+ /** Move the current index to `index` (clamped/looped) and focus the item.
85
+ * Pass `focus = false` to move the tab stop (tabindex bookkeeping) without
86
+ * focusing the element. */
87
+ setCurrent(index, focus = true) {
86
88
  const items = this._collection.items();
87
89
  if (items.length === 0) return;
88
90
  let next = index;
89
91
  if (this._loop[0]()) next = (index % items.length + items.length) % items.length;
90
92
  else next = Math.max(0, Math.min(items.length - 1, index));
91
93
  this._currentIndex[1](next);
92
- items[next].focus?.();
94
+ if (focus) items[next].focus?.();
93
95
  }
94
96
  /** Resolve the effective direction (own attr, else nearest config-provider). */
95
97
  _direction() {
@@ -1 +1 @@
1
- {"version":3,"file":"roving-focus.js","names":[],"sources":["../src/roving-focus/index.ts"],"sourcesContent":["/**\n * `<aihu-roving-focus>` — WAI-ARIA roving-tabindex container. Exactly one item\n * carries `tabindex=\"0\"` (the current); all others `tabindex=\"-1\"`. Arrow keys\n * move the current index (respecting `orientation` + `loop` + RTL `dir`),\n * Home/End jump to the first/last item, and `element.focus()` follows. It\n * imposes NO role (the consumer sets `role=\"toolbar\"`/`\"menu\"`/etc.) and ships\n * NO CSS.\n *\n * Items are enumerated via the `collection` substrate (Task 8): each focusable\n * descendant registers itself; the container reuses `createCollection()` so\n * item order tracks the DOM.\n *\n * Reflected attributes: `orientation` (`\"horizontal\" | \"vertical\" | \"both\"`),\n * `loop` (boolean), `dir` (`\"ltr\" | \"rtl\"` — inherited from a `config-provider`\n * ancestor if present).\n * Owned signals: `currentIndex`, `items` (from the collection), `activeId`\n * (computed).\n */\n\nimport { computed, effect, type Read, signal } from '@aihu/signals'\nimport { collectionContext, createCollection } from '../collection/index.ts'\nimport { configContext, type Direction } from '../config-provider/index.ts'\nimport { injectValue, provideContext } from '../dom-context.ts'\n\nexport type Orientation = 'horizontal' | 'vertical' | 'both'\n\nexport class AihuRovingFocus extends HTMLElement {\n static readonly observedAttributes = ['orientation', 'loop', 'dir']\n\n private readonly _collection = createCollection()\n private readonly _currentIndex = signal(0)\n private readonly _loop = signal(false)\n private readonly _orientation = signal<Orientation>('horizontal')\n private readonly _dirAttr = signal<Direction | null>(null)\n\n readonly activeId: Read<string | null>\n\n private _disposers: Array<() => void> = []\n\n get currentIndex(): Read<number> {\n return this._currentIndex[0]\n }\n get items(): Read<Element[]> {\n return this._collection.items\n }\n\n constructor() {\n super()\n // Reuse the collection substrate for descendant enumeration.\n provideContext(this, collectionContext, this._collection)\n this.activeId = computed(() => {\n const items = this._collection.items()\n const el = items[this._currentIndex[0]()]\n return el ? el.id || null : null\n })\n }\n\n connectedCallback(): void {\n this._syncFromAttrs()\n this.addEventListener('keydown', this._onKeydown)\n\n // Stamp roving tabindex whenever items or currentIndex change.\n this._disposers.push(\n effect(() => {\n const items = this._collection.items()\n const current = this._currentIndex[0]()\n items.forEach((el, i) => {\n el.setAttribute('tabindex', i === current ? '0' : '-1')\n })\n }),\n )\n }\n\n disconnectedCallback(): void {\n this.removeEventListener('keydown', this._onKeydown)\n for (const d of this._disposers) d()\n this._disposers = []\n }\n\n attributeChangedCallback(name: string, _old: string | null, value: string | null): void {\n switch (name) {\n case 'orientation':\n if (value === 'horizontal' || value === 'vertical' || value === 'both')\n this._orientation[1](value)\n break\n case 'loop':\n this._loop[1](value !== null)\n break\n case 'dir':\n this._dirAttr[1](value === 'rtl' ? 'rtl' : value === 'ltr' ? 'ltr' : null)\n break\n }\n }\n\n /** Register a focusable descendant; returns an unregister disposer. */\n register(el: Element): () => void {\n return this._collection.register(el)\n }\n\n /** Move the current index to `index` (clamped/looped) and focus the item. */\n setCurrent(index: number): void {\n const items = this._collection.items()\n if (items.length === 0) return\n let next = index\n if (this._loop[0]()) {\n next = ((index % items.length) + items.length) % items.length\n } else {\n next = Math.max(0, Math.min(items.length - 1, index))\n }\n this._currentIndex[1](next)\n ;(items[next] as HTMLElement).focus?.()\n }\n\n /** Resolve the effective direction (own attr, else nearest config-provider). */\n private _direction(): Direction {\n const own = this._dirAttr[0]()\n if (own) return own\n try {\n return injectValue(this, configContext).dir()\n } catch {\n return 'ltr'\n }\n }\n\n private readonly _onKeydown = (ev: KeyboardEvent): void => {\n const orientation = this._orientation[0]()\n const horizontal = orientation === 'horizontal' || orientation === 'both'\n const vertical = orientation === 'vertical' || orientation === 'both'\n const rtl = this._direction() === 'rtl'\n const current = this._currentIndex[0]()\n\n let handled = true\n switch (ev.key) {\n case 'ArrowRight':\n if (horizontal) this.setCurrent(current + (rtl ? -1 : 1))\n else handled = false\n break\n case 'ArrowLeft':\n if (horizontal) this.setCurrent(current + (rtl ? 1 : -1))\n else handled = false\n break\n case 'ArrowDown':\n if (vertical) this.setCurrent(current + 1)\n else handled = false\n break\n case 'ArrowUp':\n if (vertical) this.setCurrent(current - 1)\n else handled = false\n break\n case 'Home':\n this.setCurrent(0)\n break\n case 'End':\n this.setCurrent(this._collection.items().length - 1)\n break\n default:\n handled = false\n }\n if (handled) ev.preventDefault()\n }\n\n private _syncFromAttrs(): void {\n const o = this.getAttribute('orientation')\n if (o === 'horizontal' || o === 'vertical' || o === 'both') this._orientation[1](o)\n this._loop[1](this.hasAttribute('loop'))\n const dir = this.getAttribute('dir')\n this._dirAttr[1](dir === 'rtl' ? 'rtl' : dir === 'ltr' ? 'ltr' : null)\n }\n}\n\nlet _defined = false\n/** Register `<aihu-roving-focus>` (idempotent). */\nexport function defineRovingFocus(tag = 'aihu-roving-focus'): void {\n if (_defined || customElements.get(tag)) {\n _defined = true\n return\n }\n customElements.define(tag, AihuRovingFocus)\n _defined = true\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"roving-focus.js","names":[],"sources":["../src/roving-focus/index.ts"],"sourcesContent":["/**\n * `<aihu-roving-focus>` — WAI-ARIA roving-tabindex container. Exactly one item\n * carries `tabindex=\"0\"` (the current); all others `tabindex=\"-1\"`. Arrow keys\n * move the current index (respecting `orientation` + `loop` + RTL `dir`),\n * Home/End jump to the first/last item, and `element.focus()` follows. It\n * imposes NO role (the consumer sets `role=\"toolbar\"`/`\"menu\"`/etc.) and ships\n * NO CSS.\n *\n * Items are enumerated via the `collection` substrate (Task 8): each focusable\n * descendant registers itself; the container reuses `createCollection()` so\n * item order tracks the DOM.\n *\n * Reflected attributes: `orientation` (`\"horizontal\" | \"vertical\" | \"both\"`),\n * `loop` (boolean), `dir` (`\"ltr\" | \"rtl\"` — inherited from a `config-provider`\n * ancestor if present).\n * Owned signals: `currentIndex`, `items` (from the collection), `activeId`\n * (computed).\n */\n\nimport { computed, effect, type Read, signal } from '@aihu/signals'\nimport { collectionContext, createCollection } from '../collection/index.ts'\nimport { configContext, type Direction } from '../config-provider/index.ts'\nimport { injectValue, provideContext } from '../dom-context.ts'\n\nexport type Orientation = 'horizontal' | 'vertical' | 'both'\n\nexport class AihuRovingFocus extends HTMLElement {\n static readonly observedAttributes = ['orientation', 'loop', 'dir']\n\n private readonly _collection = createCollection()\n private readonly _currentIndex = signal(0)\n private readonly _loop = signal(false)\n private readonly _orientation = signal<Orientation>('horizontal')\n private readonly _dirAttr = signal<Direction | null>(null)\n\n readonly activeId: Read<string | null>\n\n private _disposers: Array<() => void> = []\n\n get currentIndex(): Read<number> {\n return this._currentIndex[0]\n }\n get items(): Read<Element[]> {\n return this._collection.items\n }\n\n constructor() {\n super()\n // Reuse the collection substrate for descendant enumeration.\n provideContext(this, collectionContext, this._collection)\n this.activeId = computed(() => {\n const items = this._collection.items()\n const el = items[this._currentIndex[0]()]\n return el ? el.id || null : null\n })\n }\n\n connectedCallback(): void {\n this._syncFromAttrs()\n this.addEventListener('keydown', this._onKeydown)\n\n // Stamp roving tabindex whenever items or currentIndex change.\n this._disposers.push(\n effect(() => {\n const items = this._collection.items()\n const current = this._currentIndex[0]()\n items.forEach((el, i) => {\n el.setAttribute('tabindex', i === current ? '0' : '-1')\n })\n }),\n )\n }\n\n disconnectedCallback(): void {\n this.removeEventListener('keydown', this._onKeydown)\n for (const d of this._disposers) d()\n this._disposers = []\n }\n\n attributeChangedCallback(name: string, _old: string | null, value: string | null): void {\n switch (name) {\n case 'orientation':\n if (value === 'horizontal' || value === 'vertical' || value === 'both')\n this._orientation[1](value)\n break\n case 'loop':\n this._loop[1](value !== null)\n break\n case 'dir':\n this._dirAttr[1](value === 'rtl' ? 'rtl' : value === 'ltr' ? 'ltr' : null)\n break\n }\n }\n\n /** Register a focusable descendant; returns an unregister disposer. */\n register(el: Element): () => void {\n return this._collection.register(el)\n }\n\n /** Move the current index to `index` (clamped/looped) and focus the item.\n * Pass `focus = false` to move the tab stop (tabindex bookkeeping) without\n * focusing the element. */\n setCurrent(index: number, focus = true): void {\n const items = this._collection.items()\n if (items.length === 0) return\n let next = index\n if (this._loop[0]()) {\n next = ((index % items.length) + items.length) % items.length\n } else {\n next = Math.max(0, Math.min(items.length - 1, index))\n }\n this._currentIndex[1](next)\n if (focus) (items[next] as HTMLElement).focus?.()\n }\n\n /** Resolve the effective direction (own attr, else nearest config-provider). */\n private _direction(): Direction {\n const own = this._dirAttr[0]()\n if (own) return own\n try {\n return injectValue(this, configContext).dir()\n } catch {\n return 'ltr'\n }\n }\n\n private readonly _onKeydown = (ev: KeyboardEvent): void => {\n const orientation = this._orientation[0]()\n const horizontal = orientation === 'horizontal' || orientation === 'both'\n const vertical = orientation === 'vertical' || orientation === 'both'\n const rtl = this._direction() === 'rtl'\n const current = this._currentIndex[0]()\n\n let handled = true\n switch (ev.key) {\n case 'ArrowRight':\n if (horizontal) this.setCurrent(current + (rtl ? -1 : 1))\n else handled = false\n break\n case 'ArrowLeft':\n if (horizontal) this.setCurrent(current + (rtl ? 1 : -1))\n else handled = false\n break\n case 'ArrowDown':\n if (vertical) this.setCurrent(current + 1)\n else handled = false\n break\n case 'ArrowUp':\n if (vertical) this.setCurrent(current - 1)\n else handled = false\n break\n case 'Home':\n this.setCurrent(0)\n break\n case 'End':\n this.setCurrent(this._collection.items().length - 1)\n break\n default:\n handled = false\n }\n if (handled) ev.preventDefault()\n }\n\n private _syncFromAttrs(): void {\n const o = this.getAttribute('orientation')\n if (o === 'horizontal' || o === 'vertical' || o === 'both') this._orientation[1](o)\n this._loop[1](this.hasAttribute('loop'))\n const dir = this.getAttribute('dir')\n this._dirAttr[1](dir === 'rtl' ? 'rtl' : dir === 'ltr' ? 'ltr' : null)\n }\n}\n\nlet _defined = false\n/** Register `<aihu-roving-focus>` (idempotent). */\nexport function defineRovingFocus(tag = 'aihu-roving-focus'): void {\n if (_defined || customElements.get(tag)) {\n _defined = true\n return\n }\n customElements.define(tag, AihuRovingFocus)\n _defined = true\n}\n"],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { Read } from "@aihu/signals";
2
+
3
+ //#region src/separator/index.d.ts
4
+ type SeparatorOrientation = 'horizontal' | 'vertical';
5
+ declare class AihuSeparator extends HTMLElement {
6
+ static readonly observedAttributes: string[];
7
+ private readonly _orientation;
8
+ private readonly _decorative;
9
+ private _disposers;
10
+ /** Consumer-supplied `role` captured on first connect (never overridden
11
+ * while non-decorative). */
12
+ private _consumerRole;
13
+ private _roleCaptured;
14
+ get orientation(): Read<SeparatorOrientation>;
15
+ get decorative(): Read<boolean>;
16
+ connectedCallback(): void;
17
+ disconnectedCallback(): void;
18
+ attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
19
+ private _syncFromAttrs;
20
+ }
21
+ /** Register `<aihu-separator>` (idempotent). */
22
+ declare function defineSeparator(tag?: string): void;
23
+ //#endregion
24
+ export { AihuSeparator, SeparatorOrientation, defineSeparator };
25
+ //# sourceMappingURL=separator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"separator.d.ts","names":[],"sources":["../src/separator/index.ts"],"mappings":";;;KAuBY,oBAAA;AAAA,cAEC,aAAA,SAAsB,WAAA;EAAA,gBACjB,kBAAA;EAAA,iBAEC,YAAA;EAAA,iBACA,WAAA;EAAA,QACT,UAAA;EAUJ;;EAAA,QANI,aAAA;EAAA,QACA,aAAA;EAAA,IAEJ,WAAA,CAAA,GAAe,IAAA,CAAK,oBAAA;EAAA,IAGpB,UAAA,CAAA,GAAc,IAAA;EAIlB,iBAAA,CAAA;EA4BA,oBAAA,CAAA;EAKA,wBAAA,CAAyB,IAAA,UAAc,IAAA,iBAAqB,KAAA;EAAA,QAWpD,cAAA;AAAA;AAUV;AAAA,iBAAgB,eAAA,CAAgB,GAAA"}
@@ -0,0 +1,85 @@
1
+ import { effect, signal } from "@aihu/signals";
2
+ //#region src/separator/index.ts
3
+ /**
4
+ * `<aihu-separator>` — headless separator (WAI-ARIA APG Separator, static
5
+ * variant). Renders nothing and ships NO CSS: it emits role + ARIA +
6
+ * `data-orientation` for the consumer to style.
7
+ *
8
+ * Behavior:
9
+ * - `role="separator"` unless `decorative` is present, in which case
10
+ * `role="none"` (the element is purely visual and removed from the
11
+ * accessibility tree). A consumer-supplied `role` attribute is respected
12
+ * for the non-decorative case.
13
+ * - `aria-orientation="vertical"` ONLY when `orientation="vertical"` —
14
+ * horizontal is the ARIA default for separators, so the attribute is
15
+ * removed for horizontal (Radix parity).
16
+ * - `data-orientation` always reflects the effective orientation.
17
+ * - Static (non-focusable, no keyboard handlers): the APG focusable-separator
18
+ * variant (window splitter) is out of scope.
19
+ *
20
+ * Reflected attributes: `orientation` (`"horizontal"` default | `"vertical"`),
21
+ * `decorative` (boolean presence). Both are reactive at runtime.
22
+ */
23
+ var AihuSeparator = class extends HTMLElement {
24
+ static observedAttributes = ["orientation", "decorative"];
25
+ _orientation = signal("horizontal");
26
+ _decorative = signal(false);
27
+ _disposers = [];
28
+ /** Consumer-supplied `role` captured on first connect (never overridden
29
+ * while non-decorative). */
30
+ _consumerRole = null;
31
+ _roleCaptured = false;
32
+ get orientation() {
33
+ return this._orientation[0];
34
+ }
35
+ get decorative() {
36
+ return this._decorative[0];
37
+ }
38
+ connectedCallback() {
39
+ this._syncFromAttrs();
40
+ if (!this._roleCaptured) {
41
+ this._consumerRole = this.getAttribute("role");
42
+ this._roleCaptured = true;
43
+ }
44
+ this._disposers.push(effect(() => {
45
+ const decorative = this._decorative[0]();
46
+ const orientation = this._orientation[0]();
47
+ this.setAttribute("role", decorative ? "none" : this._consumerRole ?? "separator");
48
+ if (!decorative && orientation === "vertical") this.setAttribute("aria-orientation", "vertical");
49
+ else this.removeAttribute("aria-orientation");
50
+ this.setAttribute("data-orientation", orientation);
51
+ }));
52
+ }
53
+ disconnectedCallback() {
54
+ for (const d of this._disposers) d();
55
+ this._disposers = [];
56
+ }
57
+ attributeChangedCallback(name, _old, value) {
58
+ switch (name) {
59
+ case "orientation":
60
+ this._orientation[1](value === "vertical" ? "vertical" : "horizontal");
61
+ break;
62
+ case "decorative":
63
+ this._decorative[1](value !== null);
64
+ break;
65
+ }
66
+ }
67
+ _syncFromAttrs() {
68
+ this._orientation[1](this.getAttribute("orientation") === "vertical" ? "vertical" : "horizontal");
69
+ this._decorative[1](this.hasAttribute("decorative"));
70
+ }
71
+ };
72
+ let _defined = false;
73
+ /** Register `<aihu-separator>` (idempotent). */
74
+ function defineSeparator(tag = "aihu-separator") {
75
+ if (_defined || customElements.get(tag)) {
76
+ _defined = true;
77
+ return;
78
+ }
79
+ customElements.define(tag, AihuSeparator);
80
+ _defined = true;
81
+ }
82
+ //#endregion
83
+ export { AihuSeparator, defineSeparator };
84
+
85
+ //# sourceMappingURL=separator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"separator.js","names":[],"sources":["../src/separator/index.ts"],"sourcesContent":["/**\n * `<aihu-separator>` — headless separator (WAI-ARIA APG Separator, static\n * variant). Renders nothing and ships NO CSS: it emits role + ARIA +\n * `data-orientation` for the consumer to style.\n *\n * Behavior:\n * - `role=\"separator\"` unless `decorative` is present, in which case\n * `role=\"none\"` (the element is purely visual and removed from the\n * accessibility tree). A consumer-supplied `role` attribute is respected\n * for the non-decorative case.\n * - `aria-orientation=\"vertical\"` ONLY when `orientation=\"vertical\"` —\n * horizontal is the ARIA default for separators, so the attribute is\n * removed for horizontal (Radix parity).\n * - `data-orientation` always reflects the effective orientation.\n * - Static (non-focusable, no keyboard handlers): the APG focusable-separator\n * variant (window splitter) is out of scope.\n *\n * Reflected attributes: `orientation` (`\"horizontal\"` default | `\"vertical\"`),\n * `decorative` (boolean presence). Both are reactive at runtime.\n */\n\nimport { effect, type Read, signal } from '@aihu/signals'\n\nexport type SeparatorOrientation = 'horizontal' | 'vertical'\n\nexport class AihuSeparator extends HTMLElement {\n static readonly observedAttributes = ['orientation', 'decorative']\n\n private readonly _orientation = signal<SeparatorOrientation>('horizontal')\n private readonly _decorative = signal(false)\n private _disposers: Array<() => void> = []\n\n /** Consumer-supplied `role` captured on first connect (never overridden\n * while non-decorative). */\n private _consumerRole: string | null = null\n private _roleCaptured = false\n\n get orientation(): Read<SeparatorOrientation> {\n return this._orientation[0]\n }\n get decorative(): Read<boolean> {\n return this._decorative[0]\n }\n\n connectedCallback(): void {\n this._syncFromAttrs()\n\n if (!this._roleCaptured) {\n this._consumerRole = this.getAttribute('role')\n this._roleCaptured = true\n }\n\n this._disposers.push(\n effect(() => {\n const decorative = this._decorative[0]()\n const orientation = this._orientation[0]()\n\n this.setAttribute('role', decorative ? 'none' : (this._consumerRole ?? 'separator'))\n\n // Horizontal is the ARIA default for separators — only stamp the\n // attribute when vertical (and never on a decorative separator).\n if (!decorative && orientation === 'vertical') {\n this.setAttribute('aria-orientation', 'vertical')\n } else {\n this.removeAttribute('aria-orientation')\n }\n\n this.setAttribute('data-orientation', orientation)\n }),\n )\n }\n\n disconnectedCallback(): void {\n for (const d of this._disposers) d()\n this._disposers = []\n }\n\n attributeChangedCallback(name: string, _old: string | null, value: string | null): void {\n switch (name) {\n case 'orientation':\n this._orientation[1](value === 'vertical' ? 'vertical' : 'horizontal')\n break\n case 'decorative':\n this._decorative[1](value !== null)\n break\n }\n }\n\n private _syncFromAttrs(): void {\n this._orientation[1](\n this.getAttribute('orientation') === 'vertical' ? 'vertical' : 'horizontal',\n )\n this._decorative[1](this.hasAttribute('decorative'))\n }\n}\n\nlet _defined = false\n/** Register `<aihu-separator>` (idempotent). */\nexport function defineSeparator(tag = 'aihu-separator'): void {\n if (_defined || customElements.get(tag)) {\n _defined = true\n return\n }\n customElements.define(tag, AihuSeparator)\n _defined = true\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAyBA,IAAa,gBAAb,cAAmC,YAAY;CAC7C,OAAgB,qBAAqB,CAAC,eAAe,aAAa;CAElE,eAAgC,OAA6B,aAAa;CAC1E,cAA+B,OAAO,MAAM;CAC5C,aAAwC,EAAE;;;CAI1C,gBAAuC;CACvC,gBAAwB;CAExB,IAAI,cAA0C;EAC5C,OAAO,KAAK,aAAa;;CAE3B,IAAI,aAA4B;EAC9B,OAAO,KAAK,YAAY;;CAG1B,oBAA0B;EACxB,KAAK,gBAAgB;EAErB,IAAI,CAAC,KAAK,eAAe;GACvB,KAAK,gBAAgB,KAAK,aAAa,OAAO;GAC9C,KAAK,gBAAgB;;EAGvB,KAAK,WAAW,KACd,aAAa;GACX,MAAM,aAAa,KAAK,YAAY,IAAI;GACxC,MAAM,cAAc,KAAK,aAAa,IAAI;GAE1C,KAAK,aAAa,QAAQ,aAAa,SAAU,KAAK,iBAAiB,YAAa;GAIpF,IAAI,CAAC,cAAc,gBAAgB,YACjC,KAAK,aAAa,oBAAoB,WAAW;QAEjD,KAAK,gBAAgB,mBAAmB;GAG1C,KAAK,aAAa,oBAAoB,YAAY;IAClD,CACH;;CAGH,uBAA6B;EAC3B,KAAK,MAAM,KAAK,KAAK,YAAY,GAAG;EACpC,KAAK,aAAa,EAAE;;CAGtB,yBAAyB,MAAc,MAAqB,OAA4B;EACtF,QAAQ,MAAR;GACE,KAAK;IACH,KAAK,aAAa,GAAG,UAAU,aAAa,aAAa,aAAa;IACtE;GACF,KAAK;IACH,KAAK,YAAY,GAAG,UAAU,KAAK;IACnC;;;CAIN,iBAA+B;EAC7B,KAAK,aAAa,GAChB,KAAK,aAAa,cAAc,KAAK,aAAa,aAAa,aAChE;EACD,KAAK,YAAY,GAAG,KAAK,aAAa,aAAa,CAAC;;;AAIxD,IAAI,WAAW;;AAEf,SAAgB,gBAAgB,MAAM,kBAAwB;CAC5D,IAAI,YAAY,eAAe,IAAI,IAAI,EAAE;EACvC,WAAW;EACX;;CAEF,eAAe,OAAO,KAAK,cAAc;CACzC,WAAW"}
@@ -0,0 +1,44 @@
1
+ import { DomContext } from "./dom-context.js";
2
+ import { Read } from "@aihu/signals";
3
+
4
+ //#region src/switch/index.d.ts
5
+ interface SwitchContextValue {
6
+ readonly checked: Read<boolean>;
7
+ readonly disabled: Read<boolean>;
8
+ }
9
+ declare const switchContext: DomContext<SwitchContextValue>;
10
+ declare class AihuSwitchRoot extends HTMLElement {
11
+ static readonly observedAttributes: string[];
12
+ private readonly _checked;
13
+ private readonly _disabled;
14
+ private readonly _required;
15
+ private readonly _name;
16
+ private readonly _value;
17
+ private _fc;
18
+ private _disposers;
19
+ private _defaultSeeded;
20
+ constructor();
21
+ get checked(): Read<boolean>;
22
+ /** Programmatic write: signal + reflected `checked` attribute (two-way,
23
+ * dialog open-attr pattern). Does NOT emit `checked-change`. */
24
+ setChecked(next: boolean): void;
25
+ connectedCallback(): void;
26
+ disconnectedCallback(): void;
27
+ attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
28
+ private _effectiveDisabled;
29
+ private _effectiveRequired;
30
+ private _syncFromAttrs;
31
+ private readonly _onKeydown;
32
+ private readonly _onClickCapture;
33
+ }
34
+ /** Presentational styling hook: mirrors the root's state, hidden from AT. */
35
+ declare class AihuSwitchThumb extends HTMLElement {
36
+ private _disposers;
37
+ connectedCallback(): void;
38
+ disconnectedCallback(): void;
39
+ }
40
+ /** Register all switch custom elements (idempotent). */
41
+ declare function defineSwitch(): void;
42
+ //#endregion
43
+ export { AihuSwitchRoot, AihuSwitchThumb, SwitchContextValue, defineSwitch, switchContext };
44
+ //# sourceMappingURL=switch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"switch.d.ts","names":[],"sources":["../src/switch/index.ts"],"mappings":";;;;UAuBiB,kBAAA;EAAA,SACN,OAAA,EAAS,IAAA;EAAA,SACT,QAAA,EAAU,IAAA;AAAA;AAAA,cAGR,aAAA,EAAa,UAAA,CAAA,kBAAA;AAAA,cAEb,cAAA,SAAuB,WAAA;EAAA,gBAClB,kBAAA;EAAA,iBASC,QAAA;EAAA,iBACA,SAAA;EAAA,iBACA,SAAA;EAAA,iBACA,KAAA;EAAA,iBACA,MAAA;EAAA,QACT,GAAA;EAAA,QACA,UAAA;EAAA,QACA,cAAA;;MAUJ,OAAA,CAAA,GAAW,IAAA;EAVP;;EAgBR,UAAA,CAAW,IAAA;EAOX,iBAAA,CAAA;EAgDA,oBAAA,CAAA;EAOA,wBAAA,CAAyB,IAAA,UAAc,IAAA,iBAAqB,KAAA;EAAA,QAqBpD,kBAAA;EAAA,QAKA,kBAAA;EAAA,QAKA,cAAA;EAAA,iBAOS,UAAA;EAAA,iBAUA,eAAA;AAAA;;cAmBN,eAAA,SAAwB,WAAA;EAAA,QAC3B,UAAA;EAER,iBAAA,CAAA;EAYA,oBAAA,CAAA;AAAA;;iBAac,YAAA,CAAA"}
package/dist/switch.js ADDED
@@ -0,0 +1,176 @@
1
+ import { createDomContext, injectValue, provideContext } from "./dom-context.js";
2
+ import { i as attachHiddenInput, r as formControlContext } from "./form-control-B_BO9j7_.js";
3
+ import { effect, signal } from "@aihu/signals";
4
+ //#region src/switch/index.ts
5
+ /**
6
+ * Headless switch — `<aihu-switch-root>` (state owner) + presentational
7
+ * `<aihu-switch-thumb>`. Implements the WAI-ARIA APG **Switch** pattern:
8
+ * `role="switch"`, binary `aria-checked="true" | "false"` (never "mixed"),
9
+ * Space AND Enter toggle. Deliberately a SIBLING of checkbox, not shared
10
+ * code — the ARIA contracts diverge (tri-state vs binary, Enter behavior).
11
+ * Emits NO CSS — each piece reflects `data-state="checked" | "unchecked"`
12
+ * (+ `data-disabled` presence) for the consumer to style.
13
+ *
14
+ * Form participation rides `attachHiddenInput` (a visually-hidden native
15
+ * checkbox in the host's light DOM). `checked-change` (detail
16
+ * `{ checked: boolean }`, bubbles, composed) is dispatched on USER-driven
17
+ * toggles only (clicks/keys) — programmatic attribute writes do not emit.
18
+ */
19
+ const switchContext = createDomContext("switch");
20
+ var AihuSwitchRoot = class extends HTMLElement {
21
+ static observedAttributes = [
22
+ "checked",
23
+ "default-checked",
24
+ "disabled",
25
+ "required",
26
+ "name",
27
+ "value"
28
+ ];
29
+ _checked = signal(false);
30
+ _disabled = signal(false);
31
+ _required = signal(false);
32
+ _name = signal(null);
33
+ _value = signal("on");
34
+ _fc = null;
35
+ _disposers = [];
36
+ _defaultSeeded = false;
37
+ constructor() {
38
+ super();
39
+ provideContext(this, switchContext, {
40
+ checked: this._checked[0],
41
+ disabled: () => this._effectiveDisabled()
42
+ });
43
+ }
44
+ get checked() {
45
+ return this._checked[0];
46
+ }
47
+ /** Programmatic write: signal + reflected `checked` attribute (two-way,
48
+ * dialog open-attr pattern). Does NOT emit `checked-change`. */
49
+ setChecked(next) {
50
+ if (next === this._checked[0]()) return;
51
+ this._checked[1](next);
52
+ if (next) this.setAttribute("checked", "");
53
+ else this.removeAttribute("checked");
54
+ }
55
+ connectedCallback() {
56
+ if (this.hasAttribute("checked")) this._checked[1](true);
57
+ else if (!this._defaultSeeded && this.hasAttribute("default-checked")) this._checked[1](true);
58
+ this._defaultSeeded = true;
59
+ this._syncFromAttrs();
60
+ if (!this.hasAttribute("role")) this.setAttribute("role", "switch");
61
+ if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "0");
62
+ this.setAttribute("data-fc-control", "");
63
+ this.addEventListener("keydown", this._onKeydown);
64
+ this.addEventListener("click", this._onClickCapture, true);
65
+ try {
66
+ this._fc = injectValue(this, formControlContext);
67
+ } catch {
68
+ this._fc = null;
69
+ }
70
+ this._disposers.push(effect(() => {
71
+ const checked = this._checked[0]();
72
+ this.setAttribute("aria-checked", checked ? "true" : "false");
73
+ if (this._effectiveRequired()) this.setAttribute("aria-required", "true");
74
+ else this.removeAttribute("aria-required");
75
+ this.setAttribute("data-state", checked ? "checked" : "unchecked");
76
+ if (this._effectiveDisabled()) this.setAttribute("data-disabled", "");
77
+ else this.removeAttribute("data-disabled");
78
+ }), attachHiddenInput(this, {
79
+ type: "checkbox",
80
+ name: this._name[0],
81
+ value: this._value[0],
82
+ checked: this._checked[0],
83
+ required: () => this._effectiveRequired(),
84
+ disabled: () => this._effectiveDisabled()
85
+ }));
86
+ }
87
+ disconnectedCallback() {
88
+ this.removeEventListener("keydown", this._onKeydown);
89
+ this.removeEventListener("click", this._onClickCapture, true);
90
+ for (const d of this._disposers) d();
91
+ this._disposers = [];
92
+ }
93
+ attributeChangedCallback(name, _old, value) {
94
+ switch (name) {
95
+ case "checked":
96
+ this._checked[1](value !== null);
97
+ break;
98
+ case "disabled":
99
+ this._disabled[1](value !== null);
100
+ break;
101
+ case "required":
102
+ this._required[1](value !== null);
103
+ break;
104
+ case "name":
105
+ this._name[1](value);
106
+ break;
107
+ case "value":
108
+ this._value[1](value ?? "on");
109
+ break;
110
+ }
111
+ }
112
+ _effectiveDisabled() {
113
+ if (this._disabled[0]()) return true;
114
+ return this._fc ? this._fc.disabled() : false;
115
+ }
116
+ _effectiveRequired() {
117
+ if (this._required[0]()) return true;
118
+ return this._fc ? this._fc.required() : false;
119
+ }
120
+ _syncFromAttrs() {
121
+ this._disabled[1](this.hasAttribute("disabled"));
122
+ this._required[1](this.hasAttribute("required"));
123
+ this._name[1](this.getAttribute("name"));
124
+ this._value[1](this.getAttribute("value") ?? "on");
125
+ }
126
+ _onKeydown = (ev) => {
127
+ if (ev.key === " " || ev.key === "Spacebar" || ev.key === "Enter") {
128
+ ev.preventDefault();
129
+ if (this._effectiveDisabled()) return;
130
+ this.click();
131
+ }
132
+ };
133
+ _onClickCapture = (ev) => {
134
+ if (this._effectiveDisabled()) {
135
+ ev.preventDefault();
136
+ ev.stopImmediatePropagation();
137
+ return;
138
+ }
139
+ const next = !this._checked[0]();
140
+ this.setChecked(next);
141
+ this.dispatchEvent(new CustomEvent("checked-change", {
142
+ detail: { checked: next },
143
+ bubbles: true,
144
+ composed: true
145
+ }));
146
+ };
147
+ };
148
+ /** Presentational styling hook: mirrors the root's state, hidden from AT. */
149
+ var AihuSwitchThumb = class extends HTMLElement {
150
+ _disposers = [];
151
+ connectedCallback() {
152
+ const ctx = injectValue(this, switchContext);
153
+ this.setAttribute("aria-hidden", "true");
154
+ this._disposers.push(effect(() => {
155
+ this.setAttribute("data-state", ctx.checked() ? "checked" : "unchecked");
156
+ if (ctx.disabled()) this.setAttribute("data-disabled", "");
157
+ else this.removeAttribute("data-disabled");
158
+ }));
159
+ }
160
+ disconnectedCallback() {
161
+ for (const d of this._disposers) d();
162
+ this._disposers = [];
163
+ }
164
+ };
165
+ const REGISTRY = [["aihu-switch-root", AihuSwitchRoot], ["aihu-switch-thumb", AihuSwitchThumb]];
166
+ let _defined = false;
167
+ /** Register all switch custom elements (idempotent). */
168
+ function defineSwitch() {
169
+ if (_defined) return;
170
+ for (const [tag, ctor] of REGISTRY) if (!customElements.get(tag)) customElements.define(tag, ctor);
171
+ _defined = true;
172
+ }
173
+ //#endregion
174
+ export { AihuSwitchRoot, AihuSwitchThumb, defineSwitch, switchContext };
175
+
176
+ //# sourceMappingURL=switch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"switch.js","names":[],"sources":["../src/switch/index.ts"],"sourcesContent":["/**\n * Headless switch — `<aihu-switch-root>` (state owner) + presentational\n * `<aihu-switch-thumb>`. Implements the WAI-ARIA APG **Switch** pattern:\n * `role=\"switch\"`, binary `aria-checked=\"true\" | \"false\"` (never \"mixed\"),\n * Space AND Enter toggle. Deliberately a SIBLING of checkbox, not shared\n * code — the ARIA contracts diverge (tri-state vs binary, Enter behavior).\n * Emits NO CSS — each piece reflects `data-state=\"checked\" | \"unchecked\"`\n * (+ `data-disabled` presence) for the consumer to style.\n *\n * Form participation rides `attachHiddenInput` (a visually-hidden native\n * checkbox in the host's light DOM). `checked-change` (detail\n * `{ checked: boolean }`, bubbles, composed) is dispatched on USER-driven\n * toggles only (clicks/keys) — programmatic attribute writes do not emit.\n */\n\nimport { effect, type Read, signal } from '@aihu/signals'\nimport { createDomContext, injectValue, provideContext } from '../dom-context.ts'\nimport {\n attachHiddenInput,\n type FormControlContextValue,\n formControlContext,\n} from '../form-control/index.ts'\n\nexport interface SwitchContextValue {\n readonly checked: Read<boolean>\n readonly disabled: Read<boolean>\n}\n\nexport const switchContext = createDomContext<SwitchContextValue>('switch')\n\nexport class AihuSwitchRoot extends HTMLElement {\n static readonly observedAttributes = [\n 'checked',\n 'default-checked',\n 'disabled',\n 'required',\n 'name',\n 'value',\n ]\n\n private readonly _checked = signal(false)\n private readonly _disabled = signal(false)\n private readonly _required = signal(false)\n private readonly _name = signal<string | null>(null)\n private readonly _value = signal('on')\n private _fc: FormControlContextValue | null = null\n private _disposers: Array<() => void> = []\n private _defaultSeeded = false\n\n constructor() {\n super()\n provideContext(this, switchContext, {\n checked: this._checked[0],\n disabled: () => this._effectiveDisabled(),\n })\n }\n\n get checked(): Read<boolean> {\n return this._checked[0]\n }\n\n /** Programmatic write: signal + reflected `checked` attribute (two-way,\n * dialog open-attr pattern). Does NOT emit `checked-change`. */\n setChecked(next: boolean): void {\n if (next === this._checked[0]()) return\n this._checked[1](next)\n if (next) this.setAttribute('checked', '')\n else this.removeAttribute('checked')\n }\n\n connectedCallback(): void {\n // Initial state: the `checked` attribute wins; otherwise `default-checked`\n // seeds ONCE (signal only — never reflected).\n if (this.hasAttribute('checked')) {\n this._checked[1](true)\n } else if (!this._defaultSeeded && this.hasAttribute('default-checked')) {\n this._checked[1](true)\n }\n this._defaultSeeded = true\n this._syncFromAttrs()\n\n if (!this.hasAttribute('role')) this.setAttribute('role', 'switch')\n if (!this.hasAttribute('tabindex')) this.setAttribute('tabindex', '0')\n // Discoverable by an ancestor <aihu-form-control> ([data-fc-control] is\n // first in its control selector, so the HOST wins over the hidden input).\n this.setAttribute('data-fc-control', '')\n\n this.addEventListener('keydown', this._onKeydown)\n // Capture so disabled suppression beats consumer handlers (button precedent).\n this.addEventListener('click', this._onClickCapture, true)\n\n try {\n this._fc = injectValue(this, formControlContext)\n } catch {\n this._fc = null\n }\n\n this._disposers.push(\n effect(() => {\n const checked = this._checked[0]()\n this.setAttribute('aria-checked', checked ? 'true' : 'false')\n if (this._effectiveRequired()) this.setAttribute('aria-required', 'true')\n else this.removeAttribute('aria-required')\n this.setAttribute('data-state', checked ? 'checked' : 'unchecked')\n if (this._effectiveDisabled()) this.setAttribute('data-disabled', '')\n else this.removeAttribute('data-disabled')\n }),\n attachHiddenInput(this, {\n type: 'checkbox',\n name: this._name[0],\n value: this._value[0],\n checked: this._checked[0],\n required: () => this._effectiveRequired(),\n disabled: () => this._effectiveDisabled(),\n }),\n )\n }\n\n disconnectedCallback(): void {\n this.removeEventListener('keydown', this._onKeydown)\n this.removeEventListener('click', this._onClickCapture, true)\n for (const d of this._disposers) d()\n this._disposers = []\n }\n\n attributeChangedCallback(name: string, _old: string | null, value: string | null): void {\n switch (name) {\n case 'checked':\n this._checked[1](value !== null)\n break\n case 'disabled':\n this._disabled[1](value !== null)\n break\n case 'required':\n this._required[1](value !== null)\n break\n case 'name':\n this._name[1](value)\n break\n case 'value':\n this._value[1](value ?? 'on')\n break\n // 'default-checked' is read once on connect — never reactive.\n }\n }\n\n private _effectiveDisabled(): boolean {\n if (this._disabled[0]()) return true\n return this._fc ? this._fc.disabled() : false\n }\n\n private _effectiveRequired(): boolean {\n if (this._required[0]()) return true\n return this._fc ? this._fc.required() : false\n }\n\n private _syncFromAttrs(): void {\n this._disabled[1](this.hasAttribute('disabled'))\n this._required[1](this.hasAttribute('required'))\n this._name[1](this.getAttribute('name'))\n this._value[1](this.getAttribute('value') ?? 'on')\n }\n\n private readonly _onKeydown = (ev: KeyboardEvent): void => {\n // APG Switch: BOTH Space and Enter toggle (unlike checkbox, where Enter\n // is explicitly inert).\n if (ev.key === ' ' || ev.key === 'Spacebar' || ev.key === 'Enter') {\n ev.preventDefault()\n if (this._effectiveDisabled()) return\n this.click()\n }\n }\n\n private readonly _onClickCapture = (ev: Event): void => {\n if (this._effectiveDisabled()) {\n ev.preventDefault()\n ev.stopImmediatePropagation()\n return\n }\n const next = !this._checked[0]()\n this.setChecked(next)\n this.dispatchEvent(\n new CustomEvent<{ checked: boolean }>('checked-change', {\n detail: { checked: next },\n bubbles: true,\n composed: true,\n }),\n )\n }\n}\n\n/** Presentational styling hook: mirrors the root's state, hidden from AT. */\nexport class AihuSwitchThumb extends HTMLElement {\n private _disposers: Array<() => void> = []\n\n connectedCallback(): void {\n const ctx = injectValue(this, switchContext)\n this.setAttribute('aria-hidden', 'true')\n this._disposers.push(\n effect(() => {\n this.setAttribute('data-state', ctx.checked() ? 'checked' : 'unchecked')\n if (ctx.disabled()) this.setAttribute('data-disabled', '')\n else this.removeAttribute('data-disabled')\n }),\n )\n }\n\n disconnectedCallback(): void {\n for (const d of this._disposers) d()\n this._disposers = []\n }\n}\n\nconst REGISTRY: Array<[string, CustomElementConstructor]> = [\n ['aihu-switch-root', AihuSwitchRoot],\n ['aihu-switch-thumb', AihuSwitchThumb],\n]\n\nlet _defined = false\n/** Register all switch custom elements (idempotent). */\nexport function defineSwitch(): void {\n if (_defined) return\n for (const [tag, ctor] of REGISTRY) {\n if (!customElements.get(tag)) customElements.define(tag, ctor)\n }\n _defined = true\n}\n"],"mappings":"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"}