@antadesign/anta 0.2.2 → 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 +6 -11
- 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/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 +9 -1
- package/dist/elements/a-icon.shapes.js +10 -1
- 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-tooltip.d.ts +43 -11
- package/dist/elements/a-tooltip.js +46 -51
- package/dist/elements/index.d.ts +9 -0
- package/dist/elements/index.js +27 -0
- package/dist/general_types.d.ts +467 -15
- 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
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { HTMLElementBase } from "../anta_helpers";
|
|
2
|
+
import "./a-input.css";
|
|
3
|
+
const FORWARDED = [
|
|
4
|
+
"placeholder",
|
|
5
|
+
"type",
|
|
6
|
+
"name",
|
|
7
|
+
"autocomplete",
|
|
8
|
+
"inputmode",
|
|
9
|
+
"maxlength",
|
|
10
|
+
"minlength",
|
|
11
|
+
"pattern",
|
|
12
|
+
"spellcheck",
|
|
13
|
+
"readonly",
|
|
14
|
+
"required",
|
|
15
|
+
"min",
|
|
16
|
+
"max",
|
|
17
|
+
"step"
|
|
18
|
+
];
|
|
19
|
+
const BOOL_FORWARDED = /* @__PURE__ */ new Set(["readonly", "required"]);
|
|
20
|
+
const CLEAR_TRIGGER = "clearrequest";
|
|
21
|
+
const CLEAR_CLICK_EVENT = "clearclick";
|
|
22
|
+
const CLEAR_INPUT_EVENT = "clearinput";
|
|
23
|
+
const SUPPORTS_FIELD_SIZING = typeof CSS !== "undefined" && !!CSS.supports?.("field-sizing", "content");
|
|
24
|
+
const SHADOW_STYLE = `
|
|
25
|
+
:host {
|
|
26
|
+
--_fs: 15px;
|
|
27
|
+
--_lh: 20px;
|
|
28
|
+
|
|
29
|
+
display: grid;
|
|
30
|
+
grid-template-columns: minmax(0, 1fr);
|
|
31
|
+
row-gap: 4px;
|
|
32
|
+
outline: none;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.label {
|
|
36
|
+
display: none;
|
|
37
|
+
color: var(--input-label);
|
|
38
|
+
font-family: var(--sans-serif);
|
|
39
|
+
font-size: var(--_fs);
|
|
40
|
+
line-height: var(--_lh);
|
|
41
|
+
font-weight: 500;
|
|
42
|
+
}
|
|
43
|
+
.label.has-label { display: block; }
|
|
44
|
+
|
|
45
|
+
.field {
|
|
46
|
+
--_bc: var(--input-border);
|
|
47
|
+
--_bw: 0.5px;
|
|
48
|
+
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
box-sizing: border-box;
|
|
52
|
+
min-height: 28px;
|
|
53
|
+
background: var(--input-bg);
|
|
54
|
+
border-radius: 4px;
|
|
55
|
+
box-shadow: inset 0 0 0 var(--_bw) var(--_bc);
|
|
56
|
+
transition: box-shadow 120ms ease;
|
|
57
|
+
}
|
|
58
|
+
:host([multiline]) .field { align-items: stretch; }
|
|
59
|
+
:host([status]) .field { --_bw: 1px; }
|
|
60
|
+
:host([size="small"]) { --_fs: 13px; --_lh: 16px; }
|
|
61
|
+
:host([size="large"]) { --_fs: 17px; --_lh: 24px; }
|
|
62
|
+
:host([size="small"]) .field { min-height: 24px; }
|
|
63
|
+
:host([size="large"]) .field { min-height: 32px; }
|
|
64
|
+
|
|
65
|
+
@media (hover: hover) and (pointer: fine) {
|
|
66
|
+
:host(:not(:disabled)) .field:hover { --_bc: var(--input-border-hover); }
|
|
67
|
+
}
|
|
68
|
+
.field:has(input:focus, textarea:focus) {
|
|
69
|
+
--_bc: var(--input-border-hover);
|
|
70
|
+
outline: 1px solid var(--focus-ring);
|
|
71
|
+
outline-offset: 1px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
input, textarea {
|
|
75
|
+
flex: 1 1 auto;
|
|
76
|
+
min-width: 0;
|
|
77
|
+
width: 100%;
|
|
78
|
+
box-sizing: border-box;
|
|
79
|
+
border: 0;
|
|
80
|
+
margin: 0;
|
|
81
|
+
padding-block: 0;
|
|
82
|
+
padding-inline: 7px;
|
|
83
|
+
background: transparent;
|
|
84
|
+
outline: none;
|
|
85
|
+
color: var(--input-text);
|
|
86
|
+
font-family: var(--sans-serif);
|
|
87
|
+
font-feature-settings: 'ss02', 'ss05';
|
|
88
|
+
font-size: var(--_fs);
|
|
89
|
+
line-height: var(--_lh);
|
|
90
|
+
font-weight: 400;
|
|
91
|
+
-webkit-appearance: none;
|
|
92
|
+
appearance: none;
|
|
93
|
+
}
|
|
94
|
+
textarea {
|
|
95
|
+
--_pad-block: 4px;
|
|
96
|
+
|
|
97
|
+
resize: none;
|
|
98
|
+
padding-block: var(--_pad-block);
|
|
99
|
+
overflow-y: auto;
|
|
100
|
+
}
|
|
101
|
+
:host([maxrows]:not([rows])) textarea {
|
|
102
|
+
max-height: calc(var(--_lh) * var(--_maxrows) + var(--_pad-block) * 2);
|
|
103
|
+
}
|
|
104
|
+
input::placeholder, textarea::placeholder { color: var(--input-placeholder); opacity: 1; }
|
|
105
|
+
input:disabled, textarea:disabled { cursor: not-allowed; }
|
|
106
|
+
input::-webkit-search-cancel-button,
|
|
107
|
+
input::-webkit-search-decoration,
|
|
108
|
+
input::-webkit-search-results-button,
|
|
109
|
+
input::-webkit-search-results-decoration,
|
|
110
|
+
input::-webkit-inner-spin-button,
|
|
111
|
+
input::-webkit-outer-spin-button { -webkit-appearance: none; appearance: none; display: none; }
|
|
112
|
+
input::-ms-clear, input::-ms-reveal { display: none; }
|
|
113
|
+
|
|
114
|
+
slot[name="leading"], slot[name="trailing"] { display: none; color: var(--input-adornment); }
|
|
115
|
+
.field.has-leading slot[name="leading"],
|
|
116
|
+
.field.has-trailing slot[name="trailing"] {
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
flex-shrink: 0;
|
|
120
|
+
}
|
|
121
|
+
.field.has-trailing slot[name="trailing"] { gap: 2px; }
|
|
122
|
+
|
|
123
|
+
:host([dim-actions]) slot[name="leading"],
|
|
124
|
+
:host([dim-actions]) slot[name="trailing"],
|
|
125
|
+
:host([dim-actions]) slot[name="clear"] {
|
|
126
|
+
opacity: 0.6;
|
|
127
|
+
transition: opacity 120ms ease;
|
|
128
|
+
}
|
|
129
|
+
:host([dim-actions]:not(:disabled)) .field:hover slot[name="leading"],
|
|
130
|
+
:host([dim-actions]:not(:disabled)) .field:hover slot[name="trailing"],
|
|
131
|
+
:host([dim-actions]:not(:disabled)) .field:hover slot[name="clear"],
|
|
132
|
+
:host([dim-actions]:not(:disabled)) .field:focus-within slot[name="leading"],
|
|
133
|
+
:host([dim-actions]:not(:disabled)) .field:focus-within slot[name="trailing"],
|
|
134
|
+
:host([dim-actions]:not(:disabled)) .field:focus-within slot[name="clear"] {
|
|
135
|
+
opacity: 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
:host(:disabled) slot[name="leading"],
|
|
139
|
+
:host(:disabled) slot[name="trailing"] { opacity: 0.5; pointer-events: none; }
|
|
140
|
+
|
|
141
|
+
slot[name="clear"] { display: none; flex-shrink: 0; }
|
|
142
|
+
.field.is-filled slot[name="clear"] { display: flex; align-items: center; }
|
|
143
|
+
:host(:disabled) slot[name="clear"],
|
|
144
|
+
:host([readonly]) slot[name="clear"] { display: none; }
|
|
145
|
+
:host([multiline]) slot[name="clear"] { align-self: flex-start; }
|
|
146
|
+
|
|
147
|
+
.field.has-leading slot[name="leading"] { margin-inline-start: 7px; }
|
|
148
|
+
.field.has-leading input,
|
|
149
|
+
.field.has-leading textarea { padding-inline-start: 5px; }
|
|
150
|
+
:host([multiline]) .field.has-leading slot[name="leading"],
|
|
151
|
+
:host([multiline]) .field.has-trailing slot[name="trailing"] {
|
|
152
|
+
align-items: flex-start;
|
|
153
|
+
padding-top: 2px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.hint {
|
|
157
|
+
display: none;
|
|
158
|
+
gap: 4px;
|
|
159
|
+
align-items: flex-start;
|
|
160
|
+
padding-inline-start: 1px;
|
|
161
|
+
color: var(--input-hint);
|
|
162
|
+
font-family: var(--sans-serif);
|
|
163
|
+
font-size: calc(var(--_fs) - 1px);
|
|
164
|
+
line-height: calc(var(--_lh) - 2px);
|
|
165
|
+
}
|
|
166
|
+
.hint.has-hint { display: flex; }
|
|
167
|
+
`;
|
|
168
|
+
class AInputElement extends HTMLElementBase {
|
|
169
|
+
static formAssociated = true;
|
|
170
|
+
static observedAttributes = [
|
|
171
|
+
...FORWARDED,
|
|
172
|
+
"value",
|
|
173
|
+
"defaultvalue",
|
|
174
|
+
"multiline",
|
|
175
|
+
"rows",
|
|
176
|
+
"maxrows",
|
|
177
|
+
"status",
|
|
178
|
+
"disabled"
|
|
179
|
+
];
|
|
180
|
+
internals;
|
|
181
|
+
field;
|
|
182
|
+
labelBox;
|
|
183
|
+
hintBox;
|
|
184
|
+
labelSlot;
|
|
185
|
+
hintSlot;
|
|
186
|
+
leadingSlot;
|
|
187
|
+
trailingSlot;
|
|
188
|
+
control;
|
|
189
|
+
// The intended value, captured even before the control exists (a framework
|
|
190
|
+
// may set the `value` property before the element connects), so the initial
|
|
191
|
+
// value isn't lost when the control is built.
|
|
192
|
+
pendingValue;
|
|
193
|
+
// True between connect and the first control build, so attribute changes for
|
|
194
|
+
// forwarded props don't try to touch a control that doesn't exist yet.
|
|
195
|
+
ready = false;
|
|
196
|
+
// Set by formDisabledCallback when an ancestor <fieldset disabled> / disabled
|
|
197
|
+
// form turns the field off (the host can't carry a [disabled] attribute itself).
|
|
198
|
+
formDisabled = false;
|
|
199
|
+
constructor() {
|
|
200
|
+
super();
|
|
201
|
+
try {
|
|
202
|
+
this.internals = this.attachInternals?.();
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.warn("a-input: ElementInternals unavailable \u2014 form association disabled.", err);
|
|
205
|
+
}
|
|
206
|
+
const shadow = this.attachShadow({ mode: "open", delegatesFocus: true });
|
|
207
|
+
const style = document.createElement("style");
|
|
208
|
+
style.textContent = SHADOW_STYLE;
|
|
209
|
+
this.labelBox = document.createElement("div");
|
|
210
|
+
this.labelBox.className = "label";
|
|
211
|
+
this.labelBox.setAttribute("part", "label");
|
|
212
|
+
this.labelSlot = document.createElement("slot");
|
|
213
|
+
this.labelSlot.name = "label";
|
|
214
|
+
this.labelBox.append(this.labelSlot);
|
|
215
|
+
this.labelSlot.addEventListener("click", () => this.control?.focus());
|
|
216
|
+
this.labelSlot.addEventListener("slotchange", this.onLabelSlotChange);
|
|
217
|
+
this.field = document.createElement("div");
|
|
218
|
+
this.field.className = "field";
|
|
219
|
+
this.field.setAttribute("part", "field");
|
|
220
|
+
this.leadingSlot = document.createElement("slot");
|
|
221
|
+
this.leadingSlot.name = "leading";
|
|
222
|
+
this.leadingSlot.setAttribute("part", "leading");
|
|
223
|
+
this.trailingSlot = document.createElement("slot");
|
|
224
|
+
this.trailingSlot.name = "trailing";
|
|
225
|
+
this.trailingSlot.setAttribute("part", "trailing");
|
|
226
|
+
this.leadingSlot.addEventListener("slotchange", () => this.field.classList.toggle("has-leading", this.leadingSlot.assignedNodes().length > 0));
|
|
227
|
+
this.trailingSlot.addEventListener("slotchange", () => this.field.classList.toggle("has-trailing", this.trailingSlot.assignedNodes().length > 0));
|
|
228
|
+
const clearSlot = document.createElement("slot");
|
|
229
|
+
clearSlot.name = "clear";
|
|
230
|
+
clearSlot.setAttribute("part", "clear");
|
|
231
|
+
this.field.append(this.leadingSlot, clearSlot, this.trailingSlot);
|
|
232
|
+
this.addEventListener(CLEAR_TRIGGER, () => {
|
|
233
|
+
const proceed = this.dispatchEvent(
|
|
234
|
+
new CustomEvent(CLEAR_CLICK_EVENT, { bubbles: true, cancelable: true })
|
|
235
|
+
);
|
|
236
|
+
if (proceed) this.clear();
|
|
237
|
+
});
|
|
238
|
+
this.hintBox = document.createElement("div");
|
|
239
|
+
this.hintBox.className = "hint";
|
|
240
|
+
this.hintBox.setAttribute("part", "hint");
|
|
241
|
+
this.hintSlot = document.createElement("slot");
|
|
242
|
+
this.hintSlot.name = "hint";
|
|
243
|
+
this.hintSlot.addEventListener("slotchange", this.onHintSlotChange);
|
|
244
|
+
this.hintBox.append(this.hintSlot);
|
|
245
|
+
const extrasSlot = document.createElement("slot");
|
|
246
|
+
shadow.append(style, this.labelBox, this.field, extrasSlot, this.hintBox);
|
|
247
|
+
}
|
|
248
|
+
connectedCallback() {
|
|
249
|
+
if (Object.prototype.hasOwnProperty.call(this, "value")) {
|
|
250
|
+
const v = this.value;
|
|
251
|
+
delete this.value;
|
|
252
|
+
this.value = v;
|
|
253
|
+
}
|
|
254
|
+
if (!this.control) this.buildControl();
|
|
255
|
+
this.ready = true;
|
|
256
|
+
}
|
|
257
|
+
attributeChangedCallback(name, _old, value) {
|
|
258
|
+
if (!this.ready && name !== "value" && name !== "defaultvalue") return;
|
|
259
|
+
if (name === "multiline" || name === "rows" || name === "maxrows") {
|
|
260
|
+
if (!this.control) return;
|
|
261
|
+
const needTextarea = this.hasAttribute("multiline") || this.hasAttribute("rows");
|
|
262
|
+
const isTextarea = this.control instanceof HTMLTextAreaElement;
|
|
263
|
+
if (needTextarea !== isTextarea) this.buildControl(this.value);
|
|
264
|
+
else if (this.control instanceof HTMLTextAreaElement) this.configureTextarea(this.control);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (name === "status") {
|
|
268
|
+
this.syncStatus();
|
|
269
|
+
this.updateValidity();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (name === "disabled") {
|
|
273
|
+
this.syncDisabled();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (name === "value") {
|
|
277
|
+
if (value !== null) this.value = value;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (name === "defaultvalue") return;
|
|
281
|
+
if (this.control) {
|
|
282
|
+
this.forward(name, value);
|
|
283
|
+
this.updateValidity();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/** (Re)build the shadow control from the current attributes. */
|
|
287
|
+
buildControl(initial) {
|
|
288
|
+
const prev = this.control;
|
|
289
|
+
const refocus = prev != null && this.shadowRoot?.activeElement === prev;
|
|
290
|
+
let selStart = null;
|
|
291
|
+
let selEnd = null;
|
|
292
|
+
if (prev) try {
|
|
293
|
+
selStart = prev.selectionStart;
|
|
294
|
+
selEnd = prev.selectionEnd;
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
const multiline = this.hasAttribute("multiline") || this.hasAttribute("rows");
|
|
298
|
+
const next = document.createElement(multiline ? "textarea" : "input");
|
|
299
|
+
next.setAttribute("part", "input");
|
|
300
|
+
if (this.control) this.control.replaceWith(next);
|
|
301
|
+
else this.leadingSlot.after(next);
|
|
302
|
+
this.control = next;
|
|
303
|
+
for (const attr of FORWARDED) {
|
|
304
|
+
if (next instanceof HTMLTextAreaElement && attr === "type") continue;
|
|
305
|
+
this.forward(attr, this.getAttribute(attr));
|
|
306
|
+
}
|
|
307
|
+
this.syncDisabled();
|
|
308
|
+
this.syncStatus();
|
|
309
|
+
this.applyLabelAria();
|
|
310
|
+
this.applyDescriptionAria();
|
|
311
|
+
if (multiline) this.configureTextarea(next);
|
|
312
|
+
const value = initial ?? this.pendingValue ?? this.getAttribute("value") ?? this.getAttribute("defaultvalue") ?? "";
|
|
313
|
+
next.value = value;
|
|
314
|
+
next.addEventListener("input", this.onInput);
|
|
315
|
+
next.addEventListener("change", this.onChange);
|
|
316
|
+
if (refocus) {
|
|
317
|
+
next.focus();
|
|
318
|
+
if (selStart != null) try {
|
|
319
|
+
next.setSelectionRange(selStart, selEnd ?? selStart);
|
|
320
|
+
} catch {
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
this.internals?.setFormValue(value);
|
|
324
|
+
this.updateValidity();
|
|
325
|
+
this.updateFilled();
|
|
326
|
+
this.syncAutoHeight();
|
|
327
|
+
}
|
|
328
|
+
/** Autogrow (no `rows`) via `field-sizing: content` — or the JS fallback where
|
|
329
|
+
* that's unsupported — capped by `maxrows`; a fixed `rows` count switches it
|
|
330
|
+
* off for a constant-height box. */
|
|
331
|
+
configureTextarea(ta) {
|
|
332
|
+
const rows = this.getAttribute("rows");
|
|
333
|
+
const maxrows = this.getAttribute("maxrows");
|
|
334
|
+
if (rows != null) {
|
|
335
|
+
ta.rows = Math.max(1, parseInt(rows, 10) || 1);
|
|
336
|
+
ta.style.setProperty("field-sizing", "fixed");
|
|
337
|
+
ta.style.removeProperty("--_maxrows");
|
|
338
|
+
ta.style.removeProperty("height");
|
|
339
|
+
} else {
|
|
340
|
+
ta.rows = 1;
|
|
341
|
+
if (SUPPORTS_FIELD_SIZING) ta.style.setProperty("field-sizing", "content");
|
|
342
|
+
else ta.style.removeProperty("field-sizing");
|
|
343
|
+
if (maxrows != null) ta.style.setProperty("--_maxrows", String(parseInt(maxrows, 10) || 1));
|
|
344
|
+
else ta.style.removeProperty("--_maxrows");
|
|
345
|
+
this.syncAutoHeight();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** Autogrow fallback for browsers without `field-sizing: content` (Firefox,
|
|
349
|
+
* Safari < 26.2). Grows the shadow textarea to fit its content; the CSS
|
|
350
|
+
* `max-height` cap (when `maxrows` is set) still clamps it and scrolls past.
|
|
351
|
+
* A no-op where `field-sizing` is supported, or when the field isn't
|
|
352
|
+
* autogrowing (fixed `rows`, or not a textarea) — those size via CSS. */
|
|
353
|
+
syncAutoHeight = () => {
|
|
354
|
+
if (SUPPORTS_FIELD_SIZING) return;
|
|
355
|
+
const ta = this.control;
|
|
356
|
+
if (!(ta instanceof HTMLTextAreaElement) || this.getAttribute("rows") != null) return;
|
|
357
|
+
ta.style.height = "auto";
|
|
358
|
+
ta.style.height = `${ta.scrollHeight}px`;
|
|
359
|
+
};
|
|
360
|
+
forward(name, value) {
|
|
361
|
+
const c = this.control;
|
|
362
|
+
if (!c) return;
|
|
363
|
+
if (c instanceof HTMLTextAreaElement && name === "type") return;
|
|
364
|
+
if (BOOL_FORWARDED.has(name)) c.toggleAttribute(name, value != null);
|
|
365
|
+
else if (value == null) c.removeAttribute(name);
|
|
366
|
+
else c.setAttribute(name, value);
|
|
367
|
+
}
|
|
368
|
+
syncDisabled() {
|
|
369
|
+
if (this.control) this.control.disabled = this.hasAttribute("disabled") || this.formDisabled;
|
|
370
|
+
}
|
|
371
|
+
/** Reflect `status` into the control's `aria-invalid` and the `:state(invalid)`
|
|
372
|
+
* custom state. Called on `status` change AND from buildControl — so a field
|
|
373
|
+
* that mounts already `status="critical"` gets `:state(invalid)` on the FIRST
|
|
374
|
+
* paint (the status attributeChangedCallback fires before connect, while
|
|
375
|
+
* `ready` is false and the early-return skips it). */
|
|
376
|
+
syncStatus() {
|
|
377
|
+
const critical = this.getAttribute("status") === "critical";
|
|
378
|
+
this.control?.setAttribute("aria-invalid", critical ? "true" : "false");
|
|
379
|
+
try {
|
|
380
|
+
critical ? this.internals?.states.add("invalid") : this.internals?.states.delete("invalid");
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
onInput = () => {
|
|
385
|
+
const v = this.control?.value ?? "";
|
|
386
|
+
this.internals?.setFormValue(v);
|
|
387
|
+
this.updateValidity();
|
|
388
|
+
this.updateFilled();
|
|
389
|
+
this.syncAutoHeight();
|
|
390
|
+
};
|
|
391
|
+
// `change` is not composed; re-emit one on the host so it escapes the shadow.
|
|
392
|
+
onChange = () => {
|
|
393
|
+
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
394
|
+
};
|
|
395
|
+
onLabelSlotChange = () => {
|
|
396
|
+
this.labelBox.classList.toggle("has-label", this.labelSlot.assignedNodes().length > 0);
|
|
397
|
+
this.applyLabelAria();
|
|
398
|
+
};
|
|
399
|
+
applyLabelAria() {
|
|
400
|
+
const c = this.control;
|
|
401
|
+
if (!c) return;
|
|
402
|
+
if (this.hasAttribute("aria-label") || this.hasAttribute("aria-labelledby")) return;
|
|
403
|
+
const text = this.labelSlot.assignedNodes().map((n) => n.textContent ?? "").join(" ").trim();
|
|
404
|
+
if (text) c.setAttribute("aria-label", text);
|
|
405
|
+
else c.removeAttribute("aria-label");
|
|
406
|
+
}
|
|
407
|
+
onHintSlotChange = () => {
|
|
408
|
+
this.hintBox.classList.toggle("has-hint", this.hintSlot.assignedNodes().length > 0);
|
|
409
|
+
this.applyDescriptionAria();
|
|
410
|
+
};
|
|
411
|
+
applyDescriptionAria() {
|
|
412
|
+
const c = this.control;
|
|
413
|
+
if (!c) return;
|
|
414
|
+
if (this.hasAttribute("aria-description") || this.hasAttribute("aria-describedby")) return;
|
|
415
|
+
const text = this.hintSlot.assignedNodes().map((n) => n.textContent ?? "").join(" ").trim();
|
|
416
|
+
if (text) c.setAttribute("aria-description", text);
|
|
417
|
+
else c.removeAttribute("aria-description");
|
|
418
|
+
}
|
|
419
|
+
updateFilled() {
|
|
420
|
+
const filled = !!this.value;
|
|
421
|
+
this.field.classList.toggle("is-filled", filled);
|
|
422
|
+
try {
|
|
423
|
+
if (filled) this.internals?.states.add("filled");
|
|
424
|
+
else this.internals?.states.delete("filled");
|
|
425
|
+
} catch {
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
updateValidity() {
|
|
429
|
+
const c = this.control;
|
|
430
|
+
if (!this.internals || !c) return;
|
|
431
|
+
if (this.getAttribute("status") === "critical" && c.validity.valid) {
|
|
432
|
+
this.internals.setValidity({ customError: true }, "Invalid value.", c);
|
|
433
|
+
} else {
|
|
434
|
+
this.internals.setValidity(c.validity, c.validationMessage, c);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/** The current text value. Reading prefers the live control; before it's
|
|
438
|
+
* built, the pending/attribute value. */
|
|
439
|
+
get value() {
|
|
440
|
+
if (this.control) return this.control.value;
|
|
441
|
+
return this.pendingValue ?? this.getAttribute("value") ?? this.getAttribute("defaultvalue") ?? "";
|
|
442
|
+
}
|
|
443
|
+
set value(v) {
|
|
444
|
+
this.pendingValue = v;
|
|
445
|
+
if (this.control && this.control.value !== v) this.control.value = v;
|
|
446
|
+
this.internals?.setFormValue(v);
|
|
447
|
+
this.updateValidity();
|
|
448
|
+
this.updateFilled();
|
|
449
|
+
this.syncAutoHeight();
|
|
450
|
+
}
|
|
451
|
+
/** Clear the field and refocus it — fired by the wrapper's clear button.
|
|
452
|
+
* Dispatches `input` + `change` on the host so controlled consumers update. */
|
|
453
|
+
clear() {
|
|
454
|
+
if (!this.control) return;
|
|
455
|
+
this.control.value = "";
|
|
456
|
+
this.internals?.setFormValue("");
|
|
457
|
+
this.updateValidity();
|
|
458
|
+
this.updateFilled();
|
|
459
|
+
this.syncAutoHeight();
|
|
460
|
+
this.dispatchEvent(new Event("input", { bubbles: true }));
|
|
461
|
+
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
462
|
+
this.dispatchEvent(new CustomEvent(CLEAR_INPUT_EVENT, { bubbles: true }));
|
|
463
|
+
this.control.focus();
|
|
464
|
+
}
|
|
465
|
+
// --- Constraint-validation API, proxied from ElementInternals so the host
|
|
466
|
+
// behaves like a native form control (`el.checkValidity()`, `el.validity`). ---
|
|
467
|
+
get validity() {
|
|
468
|
+
return this.internals?.validity;
|
|
469
|
+
}
|
|
470
|
+
get validationMessage() {
|
|
471
|
+
return this.internals?.validationMessage ?? "";
|
|
472
|
+
}
|
|
473
|
+
get willValidate() {
|
|
474
|
+
return this.internals?.willValidate ?? false;
|
|
475
|
+
}
|
|
476
|
+
checkValidity() {
|
|
477
|
+
return this.internals?.checkValidity() ?? true;
|
|
478
|
+
}
|
|
479
|
+
reportValidity() {
|
|
480
|
+
return this.internals?.reportValidity() ?? true;
|
|
481
|
+
}
|
|
482
|
+
/** Form field name — mirrors the `name` attribute, like native `<input>.name`,
|
|
483
|
+
* so `el.name` works (e.g. keying validation messages by field in a form loop). */
|
|
484
|
+
get name() {
|
|
485
|
+
return this.getAttribute("name") ?? "";
|
|
486
|
+
}
|
|
487
|
+
// --- Form-associated callbacks ---
|
|
488
|
+
formResetCallback() {
|
|
489
|
+
this.value = this.getAttribute("defaultvalue") ?? "";
|
|
490
|
+
this.dispatchEvent(new Event("input", { bubbles: true }));
|
|
491
|
+
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
492
|
+
}
|
|
493
|
+
formDisabledCallback(disabled) {
|
|
494
|
+
this.formDisabled = disabled;
|
|
495
|
+
this.syncDisabled();
|
|
496
|
+
}
|
|
497
|
+
formStateRestoreCallback(state) {
|
|
498
|
+
this.value = state ?? "";
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function register_a_input() {
|
|
502
|
+
if (typeof customElements === "undefined") return;
|
|
503
|
+
if (!customElements.get("a-input")) {
|
|
504
|
+
customElements.define("a-input", AInputElement);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
register_a_input();
|
|
508
|
+
export {
|
|
509
|
+
AInputElement,
|
|
510
|
+
register_a_input
|
|
511
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer anta{a-menu-group:not(:defined){display:none}a-menu-group{display:block}a-menu-group-label{display:block;padding:6px 8px 2px;color:var(--text-5);font-size:.75em;font-weight:600;letter-spacing:.06em;text-transform:uppercase;user-select:none}}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { HTMLElementBase } from '../anta_helpers';
|
|
2
|
+
import './a-menu-group.css';
|
|
3
|
+
/**
|
|
4
|
+
* `<a-menu-group>` — a titled section grouping related menu items.
|
|
5
|
+
* No JS / no shadow DOM; styled by `a-menu-group.css`. The group's heading
|
|
6
|
+
* is a non-focusable `<a-menu-group-label>` child rendered by the
|
|
7
|
+
* `MenuGroup` JSX wrapper; `role="group"` + `aria-label` are added there too.
|
|
8
|
+
* Keyboard navigation in `<a-menu>` flattens items across group boundaries,
|
|
9
|
+
* skipping the heading.
|
|
10
|
+
*/
|
|
11
|
+
export declare class AMenuGroupElement extends HTMLElementBase {
|
|
12
|
+
}
|
|
13
|
+
export declare function register_a_menu_group(): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HTMLElementBase } from "../anta_helpers";
|
|
2
|
+
import "./a-menu-group.css";
|
|
3
|
+
class AMenuGroupElement extends HTMLElementBase {
|
|
4
|
+
}
|
|
5
|
+
function register_a_menu_group() {
|
|
6
|
+
if (typeof customElements === "undefined") return;
|
|
7
|
+
if (!customElements.get("a-menu-group")) {
|
|
8
|
+
customElements.define("a-menu-group", AMenuGroupElement);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
register_a_menu_group();
|
|
12
|
+
export {
|
|
13
|
+
AMenuGroupElement,
|
|
14
|
+
register_a_menu_group
|
|
15
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer anta{a-menu-item:not(:defined){display:none}a-menu-item{--menu-item-color: var(--text-2);--menu-item-icon-color: var(--text-3);--menu-item-hover: 5%;--menu-item-active: 9%;--menu-item-timing-in: 60ms ease-in-out;--menu-item-timing-out: .28s ease-out;--menu-item-padding-x: 8px;--menu-item-padding-y: 6px;display:flex;flex-shrink:0;align-items:center;gap:.6ch;box-sizing:border-box;width:100%;min-height:28px;padding-block:var(--menu-item-padding-y);padding-inline:var(--menu-item-padding-x);border-radius:var(--radius-sm, 4px);color:var(--menu-item-color);background:transparent;transition:background-color var(--menu-item-timing-out);font:inherit;font-size:14px;line-height:16px;text-align:left;white-space:nowrap;cursor:pointer;user-select:none;outline:none}a-menu-item:hover,a-menu-item:focus-visible{background:color-mix(in oklch,currentColor var(--menu-item-hover),transparent);transition:background-color var(--menu-item-timing-in)}a-menu-item:active{background:color-mix(in oklch,currentColor var(--menu-item-active),transparent);transition:none}a-menu-item:focus-visible{outline:1px solid var(--focus-ring);outline-offset:1px}a-menu-item>a-menu-item-label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}a-menu-item>a-icon{flex:none;--icon-size: 16px;color:var(--menu-item-icon-color)}a-menu-item:has(>a-icon:first-child:not(:only-child)){padding-inline-start:max(0px,var(--menu-item-padding-x) - 2px)}a-menu-item:has(>a-icon:last-child:not(:only-child)){padding-inline-end:max(0px,var(--menu-item-padding-x) - 2px)}a-menu-item>kbd{flex:none;margin-left:auto;padding-left:12px;color:var(--text-5);font:inherit;font-size:1em;letter-spacing:.1ch;white-space:nowrap}a-menu-item[submenu]>a-icon:last-of-type{margin-left:auto}a-menu-item[submenu]>a-icon:last-of-type,a-menu-item>a-icon:last-child:not(:only-child){position:relative;inset-inline-end:-2px}a-menu-item[tone=brand],a-menu-item[tone=info],a-menu-item[tone=success],a-menu-item[tone=warning],a-menu-item[tone=critical]{--menu-item-icon-color: var(--menu-item-color)}a-menu-item[tone=brand]{--menu-item-color: var(--text-2-brand)}a-menu-item[tone=info]{--menu-item-color: var(--text-2-info)}a-menu-item[tone=success]{--menu-item-color: var(--text-2-success)}a-menu-item[tone=warning]{--menu-item-color: var(--text-2-warning)}a-menu-item[tone=critical]{--menu-item-color: var(--text-2-critical)}a-menu-item[disabled]{--menu-item-color: var(--text-5);--menu-item-icon-color: var(--text-5);cursor:default;pointer-events:none}.dark a-menu-item{--menu-item-hover: 10%;--menu-item-active: 15%}}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { HTMLElementBase } from '../anta_helpers';
|
|
2
|
+
import './a-menu-item.css';
|
|
3
|
+
declare global {
|
|
4
|
+
interface Document {
|
|
5
|
+
hasKeyListenerForAMenuItem?: boolean;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* `<a-menu-item>` — a single row inside an `<a-menu>`.
|
|
10
|
+
*
|
|
11
|
+
* No shadow DOM: its content (leading icon, label, trailing `kbd` /
|
|
12
|
+
* chevron) is slotted light DOM, styled entirely from `a-menu-item.css`.
|
|
13
|
+
* The element carries almost no logic — the parent `<a-menu>` owns
|
|
14
|
+
* click delegation, keyboard navigation, and the close contract. This
|
|
15
|
+
* class exists so the menu can identify items via `instanceof` and so
|
|
16
|
+
* Enter / Space on a focused item synthesizes a click (the single
|
|
17
|
+
* activation path that flows through the menu's click delegation).
|
|
18
|
+
*
|
|
19
|
+
* Static ARIA (`role="menuitem"`, `tabindex`, `aria-haspopup` and the
|
|
20
|
+
* `aria-expanded="false"` baseline on submenu parents) is added by the
|
|
21
|
+
* `MenuItem` JSX wrapper, never here — the element must stay re-renderable
|
|
22
|
+
* from any reactive engine without churning host attributes. The one dynamic
|
|
23
|
+
* bit, live `aria-expanded` on a submenu parent, is reflected by the nested
|
|
24
|
+
* `a-menu` element, which owns that state (see `reflectExpanded` there).
|
|
25
|
+
*
|
|
26
|
+
* Styling notes (`a-menu-item.css` ships comment-free):
|
|
27
|
+
* - `a-menu-item:not(:defined)` is hidden against the pre-upgrade flash
|
|
28
|
+
* (items would render inline in the page before registration).
|
|
29
|
+
* - The `--menu-item-*` custom properties are INTERNAL plumbing for the
|
|
30
|
+
* tone × dark matrix, not a public theming API: `--menu-item-color` is the
|
|
31
|
+
* text color and the hover/active tint mixes from `currentColor` at the
|
|
32
|
+
* `--menu-item-hover` / `--menu-item-active` percentages, so it tracks the
|
|
33
|
+
* tone for free (and toned rows don't read heavier than gray ones). Dark
|
|
34
|
+
* mode just raises the percentages.
|
|
35
|
+
* - Focus ring mirrors a-button: 1px outline at +1px offset, sitting in the
|
|
36
|
+
* surface's 4px padding; pairs with the hover background so keyboard focus
|
|
37
|
+
* reads as tint + ring.
|
|
38
|
+
* - Optical side padding (same idea as a-button): a non-only icon at an edge
|
|
39
|
+
* is trimmed ~2px on that side. Submenu items keep symmetric padding —
|
|
40
|
+
* the nested `<a-menu>` is the actual last child, so the trim rule doesn't
|
|
41
|
+
* fire — and the chevron / a trailing icon is instead nudged toward the
|
|
42
|
+
* edge with relative positioning (visual only, no reflow).
|
|
43
|
+
*/
|
|
44
|
+
export declare class AMenuItemElement extends HTMLElementBase {
|
|
45
|
+
connectedCallback(): void;
|
|
46
|
+
}
|
|
47
|
+
export declare function register_a_menu_item(): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { HTMLElementBase } from "../anta_helpers";
|
|
2
|
+
import "./a-menu-item.css";
|
|
3
|
+
class AMenuItemElement extends HTMLElementBase {
|
|
4
|
+
connectedCallback() {
|
|
5
|
+
const doc = this.doc;
|
|
6
|
+
if (!doc.hasKeyListenerForAMenuItem) {
|
|
7
|
+
doc.addEventListener("keydown", handleKeyDown, true);
|
|
8
|
+
doc.hasKeyListenerForAMenuItem = true;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function handleKeyDown(e) {
|
|
13
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
14
|
+
const el = e.target?.closest?.("a-menu-item");
|
|
15
|
+
if (!el) return;
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
if (el.hasAttribute("disabled")) return;
|
|
18
|
+
el.click();
|
|
19
|
+
}
|
|
20
|
+
function register_a_menu_item() {
|
|
21
|
+
if (typeof customElements === "undefined") return;
|
|
22
|
+
if (!customElements.get("a-menu-item")) {
|
|
23
|
+
customElements.define("a-menu-item", AMenuItemElement);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
register_a_menu_item();
|
|
27
|
+
export {
|
|
28
|
+
AMenuItemElement,
|
|
29
|
+
register_a_menu_item
|
|
30
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer anta{a-menu-separator:not(:defined){display:none}a-menu-separator{display:block;flex-shrink:0;height:1px;margin:2px 4px;background:var(--border-5)}}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { HTMLElementBase } from '../anta_helpers';
|
|
2
|
+
import './a-menu-separator.css';
|
|
3
|
+
/**
|
|
4
|
+
* `<a-menu-separator>` — a thin divider between groups of menu items.
|
|
5
|
+
* No JS / no shadow DOM; styled entirely by `a-menu-separator.css`. The
|
|
6
|
+
* trivial class exists only so importing this module registers the tag and
|
|
7
|
+
* pulls its CSS along the granular entry point. `role="separator"` is added
|
|
8
|
+
* by the `MenuSeparator` JSX wrapper. The hairline uses `--border-5` — the
|
|
9
|
+
* most subtle border token, already mode-adaptive, so no `.dark` override.
|
|
10
|
+
*/
|
|
11
|
+
export declare class AMenuSeparatorElement extends HTMLElementBase {
|
|
12
|
+
}
|
|
13
|
+
export declare function register_a_menu_separator(): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HTMLElementBase } from "../anta_helpers";
|
|
2
|
+
import "./a-menu-separator.css";
|
|
3
|
+
class AMenuSeparatorElement extends HTMLElementBase {
|
|
4
|
+
}
|
|
5
|
+
function register_a_menu_separator() {
|
|
6
|
+
if (typeof customElements === "undefined") return;
|
|
7
|
+
if (!customElements.get("a-menu-separator")) {
|
|
8
|
+
customElements.define("a-menu-separator", AMenuSeparatorElement);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
register_a_menu_separator();
|
|
12
|
+
export {
|
|
13
|
+
AMenuSeparatorElement,
|
|
14
|
+
register_a_menu_separator
|
|
15
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer anta{a-menu:not(:defined){display:none}a-menu{--menu-bg: color-mix(in oklch, var(--bg-1) 90%, transparent);--menu-radius: var(--radius-md, 8px);--menu-border: 1px solid var(--border-3);--menu-shadow: 0 2px 6px color-mix(in oklch, var(--text-1) 8%, transparent), 0 8px 24px color-mix(in oklch, var(--text-1) 10%, transparent);--menu-padding: 4px;--menu-min-width: 88px;--menu-backdrop-filter: blur(20px)}.dark a-menu{--menu-bg: color-mix(in oklch, var(--bg-1) 78%, transparent);--menu-shadow: 0 2px 8px color-mix(in oklch, black 50%, transparent), 0 12px 32px color-mix(in oklch, black 40%, transparent), inset 0 0 0 1px color-mix(in oklch, white 8%, transparent)}}
|