@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.
- package/README.md +14 -0
- package/dist/anta_helpers.d.ts +39 -1
- package/dist/anta_helpers.js +30 -2
- package/dist/components/Button.d.ts +7 -4
- package/dist/components/Button.js +12 -14
- package/dist/components/Checkbox.d.ts +97 -0
- package/dist/components/Checkbox.js +77 -0
- package/dist/components/Expander.d.ts +74 -0
- package/dist/components/Expander.js +53 -0
- package/dist/components/Input.d.ts +159 -0
- package/dist/components/Input.js +150 -0
- package/dist/components/Menu.d.ts +70 -0
- package/dist/components/Menu.js +42 -0
- package/dist/components/MenuGroup.d.ts +24 -0
- package/dist/components/MenuGroup.js +19 -0
- package/dist/components/MenuItem.d.ts +50 -0
- package/dist/components/MenuItem.js +41 -0
- package/dist/components/MenuSeparator.d.ts +14 -0
- package/dist/components/MenuSeparator.js +7 -0
- package/dist/components/Progress.d.ts +12 -6
- package/dist/components/Progress.js +7 -4
- package/dist/components/Radio.d.ts +37 -0
- package/dist/components/Radio.js +33 -0
- package/dist/components/RadioGroup.d.ts +119 -0
- package/dist/components/RadioGroup.js +108 -0
- package/dist/components/Tag.d.ts +38 -5
- package/dist/components/Tag.js +9 -5
- package/dist/components/Text.d.ts +27 -12
- package/dist/components/Text.js +6 -3
- package/dist/components/Title.d.ts +10 -1
- package/dist/components/Tooltip.d.ts +19 -9
- package/dist/components/Tooltip.js +2 -2
- package/dist/elements/a-button.css +1 -1
- package/dist/elements/a-button.d.ts +56 -0
- package/dist/elements/a-button.js +13 -11
- package/dist/elements/a-checkbox.css +1 -0
- package/dist/elements/a-checkbox.d.ts +52 -0
- package/dist/elements/a-checkbox.js +130 -0
- package/dist/elements/a-expander.css +1 -0
- package/dist/elements/a-expander.d.ts +28 -0
- package/dist/elements/a-expander.js +237 -0
- package/dist/elements/a-icon.d.ts +14 -0
- package/dist/elements/a-icon.shapes.css +1 -1
- package/dist/elements/a-icon.shapes.d.ts +10 -2
- package/dist/elements/a-icon.shapes.js +12 -3
- package/dist/elements/a-input.css +1 -0
- package/dist/elements/a-input.d.ts +68 -0
- package/dist/elements/a-input.js +511 -0
- package/dist/elements/a-menu-group.css +1 -0
- package/dist/elements/a-menu-group.d.ts +13 -0
- package/dist/elements/a-menu-group.js +15 -0
- package/dist/elements/a-menu-item.css +1 -0
- package/dist/elements/a-menu-item.d.ts +47 -0
- package/dist/elements/a-menu-item.js +30 -0
- package/dist/elements/a-menu-separator.css +1 -0
- package/dist/elements/a-menu-separator.d.ts +13 -0
- package/dist/elements/a-menu-separator.js +15 -0
- package/dist/elements/a-menu.css +1 -0
- package/dist/elements/a-menu.d.ts +171 -0
- package/dist/elements/a-menu.js +714 -0
- package/dist/elements/a-progress.css +1 -1
- package/dist/elements/a-progress.d.ts +12 -0
- package/dist/elements/a-progress.js +1 -0
- package/dist/elements/a-radio-group.css +1 -0
- package/dist/elements/a-radio-group.d.ts +33 -0
- package/dist/elements/a-radio-group.js +160 -0
- package/dist/elements/a-radio.css +1 -0
- package/dist/elements/a-radio.d.ts +14 -0
- package/dist/elements/a-radio.js +46 -0
- package/dist/elements/a-tag.css +1 -1
- package/dist/elements/a-text.css +1 -1
- package/dist/elements/a-text.d.ts +42 -3
- package/dist/elements/a-text.js +73 -33
- package/dist/elements/a-title.css +1 -1
- package/dist/elements/a-tooltip.d.ts +65 -15
- package/dist/elements/a-tooltip.js +155 -115
- package/dist/elements/index.d.ts +9 -0
- package/dist/elements/index.js +27 -0
- package/dist/general_types.d.ts +471 -19
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/jsx-runtime.d.ts +42 -7
- package/dist/jsx-runtime.js +14 -2
- package/dist/tokens.css +1 -1
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|