@antadesign/anta 0.2.1 → 0.3.0

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 (85) hide show
  1. package/README.md +14 -0
  2. package/dist/anta_helpers.d.ts +39 -1
  3. package/dist/anta_helpers.js +30 -2
  4. package/dist/components/Button.d.ts +7 -4
  5. package/dist/components/Button.js +12 -14
  6. package/dist/components/Checkbox.d.ts +97 -0
  7. package/dist/components/Checkbox.js +77 -0
  8. package/dist/components/Expander.d.ts +74 -0
  9. package/dist/components/Expander.js +53 -0
  10. package/dist/components/Input.d.ts +159 -0
  11. package/dist/components/Input.js +150 -0
  12. package/dist/components/Menu.d.ts +70 -0
  13. package/dist/components/Menu.js +42 -0
  14. package/dist/components/MenuGroup.d.ts +24 -0
  15. package/dist/components/MenuGroup.js +19 -0
  16. package/dist/components/MenuItem.d.ts +50 -0
  17. package/dist/components/MenuItem.js +41 -0
  18. package/dist/components/MenuSeparator.d.ts +14 -0
  19. package/dist/components/MenuSeparator.js +7 -0
  20. package/dist/components/Progress.d.ts +12 -6
  21. package/dist/components/Progress.js +7 -4
  22. package/dist/components/Radio.d.ts +37 -0
  23. package/dist/components/Radio.js +33 -0
  24. package/dist/components/RadioGroup.d.ts +119 -0
  25. package/dist/components/RadioGroup.js +108 -0
  26. package/dist/components/Tag.d.ts +38 -5
  27. package/dist/components/Tag.js +9 -5
  28. package/dist/components/Text.d.ts +27 -12
  29. package/dist/components/Text.js +6 -3
  30. package/dist/components/Title.d.ts +10 -1
  31. package/dist/components/Tooltip.d.ts +19 -9
  32. package/dist/components/Tooltip.js +2 -2
  33. package/dist/elements/a-button.css +1 -1
  34. package/dist/elements/a-button.d.ts +56 -0
  35. package/dist/elements/a-button.js +13 -11
  36. package/dist/elements/a-checkbox.css +1 -0
  37. package/dist/elements/a-checkbox.d.ts +52 -0
  38. package/dist/elements/a-checkbox.js +130 -0
  39. package/dist/elements/a-expander.css +1 -0
  40. package/dist/elements/a-expander.d.ts +28 -0
  41. package/dist/elements/a-expander.js +237 -0
  42. package/dist/elements/a-icon.d.ts +14 -0
  43. package/dist/elements/a-icon.shapes.css +1 -1
  44. package/dist/elements/a-icon.shapes.d.ts +10 -2
  45. package/dist/elements/a-icon.shapes.js +12 -3
  46. package/dist/elements/a-input.css +1 -0
  47. package/dist/elements/a-input.d.ts +68 -0
  48. package/dist/elements/a-input.js +511 -0
  49. package/dist/elements/a-menu-group.css +1 -0
  50. package/dist/elements/a-menu-group.d.ts +13 -0
  51. package/dist/elements/a-menu-group.js +15 -0
  52. package/dist/elements/a-menu-item.css +1 -0
  53. package/dist/elements/a-menu-item.d.ts +47 -0
  54. package/dist/elements/a-menu-item.js +30 -0
  55. package/dist/elements/a-menu-separator.css +1 -0
  56. package/dist/elements/a-menu-separator.d.ts +13 -0
  57. package/dist/elements/a-menu-separator.js +15 -0
  58. package/dist/elements/a-menu.css +1 -0
  59. package/dist/elements/a-menu.d.ts +171 -0
  60. package/dist/elements/a-menu.js +714 -0
  61. package/dist/elements/a-progress.css +1 -1
  62. package/dist/elements/a-progress.d.ts +12 -0
  63. package/dist/elements/a-progress.js +1 -0
  64. package/dist/elements/a-radio-group.css +1 -0
  65. package/dist/elements/a-radio-group.d.ts +33 -0
  66. package/dist/elements/a-radio-group.js +160 -0
  67. package/dist/elements/a-radio.css +1 -0
  68. package/dist/elements/a-radio.d.ts +14 -0
  69. package/dist/elements/a-radio.js +46 -0
  70. package/dist/elements/a-tag.css +1 -1
  71. package/dist/elements/a-text.css +1 -1
  72. package/dist/elements/a-text.d.ts +42 -3
  73. package/dist/elements/a-text.js +73 -33
  74. package/dist/elements/a-title.css +1 -1
  75. package/dist/elements/a-tooltip.d.ts +65 -15
  76. package/dist/elements/a-tooltip.js +155 -115
  77. package/dist/elements/index.d.ts +9 -0
  78. package/dist/elements/index.js +27 -0
  79. package/dist/general_types.d.ts +471 -19
  80. package/dist/index.d.ts +16 -0
  81. package/dist/index.js +16 -0
  82. package/dist/jsx-runtime.d.ts +42 -7
  83. package/dist/jsx-runtime.js +14 -2
  84. package/dist/tokens.css +1 -1
  85. package/package.json +1 -1
@@ -5,6 +5,62 @@ declare global {
5
5
  hasKeyListenerForAButton?: boolean;
6
6
  }
7
7
  }
8
+ /**
9
+ * `<a-button>` — button element (also styles `a[role="button"]`).
10
+ *
11
+ * Styling notes (`a-button.css` ships comment-free):
12
+ * - **Token architecture.** The base rule carries shared constants plus the
13
+ * neutral tone tokens (neutral is the implicit default; its chip uses a
14
+ * softer 5% rest alpha vs 10% for named tones). Named tones override via
15
+ * `[tone="X"]`; critical/warning tertiary active steps heavier (20% vs
16
+ * 15%). Final colors are wired through `--button-fg`/`--button-bg`:
17
+ * secondary is declared unscoped so a bare `a-button` consumer selector
18
+ * can override it without bumping specificity; primary/tertiary/quaternary
19
+ * match an explicit `[priority]` and win on specificity. Secondary's rest
20
+ * label is darkened by `--button-fg-secondary-l-shift` oklch lightness
21
+ * (0.05 light mode, zeroed in dark) at the `--button-fg` wiring, so it
22
+ * covers named and custom tones alike; secondary also carries a 1px
23
+ * hairline box-shadow in the current fg tone at 50% alpha, which the
24
+ * other priorities cancel in their own blocks. `[selected]` adds a 1px
25
+ * inset ring in `currentColor`, declared after the priority blocks so it
26
+ * survives their `box-shadow: none` cancels.
27
+ * - **Dark mode** re-tunes with heavier alphas (30/40% vs 10/15%) and flips
28
+ * neutral's anchor to a lilac — mixing dark gray into a dark bg yields no
29
+ * contrast.
30
+ * - **Custom tones** (any non-named `tone` value): primary uses the literal
31
+ * color; other priorities derive from the source HUE via oklch relative
32
+ * color with lightness/chroma/alpha pinned near Brand. The `--_tone-*`
33
+ * knobs are the only numbers to tune (the `.dark` block re-tunes them).
34
+ * The JSX wrapper writes `--button-tone-source` inline; a typed `attr()`
35
+ * fallback picks up raw `<a-button tone="…">` on Chrome 133+/Safari 18.2+.
36
+ * - **Hover is gated** to `(hover: hover) and (pointer: fine)` so it doesn't
37
+ * stick after a tap; `:active`/`[selected]` stay ungated (correct press
38
+ * feedback on touch). Quaternary rests at 90% fg alpha with a slightly
39
+ * heavier `font-weight: 415` (quieter than tertiary but legible), restores
40
+ * full opacity on hover, lightens by 0.05 oklch L on press (dark mode
41
+ * returns to rest instead — it's already light), and has instant
42
+ * transitions in every direction.
43
+ * - **Layout gotchas:** `flex-shrink: 0` (shrinking + `overflow: hidden`
44
+ * would clip the label silently); `font-variation-settings` must restate
45
+ * ALL axes — it replaces rather than merges, and dropping "slnt"/"ital"
46
+ * renders italic in Safari; the label uses a 17px line box + 1px bottom
47
+ * padding for optical vertical centering at an unchanged 18px box.
48
+ * - **Icon-only** is purely structural — `:has(> a-icon:only-child)` gives
49
+ * square padding + a min-size pin (and centers when the host is sized
50
+ * bigger). A non-only edge icon trims ~2px off its side's padding
51
+ * (optical), `max(0px, …)` keeping paddingless at 0.
52
+ * - **Underline** renders only on tertiary/quaternary: 0.5px hairline at 75%
53
+ * alpha at rest, 1px at full alpha on hover/`[selected]`, mirroring the
54
+ * prose-link rule in `<Text>`.
55
+ * - **Loading** paints a blurred diagonal stripe overlay; the x-period is
56
+ * the diagonal period projected onto x so the slide loops seamlessly
57
+ * (overshooting left by one period), and a large negative
58
+ * `animation-delay` desyncs instances so a row doesn't pulse in lockstep.
59
+ * - **Disabled** sets `background-color`/`color` directly (not the vars) so
60
+ * an inline `--button-bg` override can't keep a disabled button alive, and
61
+ * skips transitions — a `.dark` toggle would flash the tone hue
62
+ * mid-resolve.
63
+ */
8
64
  export declare class AButtonElement extends HTMLElementBase {
9
65
  connectedCallback(): void;
10
66
  }
@@ -1,27 +1,31 @@
1
1
  import { HTMLElementBase } from "../anta_helpers";
2
2
  import "./a-button.css";
3
+ function installDocumentHandlers(doc) {
4
+ if (!doc || doc.hasKeyListenerForAButton) return;
5
+ doc.addEventListener("keydown", handleKeyDown, true);
6
+ doc.addEventListener("click", handleClick, true);
7
+ doc.hasKeyListenerForAButton = true;
8
+ }
3
9
  class AButtonElement extends HTMLElementBase {
4
10
  connectedCallback() {
5
- if (!document.hasKeyListenerForAButton) {
6
- document.addEventListener("keydown", handleKeyDown, true);
7
- document.addEventListener("click", handleClick, true);
8
- document.hasKeyListenerForAButton = true;
9
- }
11
+ installDocumentHandlers(this.doc);
10
12
  }
11
13
  }
12
14
  function findForm(el) {
13
15
  const formId = el.getAttribute("form");
14
16
  if (formId) {
15
- return document.getElementById(formId);
17
+ return el.ownerDocument.getElementById(formId);
16
18
  }
17
19
  return el.closest("form");
18
20
  }
19
21
  function handleKeyDown(e) {
20
22
  if (e.key !== "Enter" && e.key !== " ") return;
23
+ if (e.repeat) return;
21
24
  const el = e.target.closest(
22
25
  "a-button"
23
26
  );
24
27
  if (!el) return;
28
+ if (el.hasAttribute("disabled") || el.hasAttribute("loading")) return;
25
29
  e.preventDefault();
26
30
  el.click();
27
31
  }
@@ -30,6 +34,7 @@ function handleClick(e) {
30
34
  "a-button"
31
35
  );
32
36
  if (!el) return;
37
+ if (el.hasAttribute("disabled") || el.hasAttribute("loading")) return;
33
38
  const customEvent = el.getAttribute("data-custom-event");
34
39
  if (customEvent) {
35
40
  el.dispatchEvent(
@@ -40,11 +45,7 @@ function handleClick(e) {
40
45
  const form = findForm(el);
41
46
  if (!form) return;
42
47
  if (type === "submit") {
43
- if (form.requestSubmit) {
44
- form.requestSubmit();
45
- } else {
46
- form.submit();
47
- }
48
+ form.requestSubmit();
48
49
  const formData = new FormData(form);
49
50
  form.dispatchEvent(
50
51
  new CustomEvent("submitdetailed", {
@@ -67,6 +68,7 @@ function handleClick(e) {
67
68
  }
68
69
  function register_a_button() {
69
70
  if (typeof customElements === "undefined") return;
71
+ installDocumentHandlers(typeof document !== "undefined" ? document : void 0);
70
72
  if (!customElements.get("a-button")) {
71
73
  customElements.define("a-button", AButtonElement);
72
74
  }
@@ -0,0 +1 @@
1
+ @layer anta{a-checkbox{--checkbox-gap: 8px;--checkbox-bg: var(--bg-1);--checkbox-fill: #635b65;--checkbox-fill-hover: #534c57;--checkbox-fill-active: #49424c;--checkbox-border-neutral: #d4ced4;--checkbox-border: var(--checkbox-border-neutral);--checkbox-border-hover: #c1b9c1;--checkbox-border-active: #9f99a1;--checkbox-icon: #ffffff;--checkbox-icon-disabled: #c1b9c1;--checkbox-bg-disabled: color-mix(in oklch, #44374b 10%, transparent);--checkbox-border-disabled: color-mix(in oklch, #44374b 15%, transparent);--checkbox-mask-size: 12px;--checkbox-mask-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");--checkbox-mask-indeterminate: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E");--checkbox-label-color: var(--text-2);--checkbox-label-color-disabled: var(--text-4);--checkbox-hint-color: var(--text-4);--checkbox-label-fs: 15px;--checkbox-label-lh: 20px;--checkbox-hint-fs: 13px;--checkbox-hint-lh: 16px;display:inline-grid;grid-template-columns:auto 1fr;align-items:center;column-gap:var(--checkbox-gap);vertical-align:middle;cursor:pointer;user-select:none;-webkit-user-drag:none;outline:none;color:var(--checkbox-label-color);font-family:var(--sans-serif);font-size:var(--checkbox-label-fs);line-height:var(--checkbox-label-lh);font-feature-settings:"ss02","ss05";&:before{content:"";grid-column:1;grid-row:1 / -1;align-self:center;box-sizing:border-box;inline-size:16px;block-size:16px;border-radius:4px;background:var(--checkbox-bg);border:1.5px solid var(--checkbox-border);transition:background-color .15s ease-out,border-color .15s ease-out}&:after{content:"";grid-column:1;grid-row:1 / -1;align-self:center;inline-size:16px;block-size:16px;background-color:transparent;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center;-webkit-mask-size:var(--checkbox-mask-size);mask-size:var(--checkbox-mask-size)}@media(hover:hover)and (pointer:fine){&:not(:disabled):hover:before{border-color:var(--checkbox-border-hover);transition:background-color 75ms ease-in,border-color 75ms ease-in}&:not(:disabled):hover:is(:state(checked),:state(indeterminate)):before{background:var(--checkbox-fill-hover);border-color:var(--checkbox-fill-hover)}}&:not(:disabled):active:before{border-color:var(--checkbox-border-active);transition:background-color 50ms linear,border-color 50ms linear}&:not(:disabled):active:is(:state(checked),:state(indeterminate)):before{background:var(--checkbox-fill-active);border-color:var(--checkbox-fill-active)}&:is(:state(checked),:state(indeterminate)){&:before{background:var(--checkbox-fill);border-color:var(--checkbox-fill)}&:after{background-color:var(--checkbox-icon)}}&:state(checked):after{-webkit-mask-image:var(--checkbox-mask-check);mask-image:var(--checkbox-mask-check)}&:state(indeterminate):after{-webkit-mask-image:var(--checkbox-mask-indeterminate);mask-image:var(--checkbox-mask-indeterminate)}&:focus-visible:before{outline:1px solid var(--focus-ring);outline-offset:1px}&:disabled{cursor:not-allowed;color:var(--checkbox-label-color-disabled);&:before{background:var(--checkbox-bg-disabled);border-color:var(--checkbox-border-disabled);transition:none}&:is(:state(checked),:state(indeterminate)):after{background-color:var(--checkbox-icon-disabled)}a-checkbox-hint{color:var(--checkbox-label-color-disabled)}}&[size=small]{--checkbox-gap: 6px;--checkbox-label-fs: 13px;--checkbox-label-lh: 16px;--checkbox-hint-fs: 12px;--checkbox-hint-lh: 14px;--checkbox-mask-size: 10px;&:before,&:after{inline-size:14px;block-size:14px}}&[size=large]{--checkbox-label-fs: 17px;--checkbox-label-lh: 24px;--checkbox-hint-fs: 15px;--checkbox-hint-lh: 19px;--checkbox-mask-size: 14px;&:before,&:after{inline-size:18px;block-size:18px}}}a-checkbox[priority=secondary]{&:not(:disabled):is(:state(checked),:state(indeterminate)){&:before{background:var(--checkbox-bg);border-color:var(--checkbox-fill)}&:after{background-color:var(--checkbox-fill)}}@media(hover:hover)and (pointer:fine){&:not(:disabled):hover:is(:state(checked),:state(indeterminate)){&:before{background:var(--checkbox-bg);border-color:var(--checkbox-fill-hover)}&:after{background-color:var(--checkbox-fill-hover)}}}&:not(:disabled):active:is(:state(checked),:state(indeterminate)){&:before{background:var(--checkbox-bg);border-color:var(--checkbox-fill-active)}&:after{background-color:var(--checkbox-fill-active)}}}.dark a-checkbox{--checkbox-mask-size: 13px}.dark a-checkbox[size=small]{--checkbox-mask-size: 11px}.dark a-checkbox[size=large]{--checkbox-mask-size: 15px}.dark a-checkbox[priority=secondary]{--checkbox-mask-size: 14px;&[size=small]{--checkbox-mask-size: 12px}&[size=large]{--checkbox-mask-size: 16px}&:not(:disabled):is(:state(checked),:state(indeterminate)){&:before{border-color:var(--checkbox-fill-active)}&:after{background-color:var(--checkbox-fill-active)}}@media(hover:hover)and (pointer:fine){&:not(:disabled):hover:is(:state(checked),:state(indeterminate)){&:before{border-color:oklch(from var(--checkbox-fill-active) calc(l + .07) c h)}&:after{background-color:oklch(from var(--checkbox-fill-active) calc(l + .07) c h)}}}&:not(:disabled):active:is(:state(checked),:state(indeterminate)){&:before{border-color:oklch(from var(--checkbox-fill-active) calc(l + .12) c h)}&:after{background-color:oklch(from var(--checkbox-fill-active) calc(l + .12) c h)}}}a-checkbox-label,a-checkbox-hint{display:block;grid-column:2;min-width:0}a-checkbox-hint{color:var(--checkbox-hint-color);font-size:var(--checkbox-hint-fs);line-height:var(--checkbox-hint-lh);margin-block-start:2px}a-checkbox[tone=brand]{--checkbox-label-color: var(--text-2-brand);--checkbox-hint-color: var(--text-4-brand);--checkbox-fill: #5f4bc3;--checkbox-fill-hover: #503cb4;--checkbox-fill-active: #483493}a-checkbox[tone=neutral]{--checkbox-fill: #635b65;--checkbox-fill-hover: #534c57;--checkbox-fill-active: #49424c}a-checkbox[tone=info]{--checkbox-label-color: var(--text-2-info);--checkbox-hint-color: var(--text-4-info);--checkbox-fill: #1f6eb2;--checkbox-fill-hover: #1a5b93;--checkbox-fill-active: #175082}a-checkbox[tone=success]{--checkbox-label-color: var(--text-2-success);--checkbox-hint-color: var(--text-4-success);--checkbox-fill: #2a7e43;--checkbox-fill-hover: #226737;--checkbox-fill-active: #1f5c31}a-checkbox[tone=warning]{--checkbox-label-color: var(--text-2-warning);--checkbox-hint-color: var(--text-4-warning);--checkbox-fill: #c37416;--checkbox-fill-hover: #ae6613;--checkbox-fill-active: #995200}a-checkbox[tone=critical]{--checkbox-label-color: var(--text-2-critical);--checkbox-hint-color: var(--text-4-critical);--checkbox-fill: #c9302c;--checkbox-fill-hover: #b02120;--checkbox-fill-active: #a01c1c}a-checkbox[tone]:not([tone=""],[tone=neutral]){--_checkbox-border-base: color-mix(in oklch, var(--checkbox-fill) 70%, var(--checkbox-border-neutral));--checkbox-border: oklch(from var(--_checkbox-border-base) calc(l + .17) c h);--checkbox-border-hover: oklch(from var(--_checkbox-border-base) calc(l + .1) c h);--checkbox-border-active: var(--_checkbox-border-base);.dark &{--checkbox-border: var(--_checkbox-border-base);--checkbox-border-hover: oklch(from var(--_checkbox-border-base) calc(l + .1) c h);--checkbox-border-active: oklch(from var(--_checkbox-border-base) calc(l + .17) c h)}}a-checkbox[tone]:not([tone=""],[tone=brand],[tone=neutral],[tone=info],[tone=success],[tone=warning],[tone=critical]){--checkbox-tone-source: attr(tone type(<color>), transparent);--_tone-l-rest: .5;--_tone-l-hover: .45;--_tone-l-active: .4;--checkbox-fill: oklch(from var(--checkbox-tone-source) var(--_tone-l-rest) c h);--checkbox-fill-hover: oklch(from var(--checkbox-tone-source) var(--_tone-l-hover) c h);--checkbox-fill-active: oklch(from var(--checkbox-tone-source) var(--_tone-l-active) c h);--checkbox-label-color: oklch(from var(--checkbox-tone-source) .5 c h);--checkbox-hint-color: oklch(from var(--checkbox-tone-source) .5 c h / .7);.dark &{--_tone-l-rest: .45;--_tone-l-hover: .5;--_tone-l-active: .57;--checkbox-label-color: oklch(from var(--checkbox-tone-source) .8 c h);--checkbox-hint-color: oklch(from var(--checkbox-tone-source) .8 c h / .7)}}.dark a-checkbox{--checkbox-border-neutral: #49424c;--checkbox-border-hover: #635b65;--checkbox-border-active: #776e77;--checkbox-fill: #534c57;--checkbox-fill-hover: #635b65;--checkbox-fill-active: #938d96;--checkbox-icon-disabled: #635b65;--checkbox-bg-disabled: color-mix(in oklch, #e4d1ef 10%, transparent);--checkbox-border-disabled: color-mix(in oklch, #e4d1ef 15%, transparent)}.dark a-checkbox[tone=brand]{--checkbox-fill: #503cb4;--checkbox-fill-hover: #5f4bc3;--checkbox-fill-active: #7460d7}.dark a-checkbox[tone=neutral]{--checkbox-fill: #534c57;--checkbox-fill-hover: #635b65;--checkbox-fill-active: #938d96}.dark a-checkbox[tone=info]{--checkbox-fill: #1a5b93;--checkbox-fill-hover: #1f6eb2;--checkbox-fill-active: #2686d9}.dark a-checkbox[tone=success]{--checkbox-fill: #226737;--checkbox-fill-hover: #2a7e43;--checkbox-fill-active: #329550}.dark a-checkbox[tone=warning]{--checkbox-fill: #7f410b;--checkbox-fill-hover: #995200;--checkbox-fill-active: #ae6613}.dark a-checkbox[tone=critical]{--checkbox-fill: #b02120;--checkbox-fill-hover: #c9302c;--checkbox-fill-active: #de4545}}
@@ -0,0 +1,52 @@
1
+ import { HTMLElementBase } from "../anta_helpers";
2
+ import "./a-checkbox.css";
3
+ /**
4
+ * `<a-checkbox>` — interactive light-DOM checkbox. Visual state lives on
5
+ * `ElementInternals` (CSS keys off `:state(checked)` / `:state(indeterminate)`),
6
+ * never on host attributes. The element **publishes `aria-checked` itself**, via
7
+ * `ElementInternals` (off the DOM), so it stays live as the element self-toggles
8
+ * uncontrolled — the wrapper does NOT set `aria-checked`. Other ARIA (`role`,
9
+ * `aria-label`) is still the wrapper's / consumer's job.
10
+ *
11
+ * One ternary state — `checked` / `unchecked` / `indeterminate` — carried by two
12
+ * attributes: `state` is the **controlled** live value (the element reflects
13
+ * changes to it), `default-state` is the **uncontrolled** seed (read at connect /
14
+ * form-reset, and re-applied if it changes while the checkbox is still uncontrolled
15
+ * and un-toggled — matching a native `<input>`, whose `checked` attribute reflects
16
+ * until the control is "dirtied" by interaction; once toggled, later `default-state`
17
+ * changes are ignored). No "controlled" flag — the attribute name carries the intent
18
+ * (`controlled ≡ hasAttribute('state')`).
19
+ *
20
+ * Follows the shared state contract (see `STATEFUL-COMPONENTS.md`): on
21
+ * interaction the element fires a cancelable `statechange` *before* applying.
22
+ * Controlled callers re-assert `state` from their store; uncontrolled callers
23
+ * can veto a transition synchronously with `event.preventDefault()`. No
24
+ * optimistic self-paint.
25
+ */
26
+ export declare class ACheckboxElement extends HTMLElementBase {
27
+ static formAssociated: boolean;
28
+ static observedAttributes: string[];
29
+ private internals?;
30
+ private currentState;
31
+ private seeded;
32
+ private dirty;
33
+ private alive;
34
+ /** Current checked state as a boolean (native-`<input>`-like). `indeterminate`
35
+ * reads false here; check `.indeterminate` for the mixed state. */
36
+ get checked(): boolean;
37
+ /** Whether the checkbox is in the indeterminate ("mixed") state. */
38
+ get indeterminate(): boolean;
39
+ constructor();
40
+ connectedCallback(): void;
41
+ attributeChangedCallback(name: string): void;
42
+ private get isDisabled();
43
+ private get isControlled();
44
+ private seed;
45
+ private emitChange;
46
+ private toggle;
47
+ private paint;
48
+ formResetCallback(): void;
49
+ formStateRestoreCallback(state: string): void;
50
+ formDisabledCallback(): void;
51
+ }
52
+ export declare function register_a_checkbox(): void;
@@ -0,0 +1,130 @@
1
+ import { HTMLElementBase } from "../anta_helpers";
2
+ import "./a-checkbox.css";
3
+ const parseState = (v) => v === "checked" || v === "indeterminate" ? v : "unchecked";
4
+ class ACheckboxElement extends HTMLElementBase {
5
+ static formAssociated = true;
6
+ static observedAttributes = ["state", "default-state", "value"];
7
+ internals;
8
+ currentState = "unchecked";
9
+ seeded = false;
10
+ // Set once the user toggles an uncontrolled checkbox — the native "dirty checked
11
+ // flag". After it's set, a changed `default-state` no longer re-seeds the display.
12
+ dirty = false;
13
+ // True after the first connect. Gates the native `change` event so it never
14
+ // fires for the initial attribute seed (only for real, post-connect transitions).
15
+ alive = false;
16
+ /** Current checked state as a boolean (native-`<input>`-like). `indeterminate`
17
+ * reads false here; check `.indeterminate` for the mixed state. */
18
+ get checked() {
19
+ return this.currentState === "checked";
20
+ }
21
+ /** Whether the checkbox is in the indeterminate ("mixed") state. */
22
+ get indeterminate() {
23
+ return this.currentState === "indeterminate";
24
+ }
25
+ constructor() {
26
+ super();
27
+ this.internals = this.attachInternals?.();
28
+ this.addEventListener("click", (e) => this.toggle(e));
29
+ this.addEventListener("keydown", (e) => {
30
+ if (e.key === " ") {
31
+ e.preventDefault();
32
+ this.click();
33
+ }
34
+ });
35
+ }
36
+ connectedCallback() {
37
+ if (!this.seeded) {
38
+ this.seed();
39
+ this.seeded = true;
40
+ }
41
+ this.paint();
42
+ this.alive = true;
43
+ }
44
+ attributeChangedCallback(name) {
45
+ if (name === "state") {
46
+ const next = parseState(this.getAttribute("state"));
47
+ const changed = next !== this.currentState;
48
+ this.currentState = next;
49
+ this.paint();
50
+ if (changed && this.alive) this.emitChange();
51
+ return;
52
+ }
53
+ if (name === "default-state" && !this.isControlled && !this.dirty) this.seed();
54
+ this.paint();
55
+ }
56
+ // Matches the host `disabled` attribute and an ancestor `<fieldset disabled>`.
57
+ get isDisabled() {
58
+ return this.matches(":disabled");
59
+ }
60
+ // Controlled mode: the `state` attribute is present and owns the live value.
61
+ get isControlled() {
62
+ return this.hasAttribute("state");
63
+ }
64
+ seed() {
65
+ this.currentState = parseState(
66
+ this.getAttribute("state") ?? this.getAttribute("default-state")
67
+ );
68
+ }
69
+ // Native `change`, fired *after* a value transition applies (user toggle or a
70
+ // controlled `state` update) — the post-apply counterpart to the cancelable,
71
+ // pre-apply `statechange`. Bubbles like a native checkbox; not composed (the
72
+ // element is light-DOM, so it reaches a wrapping form / listener directly).
73
+ emitChange() {
74
+ this.dispatchEvent(new Event("change", { bubbles: true }));
75
+ }
76
+ toggle(_e) {
77
+ if (this.isDisabled) return;
78
+ const prev = this.currentState;
79
+ const next = prev === "checked" ? "unchecked" : "checked";
80
+ const ok = this.dispatchEvent(
81
+ new CustomEvent("statechange", {
82
+ cancelable: true,
83
+ bubbles: true,
84
+ composed: true,
85
+ detail: { next, prev }
86
+ })
87
+ );
88
+ if (this.isControlled) return;
89
+ if (ok) {
90
+ this.currentState = next;
91
+ this.dirty = true;
92
+ this.paint();
93
+ this.emitChange();
94
+ }
95
+ }
96
+ paint() {
97
+ const i = this.internals;
98
+ if (!i) return;
99
+ i.states.delete("checked");
100
+ i.states.delete("indeterminate");
101
+ if (this.currentState === "indeterminate") i.states.add("indeterminate");
102
+ else if (this.currentState === "checked") i.states.add("checked");
103
+ i.ariaChecked = this.currentState === "indeterminate" ? "mixed" : this.currentState === "checked" ? "true" : "false";
104
+ i.setFormValue?.(
105
+ this.currentState === "checked" ? this.getAttribute("value") ?? "on" : null,
106
+ this.currentState
107
+ );
108
+ }
109
+ formResetCallback() {
110
+ this.seed();
111
+ this.paint();
112
+ }
113
+ formStateRestoreCallback(state) {
114
+ this.currentState = parseState(state);
115
+ this.paint();
116
+ }
117
+ formDisabledCallback() {
118
+ this.paint();
119
+ }
120
+ }
121
+ function register_a_checkbox() {
122
+ if (typeof customElements === "undefined") return;
123
+ if (!customElements.get("a-checkbox"))
124
+ customElements.define("a-checkbox", ACheckboxElement);
125
+ }
126
+ register_a_checkbox();
127
+ export {
128
+ ACheckboxElement,
129
+ register_a_checkbox
130
+ };
@@ -0,0 +1 @@
1
+ @layer anta{a-expander{--expander-text: var(--text-2);--expander-text-hover: var(--text-1);--expander-bg-secondary: var(--bg-2);--expander-border-secondary: var(--border-5);--expander-bg-primary: var(--bg-4);--expander-border-primary: var(--border-4);--expander-bg: var(--expander-bg-secondary);--expander-border: var(--expander-border-secondary);--expander-bg-hover-lift: .01;--expander-bg-hover: oklch(from var(--expander-bg) calc(l + var(--expander-bg-hover-lift)) c h);--expander-gutter: 24px;--_expander-chevron: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'%3e%3cpath stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m9 18 6-6-6-6'/%3e%3c/svg%3e");display:block;width:100%;box-sizing:border-box;border:1px solid var(--expander-border);border-radius:4px;color:var(--expander-text);background:var(--expander-bg);transition:background-color .1s ease}a-expander:not(:defined) a-expander-details{display:none}a-expander:not(:defined)[state=open] a-expander-details,a-expander:not(:defined)[default-state=open] a-expander-details{display:block}a-expander:not(:defined) a-expander-summary{position:relative;padding:6px 4px 6px var(--expander-gutter)}a-expander:not(:defined) a-expander-summary:before{content:"";position:absolute;left:calc(var(--expander-gutter) - 16px - 2px);top:50%;width:16px;height:16px;transform:translateY(-50%);background-color:currentColor;-webkit-mask:var(--_expander-chevron) center / contain no-repeat;mask:var(--_expander-chevron) center / contain no-repeat;opacity:.6}a-expander-summary{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-decoration-line:var(--_summary-underline, none);text-decoration-style:dotted;text-decoration-color:color-mix(in oklch,currentColor 75%,transparent);text-decoration-thickness:1px;text-underline-offset:3px;transition:color .15s ease}a-expander-details{display:block;padding:2px 4px 6px var(--expander-gutter);line-height:20px}a-expander[priority=primary]{--expander-bg: var(--expander-bg-primary);--expander-border: var(--expander-border-primary)}a-expander[priority=tertiary]{--expander-bg: transparent;--expander-border: transparent}a-expander:not([disabled]):not(:state(open)):hover{background:var(--expander-bg-hover)}a-expander:not([disabled]):not(:state(open)):active{background:var(--expander-bg)}a-expander[priority=tertiary][outdent]{--expander-gutter: 0px}a-expander[tone=brand]{--expander-text: var(--text-2-brand);--expander-text-hover: var(--text-1-brand);--expander-bg-secondary: var(--bg-2-brand);--expander-border-secondary: var(--border-5-brand);--expander-bg-primary: var(--bg-4-brand);--expander-border-primary: var(--border-4-brand)}a-expander[tone=info]{--expander-text: var(--text-2-info);--expander-text-hover: var(--text-1-info);--expander-bg-secondary: var(--bg-2-info);--expander-border-secondary: var(--border-5-info);--expander-bg-primary: var(--bg-4-info);--expander-border-primary: var(--border-4-info)}a-expander[tone=success]{--expander-text: var(--text-2-success);--expander-text-hover: var(--text-1-success);--expander-bg-secondary: var(--bg-2-success);--expander-border-secondary: var(--border-5-success);--expander-bg-primary: var(--bg-4-success);--expander-border-primary: var(--border-4-success)}a-expander[tone=warning]{--expander-text: var(--text-2-warning);--expander-text-hover: var(--text-1-warning);--expander-bg-secondary: var(--bg-2-warning);--expander-border-secondary: var(--border-5-warning);--expander-bg-primary: var(--bg-4-warning);--expander-border-primary: var(--border-4-warning)}a-expander[tone=critical]{--expander-text: var(--text-2-critical);--expander-text-hover: var(--text-1-critical);--expander-bg-secondary: var(--bg-2-critical);--expander-border-secondary: var(--border-5-critical);--expander-bg-primary: var(--bg-4-critical);--expander-border-primary: var(--border-4-critical)}a-expander[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical]){--expander-tone-source: attr(tone type(<color>), currentColor);--expander-text: oklch(from var(--expander-tone-source) .41 .15 h);--expander-text-hover: oklch(from var(--expander-tone-source) .33 .15 h);--expander-bg-secondary: oklch(from var(--expander-tone-source) .99 .006 h);--expander-border-secondary: oklch(from var(--expander-tone-source) .93 .03 h);--expander-bg-primary: oklch(from var(--expander-tone-source) .955 .02 h);--expander-border-primary: oklch(from var(--expander-tone-source) .895 .045 h)}.dark a-expander[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical]){--expander-text: oklch(from var(--expander-tone-source) .75 .11 h);--expander-text-hover: oklch(from var(--expander-tone-source) .82 .1 h);--expander-bg-secondary: oklch(from var(--expander-tone-source) .13 .02 h);--expander-border-secondary: oklch(from var(--expander-tone-source) .22 .05 h);--expander-bg-primary: oklch(from var(--expander-tone-source) .19 .04 h);--expander-border-primary: oklch(from var(--expander-tone-source) .26 .07 h)}a-expander[disabled],a-expander[disabled][tone]{--expander-text: #afa9b1}.dark a-expander[disabled],.dark a-expander[disabled][tone]{--expander-text: #635b65}}
@@ -0,0 +1,28 @@
1
+ import { HTMLElementBase } from '../anta_helpers';
2
+ import './a-expander.css';
3
+ export declare class AExpanderElement extends HTMLElementBase {
4
+ static observedAttributes: string[];
5
+ private summary;
6
+ private region;
7
+ private internals?;
8
+ constructor();
9
+ /** Controlled mode: the `state` attribute is present and owns the state. */
10
+ private get controlled();
11
+ /** The currently *applied* state (read from the shadow, the source of truth
12
+ * for what's painted). */
13
+ private get current();
14
+ connectedCallback(): void;
15
+ attributeChangedCallback(name: string): void;
16
+ private syncDisabled;
17
+ /** Apply state to the shadow internals (idempotent; reads the host, writes
18
+ * only the shadow). `aria-expanded` is both the a11y signal and the CSS
19
+ * state hook; `inert` keeps collapsed content out of the tab order and the
20
+ * accessibility tree. */
21
+ private applyState;
22
+ /** Compute the requested next state, announce it (cancelable, *before*
23
+ * applying), then — uncontrolled only, and only if not vetoed — apply it.
24
+ * Controlled: never self-apply; the consumer answers via the `state`
25
+ * attribute. See STATEFUL-COMPONENTS.md. */
26
+ private onSummaryClick;
27
+ }
28
+ export declare function register_a_expander(): void;
@@ -0,0 +1,237 @@
1
+ import { HTMLElementBase } from "../anta_helpers";
2
+ import "./a-expander.css";
3
+ const ANIM_MS = 200;
4
+ const SUMMARY_TYPE_SCALE = {
5
+ "1": [28, 32],
6
+ "2": [24, 28],
7
+ "3": [20, 24],
8
+ "4": [17, 20],
9
+ "5": [15, 20],
10
+ "6": [13, 16]
11
+ };
12
+ const SUMMARY_FONT_WEIGHT = 584.62;
13
+ const SUMMARY_LEVEL_RULES = Object.entries(SUMMARY_TYPE_SCALE).map(
14
+ ([level, [size, line]]) => `:host([level="${level}"]) button { font-size: ${size}px; line-height: ${line}px; }`
15
+ ).join("\n ");
16
+ const SHADOW_STYLE = `
17
+ :host { display: block; }
18
+
19
+ .header {
20
+ display: flex;
21
+ /* Stretch so the trigger button (and the actions row) fill the full host
22
+ height \u2014 the title's hit area is the whole header, not just the text. */
23
+ align-items: stretch;
24
+ }
25
+
26
+ slot[name="actions"] { display: none; }
27
+ .header.has-actions slot[name="actions"] {
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 2px;
31
+ flex-shrink: 0;
32
+ margin-inline-start: 8px;
33
+ /* Inset from the right edge (the host has no padding of its own). */
34
+ margin-inline-end: 4px;
35
+ }
36
+
37
+ button {
38
+ appearance: none;
39
+ -webkit-appearance: none;
40
+ background: none;
41
+ border: none;
42
+ margin: 0;
43
+ /* The button is the full-bleed header: it spans the host's width (flex: 1)
44
+ and height (.header stretch). The title is inset by --expander-gutter
45
+ (the chevron lives absolutely inside that gutter, below \u2014 it never pushes
46
+ the title), so the title + the body share one inset value; 6px block
47
+ gives the header a touch more height. Restyle it edge-to-edge via
48
+ ::part(summary). */
49
+ position: relative;
50
+ padding: 6px 4px 6px var(--expander-gutter);
51
+ flex: 1;
52
+ min-width: 0;
53
+ font: inherit;
54
+ color: inherit;
55
+ text-align: left;
56
+
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 0;
60
+ cursor: pointer;
61
+ user-select: none;
62
+ border-radius: 2px;
63
+ outline-offset: 4px;
64
+ font-size: ${SUMMARY_TYPE_SCALE["5"][0]}px;
65
+ line-height: ${SUMMARY_TYPE_SCALE["5"][1]}px;
66
+ font-weight: ${SUMMARY_FONT_WEIGHT};
67
+ letter-spacing: 0;
68
+ }
69
+
70
+ ${SUMMARY_LEVEL_RULES}
71
+
72
+ /* The chevron sits absolutely INSIDE the gutter \u2014 out of flow, so it never
73
+ pushes the title. Its right edge lands 2px before the gutter (left = gutter
74
+ - 16px - 2px), leaving a 2px gap before the title; at the default 24px
75
+ gutter the chevron box is [6px, 22px], and on outdent (gutter 0) it
76
+ auto-hangs at -18px in the negative gutter. One var drives the title inset,
77
+ the body inset, and the chevron position. */
78
+ button::before {
79
+ content: '';
80
+ position: absolute;
81
+ left: calc(var(--expander-gutter) - 16px - 2px);
82
+ top: 50%;
83
+ width: 16px;
84
+ height: 16px;
85
+ background-color: currentColor;
86
+ -webkit-mask-image: var(--_expander-chevron);
87
+ mask-image: var(--_expander-chevron);
88
+ -webkit-mask-size: contain;
89
+ mask-size: contain;
90
+ -webkit-mask-repeat: no-repeat;
91
+ mask-repeat: no-repeat;
92
+ -webkit-mask-position: center;
93
+ mask-position: center;
94
+ opacity: 0.6;
95
+ /* translateY centers it; the open state adds rotate (same transform so the
96
+ transition animates only the rotation). */
97
+ transform: translateY(-50%);
98
+ transition: transform 150ms ease, opacity 150ms ease, background-color 150ms ease;
99
+ }
100
+ button:enabled:hover::before { opacity: 1; }
101
+ button[aria-expanded="true"]::before { transform: translateY(-50%) rotate(90deg); opacity: 1; }
102
+
103
+ button:enabled:hover { color: var(--expander-text-hover); }
104
+ :host([priority="tertiary"]) button:enabled:hover { --_summary-underline: underline; }
105
+ button:enabled:active { color: var(--expander-text); }
106
+ button:disabled { cursor: default; }
107
+ button:enabled:not([aria-expanded="true"]):active::before {
108
+ opacity: 0.6;
109
+ }
110
+
111
+ .region {
112
+ display: grid;
113
+ grid-template-rows: 0fr;
114
+ }
115
+ .header:has(button[aria-expanded="true"]) + .region { grid-template-rows: 1fr; }
116
+
117
+ /* Outdent flush correction: the host keeps its 1px (transparent) border for
118
+ cross-priority box stability, which insets the content-box by 1px. Pull the
119
+ header + region back by that 1px so the title and body land exactly on the
120
+ surrounding content (the chevron, absolutely positioned inside the header,
121
+ rides along). Logical start so it follows RTL. */
122
+ :host([priority="tertiary"][outdent]) .header,
123
+ :host([priority="tertiary"][outdent]) .region {
124
+ margin-inline-start: -1px;
125
+ }
126
+ @media (prefers-reduced-motion: no-preference) {
127
+ .region { transition: grid-template-rows ${ANIM_MS}ms ease; }
128
+ }
129
+
130
+ [part="content"] { display: block; min-height: 0; overflow: clip; }
131
+ .header:has(button[aria-expanded="true"]) + .region [part="content"] {
132
+ overflow: visible;
133
+ transition: overflow 0s ${ANIM_MS}ms;
134
+ transition-behavior: allow-discrete;
135
+ }
136
+ `;
137
+ const parseState = (v) => v === "open" ? "open" : "closed";
138
+ class AExpanderElement extends HTMLElementBase {
139
+ static observedAttributes = ["state", "disabled"];
140
+ summary;
141
+ region;
142
+ // ElementInternals exposes the open/closed state as a custom state
143
+ // (`:state(open)`) for consumer styling — e.g. rotating a chevron that lives
144
+ // in `actions`, or matching an open header to a design. It's NOT a host
145
+ // attribute (declarative-DOM safe: no host mutation, no reconciliation
146
+ // churn), just element-internal state the browser exposes for CSS matching.
147
+ // Guarded for engines without attachInternals / CustomStateSet (no-op there).
148
+ internals;
149
+ constructor() {
150
+ super();
151
+ this.internals = typeof this.attachInternals === "function" ? this.attachInternals() : void 0;
152
+ const shadow = this.attachShadow({ mode: "open" });
153
+ const style = document.createElement("style");
154
+ style.textContent = SHADOW_STYLE;
155
+ this.summary = document.createElement("button");
156
+ this.summary.type = "button";
157
+ this.summary.setAttribute("aria-expanded", "false");
158
+ this.summary.setAttribute("part", "summary");
159
+ const titleSlot = document.createElement("slot");
160
+ titleSlot.name = "title";
161
+ this.summary.append(titleSlot);
162
+ this.summary.addEventListener("click", this.onSummaryClick);
163
+ const header = document.createElement("div");
164
+ header.className = "header";
165
+ const actionsSlot = document.createElement("slot");
166
+ actionsSlot.name = "actions";
167
+ actionsSlot.setAttribute("part", "actions");
168
+ actionsSlot.addEventListener("slotchange", () => {
169
+ header.classList.toggle("has-actions", actionsSlot.assignedElements().length > 0);
170
+ });
171
+ header.append(this.summary, actionsSlot);
172
+ this.region = document.createElement("div");
173
+ this.region.className = "region";
174
+ const content = document.createElement("slot");
175
+ content.setAttribute("part", "content");
176
+ this.region.append(content);
177
+ shadow.append(style, header, this.region);
178
+ }
179
+ /** Controlled mode: the `state` attribute is present and owns the state. */
180
+ get controlled() {
181
+ return this.hasAttribute("state");
182
+ }
183
+ /** The currently *applied* state (read from the shadow, the source of truth
184
+ * for what's painted). */
185
+ get current() {
186
+ return this.summary.getAttribute("aria-expanded") === "true" ? "open" : "closed";
187
+ }
188
+ connectedCallback() {
189
+ this.syncDisabled();
190
+ this.applyState(parseState(this.getAttribute(this.controlled ? "state" : "default-state")));
191
+ }
192
+ attributeChangedCallback(name) {
193
+ if (name === "disabled") this.syncDisabled();
194
+ else if (name === "state" && this.controlled) this.applyState(parseState(this.getAttribute("state")));
195
+ }
196
+ syncDisabled() {
197
+ this.summary.disabled = this.hasAttribute("disabled");
198
+ }
199
+ /** Apply state to the shadow internals (idempotent; reads the host, writes
200
+ * only the shadow). `aria-expanded` is both the a11y signal and the CSS
201
+ * state hook; `inert` keeps collapsed content out of the tab order and the
202
+ * accessibility tree. */
203
+ applyState(state) {
204
+ const open = state === "open";
205
+ this.summary.setAttribute("aria-expanded", open ? "true" : "false");
206
+ this.region.inert = !open;
207
+ try {
208
+ if (open) this.internals?.states.add("open");
209
+ else this.internals?.states.delete("open");
210
+ } catch {
211
+ }
212
+ }
213
+ /** Compute the requested next state, announce it (cancelable, *before*
214
+ * applying), then — uncontrolled only, and only if not vetoed — apply it.
215
+ * Controlled: never self-apply; the consumer answers via the `state`
216
+ * attribute. See STATEFUL-COMPONENTS.md. */
217
+ onSummaryClick = () => {
218
+ const prev = this.current;
219
+ const next = prev === "open" ? "closed" : "open";
220
+ const ok = this.dispatchEvent(
221
+ new CustomEvent("statechange", { cancelable: true, detail: { next, prev } })
222
+ );
223
+ if (this.controlled) return;
224
+ if (ok) this.applyState(next);
225
+ };
226
+ }
227
+ function register_a_expander() {
228
+ if (typeof customElements === "undefined") return;
229
+ if (!customElements.get("a-expander")) {
230
+ customElements.define("a-expander", AExpanderElement);
231
+ }
232
+ }
233
+ register_a_expander();
234
+ export {
235
+ AExpanderElement,
236
+ register_a_expander
237
+ };
@@ -8,6 +8,20 @@ import './a-icon.shapes.css';
8
8
  * CSS: the base rule (`a-icon.css`) sets up the mask compositing, and
9
9
  * the per-shape rules (`a-icon.shapes.css`, generated) supply the
10
10
  * `--icon` URL. Color follows `currentColor`.
11
+ *
12
+ * Sizing notes (`a-icon.css` ships comment-free):
13
+ * - The 16px default lives in a plain `--icon-size: 16px` declaration,
14
+ * never inside `attr()` — Safari has no typed `attr()` for `<length>`,
15
+ * so a default on the attr() path can't resolve there and the
16
+ * var-substituted `width` would collapse to 0 on an empty inline-block.
17
+ * - `<a-icon size="N">` maps to `--icon-size` via CSS Values 5 typed
18
+ * `attr()` (Chrome 133+ / Safari 18.2+) behind an `@supports` guard.
19
+ * Custom properties parse permissively, so without the guard the invalid
20
+ * `attr()` would only fail later, at `width: var(--icon-size)`
21
+ * substitution — collapsing the icon to 0×0 on unsupported engines
22
+ * instead of falling back to the 16px base. The `<Icon size={N}>` JSX
23
+ * wrapper sets `--icon-size` inline as a plain length and is the
24
+ * cross-browser path.
11
25
  */
12
26
  export declare class AIconElement extends HTMLElementBase {
13
27
  }