@akonwi/mica 0.8.0 → 0.10.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 +5 -0
- package/THIRD_PARTY_LICENSES.md +28 -0
- package/avatar.d.ts +10 -0
- package/avatar.js +301 -0
- package/drawer.d.ts +3 -2
- package/drawer.js +74 -10
- package/mica.css +367 -15
- package/package.json +14 -2
- package/tabs.d.ts +2 -1
- package/tabs.js +139 -17
- package/types/react.d.ts +7 -1
package/README.md
CHANGED
|
@@ -65,6 +65,11 @@ Every component states where its behavior comes from
|
|
|
65
65
|
`select.js`, `toast.js`. Each is a tiny standalone module that enhances
|
|
66
66
|
working markup — never renders it. There is no shared runtime.
|
|
67
67
|
|
|
68
|
+
`m-avatar` accepts authored initials or a direct native `img`, both CSS-only.
|
|
69
|
+
`avatar.js` is a separate opt-in visual enhancement: importing it locally
|
|
70
|
+
derives deterministic color and features from the `blobatar` seed without a
|
|
71
|
+
network request.
|
|
72
|
+
|
|
68
73
|
## Your CSS always wins
|
|
69
74
|
|
|
70
75
|
All of mica ships inside `@layer mica.*`. Unlayered user CSS beats layered
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Third-party licenses
|
|
2
|
+
|
|
3
|
+
## Blobatar 2.4.0
|
|
4
|
+
|
|
5
|
+
Mica's optional `avatar.js` module incorporates code from
|
|
6
|
+
[Blobatar](https://github.com/Alain00/blobatar).
|
|
7
|
+
|
|
8
|
+
MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Alain
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
package/avatar.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* Type declarations for mica/avatar.js — side-effect module.
|
|
2
|
+
* Registers <m-avatar> and locally generates Blobatar-derived paint for its
|
|
3
|
+
* `blobatar` seed. Authored initials remain the no-module fallback.
|
|
4
|
+
*/
|
|
5
|
+
declare global {
|
|
6
|
+
interface HTMLElementTagNameMap {
|
|
7
|
+
"m-avatar": HTMLElement;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export {};
|
package/avatar.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/*! mica/avatar.js — generated by tools/build-avatar.ts.
|
|
2
|
+
* Includes Blobatar 2.4.0 (MIT); see THIRD_PARTY_LICENSES.md. */
|
|
3
|
+
// node_modules/blobatar/dist/index.js
|
|
4
|
+
function L({ l: t, c: n, h: e }) {
|
|
5
|
+
let o = e * Math.PI / 180, a = n * Math.cos(o), r = n * Math.sin(o), s = t + 0.3963377774 * a + 0.2158037573 * r, c = t - 0.1055613458 * a - 0.0638541728 * r, i = t - 0.0894841775 * a - 1.291485548 * r, m = s * s * s, l = c * c * c, u = i * i * i;
|
|
6
|
+
return [4.0767416621 * m - 3.3077115913 * l + 0.2309699292 * u, -1.2684380046 * m + 2.6097574011 * l - 0.3413193965 * u, -0.0041960863 * m - 0.7034186147 * l + 1.707614701 * u];
|
|
7
|
+
}
|
|
8
|
+
var q = (t) => t.every((n) => n >= -0.0001 && n <= 1.0001);
|
|
9
|
+
function K(t) {
|
|
10
|
+
let n = L(t);
|
|
11
|
+
if (!q(n)) {
|
|
12
|
+
let e = 0, o = t.c;
|
|
13
|
+
for (let a = 0;a < 12; a++) {
|
|
14
|
+
let r = (e + o) / 2;
|
|
15
|
+
if (q(L({ ...t, c: r })))
|
|
16
|
+
e = r;
|
|
17
|
+
else
|
|
18
|
+
o = r;
|
|
19
|
+
}
|
|
20
|
+
n = L({ ...t, c: e });
|
|
21
|
+
}
|
|
22
|
+
return n.map((e) => Math.min(1, Math.max(0, e)));
|
|
23
|
+
}
|
|
24
|
+
function D(t) {
|
|
25
|
+
let [n, e, o] = K(t);
|
|
26
|
+
return 0.2126 * n + 0.7152 * e + 0.0722 * o;
|
|
27
|
+
}
|
|
28
|
+
function k(t, n) {
|
|
29
|
+
let e = D(t), o = D(n);
|
|
30
|
+
return (Math.max(e, o) + 0.05) / (Math.min(e, o) + 0.05);
|
|
31
|
+
}
|
|
32
|
+
function B(t, n, e) {
|
|
33
|
+
if (k(t, n) >= e)
|
|
34
|
+
return t;
|
|
35
|
+
let o = t.l >= n.l ? 1 : -1;
|
|
36
|
+
for (let s of [o, -o]) {
|
|
37
|
+
let c = { ...t };
|
|
38
|
+
for (let i = 0;i < 60; i++) {
|
|
39
|
+
if (c.l = Math.min(1, Math.max(0, c.l + s * 0.02)), k(c, n) >= e)
|
|
40
|
+
return c;
|
|
41
|
+
if (c.l === 0 || c.l === 1)
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
let a = { ...t, l: 0, c: 0 }, r = { ...t, l: 1, c: 0 };
|
|
46
|
+
return k(a, n) >= k(r, n) ? a : r;
|
|
47
|
+
}
|
|
48
|
+
function P(t) {
|
|
49
|
+
return "#" + K(t).map((n) => {
|
|
50
|
+
let e = n <= 0.0031308 ? 12.92 * n : 1.055 * Math.pow(n, 0.4166666666666667) - 0.055;
|
|
51
|
+
return Math.round(e * 255).toString(16).padStart(2, "0");
|
|
52
|
+
}).join("");
|
|
53
|
+
}
|
|
54
|
+
var Z = [[0.2, { l: 0.86, c: 0.085 }], [0.36, { l: 0.9, c: 0.028 }], [0.62, { l: 0.73, c: 0.135 }], [0.8, { l: 0.62, c: 0.165 }], [0.93, { l: 0.87, c: 0.16 }], [1, { l: 0.34, c: 0.035 }]];
|
|
55
|
+
var Et = (t) => Z.find(([n]) => t < n)?.[1] ?? Z[0][1];
|
|
56
|
+
var U = { l: 0.145, c: 0, h: 0 };
|
|
57
|
+
var Q = 1.5;
|
|
58
|
+
var Lt = (t, n) => {
|
|
59
|
+
let e = Et(n), o = B({ l: e.l, c: e.c, h: t }, U, Q);
|
|
60
|
+
return { bg: { l: 0.965, c: 0.01, h: t }, head: o, eye: o.l >= 0.5 ? { l: 0.17, c: 0.02, h: t } : { l: 0.97, c: 0.012, h: t } };
|
|
61
|
+
};
|
|
62
|
+
var X = [["head", "bg", 1.25], ["eye", "head", 4.5]];
|
|
63
|
+
function Y(t, n = true, e = 0) {
|
|
64
|
+
let o = Lt(t, e);
|
|
65
|
+
if (n)
|
|
66
|
+
for (let [a, r, s] of X)
|
|
67
|
+
o[a] = B(o[a], o[r], s);
|
|
68
|
+
return o;
|
|
69
|
+
}
|
|
70
|
+
function w(t, n = true, e = 0) {
|
|
71
|
+
let o = Y(t, n, e), a = {};
|
|
72
|
+
for (let r in o)
|
|
73
|
+
a[r] = P(o[r]);
|
|
74
|
+
return a;
|
|
75
|
+
}
|
|
76
|
+
var y = (t) => {
|
|
77
|
+
let n = Math.round(t * 100) / 100;
|
|
78
|
+
return Object.is(n, -0) ? "0" : String(n);
|
|
79
|
+
};
|
|
80
|
+
function T({ cx: t, cy: n, rx: e, ry: o, n: a = 4, rot: r = 0 }) {
|
|
81
|
+
let s = Math.min(1, (8 * Math.pow(2, -1 / a) - 4) / 3), c = e, i = o, m = c * s, l = i * s, u = [[c, 0], [c, l], [m, i], [0, i], [-m, i], [-c, l], [-c, 0], [-c, -l], [-m, -i], [0, -i], [m, -i], [c, -l], [c, 0]], x = r * Math.PI / 180, p = Math.cos(x), d = Math.sin(x), h = (b) => {
|
|
82
|
+
let [g, M] = u[b];
|
|
83
|
+
return `${y(t + g * p - M * d)} ${y(n + g * d + M * p)}`;
|
|
84
|
+
}, f = `M${h(0)}`;
|
|
85
|
+
for (let b = 1;b < 13; b += 3)
|
|
86
|
+
f += `C${h(b)} ${h(b + 1)} ${h(b + 2)}`;
|
|
87
|
+
return f + "Z";
|
|
88
|
+
}
|
|
89
|
+
function G(t, n, e, o, a, r = 0) {
|
|
90
|
+
let s = a.length, c = r * Math.PI / 180, i = a.map((u, x) => {
|
|
91
|
+
let p = c + 2 * Math.PI * x / s;
|
|
92
|
+
return [t + e * u * Math.cos(p), n + o * u * Math.sin(p)];
|
|
93
|
+
}), m = (u) => i[(u % s + s) % s], l = `M${y(m(0)[0])} ${y(m(0)[1])}`;
|
|
94
|
+
for (let u = 0;u < s; u++) {
|
|
95
|
+
let [x, p] = m(u - 1), [d, h] = m(u), [f, b] = m(u + 1), [g, M] = m(u + 2);
|
|
96
|
+
l += `C${y(d + (f - x) / 6)} ${y(h + (b - p) / 6)} ${y(f - (g - d) / 6)} ${y(b - (M - h) / 6)} ${y(f)} ${y(b)}`;
|
|
97
|
+
}
|
|
98
|
+
return l + "Z";
|
|
99
|
+
}
|
|
100
|
+
function J({ cx: t, cy: n, rx: e, ry: o, sides: a, round: r = 0.3, rot: s = 0 }) {
|
|
101
|
+
let c = r > 0 ? r < 1 ? r / 2 : 0.5 : 0, i = s * Math.PI / 180 - Math.PI / 2, m = Array.from({ length: a }, (p, d) => {
|
|
102
|
+
let h = i + 2 * Math.PI * d / a;
|
|
103
|
+
return [t + e * Math.cos(h), n + o * Math.sin(h)];
|
|
104
|
+
}), l = (p) => m[(p % a + a) % a], u = (p, d) => {
|
|
105
|
+
let [h, f] = l(p), [b, g] = l(d);
|
|
106
|
+
return `${y(h + (b - h) * c)} ${y(f + (g - f) * c)}`;
|
|
107
|
+
}, x = `M${u(0, -1)}`;
|
|
108
|
+
for (let p = 0;p < a; p++) {
|
|
109
|
+
let [d, h] = l(p);
|
|
110
|
+
if (x += `Q${y(d)} ${y(h)} ${u(p, p + 1)}`, c < 0.5)
|
|
111
|
+
x += `L${u(p + 1, p)}`;
|
|
112
|
+
}
|
|
113
|
+
return x + "Z";
|
|
114
|
+
}
|
|
115
|
+
function W(t, n, e, o) {
|
|
116
|
+
let a = y(t - e), r = y(t + e);
|
|
117
|
+
return `M${a} ${y(n - o)}H${r}V${y(n + o)}H${a}Z`;
|
|
118
|
+
}
|
|
119
|
+
function tt(t, n, e, o, a) {
|
|
120
|
+
let r = Math.max(1.05, a), s = e * Math.sqrt(1 - 1 / (r * r)), c = n - o / r, i = n - r * o, m = s * 0.14, l = c + 0.86 * (i - c);
|
|
121
|
+
return `M${y(t - s)} ${y(c)}L${y(t - m)} ${y(l)}Q${y(t)} ${y(i)} ${y(t + m)} ${y(l)}L${y(t + s)} ${y(c)}Z`;
|
|
122
|
+
}
|
|
123
|
+
function I(t, n) {
|
|
124
|
+
for (let e = 0;e < n.length; e++)
|
|
125
|
+
t = Math.imul(t ^ n[e], 3432918353), t = t << 13 | t >>> 19;
|
|
126
|
+
return t;
|
|
127
|
+
}
|
|
128
|
+
function wt(t) {
|
|
129
|
+
return t = Math.imul(t ^ t >>> 16, 2246822507), t = Math.imul(t ^ t >>> 13, 3266489909), (t ^ t >>> 16) >>> 0;
|
|
130
|
+
}
|
|
131
|
+
var nt = new TextEncoder;
|
|
132
|
+
function et(t) {
|
|
133
|
+
return t.normalize("NFC").trim().toLowerCase();
|
|
134
|
+
}
|
|
135
|
+
function rt(t, n = true) {
|
|
136
|
+
let e = n ? et(t) : t;
|
|
137
|
+
return I(1779033703 ^ e.length, nt.encode(e));
|
|
138
|
+
}
|
|
139
|
+
function v(t, n) {
|
|
140
|
+
return wt(I(I(t, Uint8Array.of(255)), nt.encode(n))) / 4294967296;
|
|
141
|
+
}
|
|
142
|
+
function A(t, n = true, e) {
|
|
143
|
+
let o = rt(t, n), a = (r) => {
|
|
144
|
+
let s = e?.[r], c = Array.isArray(s) ? s[Math.floor(v(o, r) * s.length)] : s;
|
|
145
|
+
return c === undefined ? v(o, r) : c > 0 ? c < 1 ? c : 0.999999 : 0;
|
|
146
|
+
};
|
|
147
|
+
return a.num = (r, s, c) => s + a(r) * (c - s), a.int = (r, s, c) => s + Math.floor(a(r) * (c - s + 1)), a.pick = (r, s) => s[Math.floor(a(r) * s.length)], a.bool = (r, s = 0.5) => a(r) < s, a.jitter = (r, s) => (a(r) * 2 - 1) * s, a;
|
|
148
|
+
}
|
|
149
|
+
function ot(t, n, e) {
|
|
150
|
+
let o = n.expression;
|
|
151
|
+
if (e || !o)
|
|
152
|
+
return { l: t, wrap: "" };
|
|
153
|
+
return o.bake(t, o.p);
|
|
154
|
+
}
|
|
155
|
+
var It = (t, n) => n?.tint ? n.tint(t, n.p) : t;
|
|
156
|
+
var at = (t, n) => n ? `<g transform="${n}">${t}</g>` : t;
|
|
157
|
+
var vt = (t) => t.replace(/[&<>]/g, (n) => n === "&" ? "&" : n === "<" ? "<" : ">");
|
|
158
|
+
function S(t, n) {
|
|
159
|
+
let e = A(t, n.normalize ?? true, n.traits);
|
|
160
|
+
return { t: e, palette: { ...w(n.hue ?? e.num("hue", 0, 360), n.contrast ?? true, n.tone ?? e("tone")), ...n.palette } };
|
|
161
|
+
}
|
|
162
|
+
var At = (t) => t.title ? `<title>${vt(t.title)}</title>` : "";
|
|
163
|
+
function st(t, n, e) {
|
|
164
|
+
let o = n.background ?? t.background;
|
|
165
|
+
if (o === false)
|
|
166
|
+
return;
|
|
167
|
+
return { d: o === "square" ? "M0 0H100V100H0Z" : T({ cx: 50, cy: 50, rx: 50, ry: 50, n: o === "circle" ? 2 : 6 }), fill: e.bg };
|
|
168
|
+
}
|
|
169
|
+
var Rt = (t) => t ? `<path d="${t.d}" fill="${t.fill}"/>` : "";
|
|
170
|
+
function ct(t) {
|
|
171
|
+
return (n, e = {}) => {
|
|
172
|
+
let { t: o, palette: a } = S(n, e), r = It(a, e.expression), s = e.size ? ` width="${e.size}" height="${e.size}"` : "", c = ot(t.layout(o), e), i = At(e) + Rt(st(t, e, r)) + at(t.render(c.l, r), c.wrap);
|
|
173
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"${s}>${i}</svg>`;
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
var ut = (t, n, e) => {
|
|
177
|
+
let o = n.rx, a = t.num("eye.rx", 0.075, 0.105) * o, r = t.num("eye.ratio", 1.9, 3.2), s = t.num("eye.scale", 0.78, 1.24), c = t.num("eye.stretch", 0.85, 1.18), i = t.num("eye.gap", 0.1, 0.24) * o, m = a * Math.max(1, s), l = a * r * Math.max(1, s * c), u = m + o * 0.03 + i, x = t.jitter("gaze.x", 0.09) * e.rx, p = t.num("gaze.y", -0.2, 0.08) * e.ry, d = t.jitter("eye.dy", 0.04) * e.ry, h = Math.hypot(m, l), f = Math.hypot((Math.abs(x) + u + h) / e.rx, (Math.abs(p) + Math.abs(d) + h) / e.ry), b = f > 0.9 ? 0.9 / f : 1, g = a * b, M = g * r, F = u * b, Pt = Math.max(0, Math.min(1, i / l)), Tt = Math.min(12, Math.asin(Pt) * 180 / Math.PI), j = t.num("eye.lean", -1, 1) * Tt, Bt = Math.max(-12, Math.min(12, j + t.jitter("eye.lean2", 3.5))), z = e.cx + x * b, C = e.cy + p * b;
|
|
178
|
+
return [{ cx: z - F, cy: C, rx: g, ry: M, n: t.num("eye.n", 3.5, 6), rot: j }, { cx: z + F, cy: C + d * b, rx: g * s, ry: M * s * c, n: t.num("eye.n", 3.5, 6), rot: Bt }];
|
|
179
|
+
};
|
|
180
|
+
function mt(t, n) {
|
|
181
|
+
let e = (r) => (t.find(([, s]) => r < s) ?? t[t.length - 1])[0];
|
|
182
|
+
function o(r) {
|
|
183
|
+
let s = e(r("shape")), c = r.num("body.r", 31, 38) * s.core, i = { cx: 50 + r.jitter("body.x", 1.5), cy: 50 + r.jitter("body.y", 1.5), rx: c, ry: c * r.num("body.ratio", 0.92, 1.08), n: r.num("body.n", 1.9, 2.5), rot: 0, radii: Array.from({ length: r.int("body.pts", 6, 8) }, (u, x) => 1 + r.jitter(`body.r${x}`, 0.16)) };
|
|
184
|
+
s.body?.(r, i);
|
|
185
|
+
let m = s.face?.(i) ?? i, l = { petals: [], extra: [] };
|
|
186
|
+
return s.decorate?.(r, i, l), { shape: s.name, draw: s.path, body: i, face: m, petals: l.petals, extra: l.extra, eyes: n(r, i, m) };
|
|
187
|
+
}
|
|
188
|
+
function a(r, s, c) {
|
|
189
|
+
let i = (u) => Math.round(u * 100) / 100, m = (u, x) => {
|
|
190
|
+
let p = `<path d="${T(u)}"/>`;
|
|
191
|
+
return c ? `<g class="mo-eye" style="--mo-wrap:${x ? 1 : -1};--mo-lean:${i(u.rot)};transform-origin:${i(u.cx)}px ${i(u.cy)}px">${p}</g>` : p;
|
|
192
|
+
}, l = `<g fill="${s.head}">` + r.petals.map((u) => `<circle cx="${i(u.cx)}" cy="${i(u.cy)}" r="${i(u.r)}"/>`).join("") + r.extra.map((u) => `<path d="${u}"/>`).join("") + `<path d="${r.draw ? r.draw(r.body) : T(r.body)}"/></g><g fill="${s.eye}"${c ? ' class="mo-eyes"' : ""}>` + r.eyes.map(m).join("") + "</g>";
|
|
193
|
+
return c ? `<g class="mo-breathe"><g class="mo-bob">${l}</g></g>` : l;
|
|
194
|
+
}
|
|
195
|
+
return { layout: o, render: a, background: false };
|
|
196
|
+
}
|
|
197
|
+
var lt = (t) => J(t);
|
|
198
|
+
var pt = (t) => G(t.cx, t.cy, t.rx, t.ry, t.radii, t.rot);
|
|
199
|
+
var R = (t) => (n) => ({ cx: n.cx, cy: n.cy, rx: n.rx * t, ry: n.ry * t });
|
|
200
|
+
var yt = (t) => R(Math.min(...t.radii) * 0.95)(t);
|
|
201
|
+
var Ft = (t) => R(0.84)(t);
|
|
202
|
+
var xt = { name: "round", core: 1 };
|
|
203
|
+
var bt = { name: "organic", core: 0.98, path: pt, face: yt };
|
|
204
|
+
var ht = { name: "boxy", core: 0.86, body: (t, n) => {
|
|
205
|
+
n.n = t.num("body.n", 3.4, 6), n.rot = t.num("body.rot", -20, 20);
|
|
206
|
+
} };
|
|
207
|
+
var dt = { name: "capsule", core: 1.02, body: (t, n) => {
|
|
208
|
+
n.ry *= t.num("capsule.squat", 0.55, 0.68);
|
|
209
|
+
}, face: R(0.94), decorate: (t, n, e) => {
|
|
210
|
+
for (let o of [-1, 1])
|
|
211
|
+
e.petals.push({ cx: n.cx + o * (n.rx - n.ry), cy: n.cy, r: n.ry });
|
|
212
|
+
}, path: (t) => W(t.cx, t.cy, t.rx - t.ry, t.ry) };
|
|
213
|
+
var ft = { name: "nub", core: 0.88, decorate: (t, n, e) => {
|
|
214
|
+
let o = t.int("nub.n", 1, 2);
|
|
215
|
+
for (let a = 0;a < o; a++) {
|
|
216
|
+
let r = t.num(`nub.a${a}`, 0, 2 * Math.PI);
|
|
217
|
+
e.petals.push({ cx: n.cx + Math.cos(r) * n.rx * 0.88, cy: n.cy + Math.sin(r) * n.rx * 0.88, r: n.rx * t.num(`nub.r${a}`, 0.24, 0.4) });
|
|
218
|
+
}
|
|
219
|
+
} };
|
|
220
|
+
var gt = { name: "cloud", core: 0.78, face: yt, path: pt, decorate: (t, n, e) => {
|
|
221
|
+
let o = t.int("cloud.n", 4, 6);
|
|
222
|
+
for (let a = 0;a < o; a++) {
|
|
223
|
+
let r = Math.PI + Math.PI * (a + 0.5) / o;
|
|
224
|
+
e.petals.push({ cx: n.cx + Math.cos(r) * n.rx * 0.8, cy: n.cy + Math.sin(r) * n.rx * 0.5, r: n.rx * t.num(`cloud.r${a}`, 0.44, 0.62) });
|
|
225
|
+
}
|
|
226
|
+
} };
|
|
227
|
+
var Mt = { name: "droplet", core: 0.78, body: (t, n) => {
|
|
228
|
+
n.cy += 0.22 * n.ry, n.n = 2;
|
|
229
|
+
}, face: (t) => ({ cx: t.cx, cy: t.cy + t.ry * 0.05, rx: t.rx * 0.88, ry: t.ry * 0.88 }), decorate: (t, n, e) => {
|
|
230
|
+
e.extra.push(tt(n.cx, n.cy, n.rx, n.ry, t.num("droplet.tip", 1.4, 1.65)));
|
|
231
|
+
} };
|
|
232
|
+
var $t = { name: "hexagon", core: 1.05, path: lt, face: Ft, body: (t, n) => {
|
|
233
|
+
n.sides = 6, n.rot = t.num("body.rot", -12, 12), n.round = t.num("poly.round", 0.24, 0.5);
|
|
234
|
+
} };
|
|
235
|
+
var kt = { name: "sun", core: 0.7, decorate: (t, n, e) => {
|
|
236
|
+
let o = t.int("sun.n", 6, 9), a = n.rx * t.num("sun.dist", 1, 1.08), r = n.rx * t.num("sun.r", 0.2, 0.26), s = t.num("sun.rot", 0, 2 * Math.PI);
|
|
237
|
+
for (let c = 0;c < o; c++) {
|
|
238
|
+
let i = s + 2 * Math.PI * c / o;
|
|
239
|
+
e.petals.push({ cx: n.cx + Math.cos(i) * a, cy: n.cy + Math.sin(i) * a, r });
|
|
240
|
+
}
|
|
241
|
+
} };
|
|
242
|
+
var Ot = { name: "triangle", core: 1.15, path: lt, body: (t, n) => {
|
|
243
|
+
n.sides = 3, n.rot = t.num("body.rot", -5, 5), n.round = t.num("poly.round", 0.24, 0.5);
|
|
244
|
+
}, face: (t) => ({ cx: t.cx, cy: t.cy + t.ry * 0.1, rx: t.rx * 0.54, ry: t.ry * 0.36 }) };
|
|
245
|
+
var jt = [[xt, 0.22], [bt, 0.48], [ht, 0.6], [dt, 0.7], [ft, 0.79], [gt, 0.86], [Mt, 0.915], [$t, 0.95], [kt, 0.98], [Ot, 1]];
|
|
246
|
+
var E = mt(jt, ut);
|
|
247
|
+
var zt = ct(E);
|
|
248
|
+
|
|
249
|
+
// tools/avatar.js
|
|
250
|
+
var PROPERTIES = [
|
|
251
|
+
"--m-avatar-content-color",
|
|
252
|
+
"--m-avatar-body-color",
|
|
253
|
+
"--m-avatar-eye-color",
|
|
254
|
+
"--m-avatar-body-inset-block",
|
|
255
|
+
"--m-avatar-body-inset-inline",
|
|
256
|
+
"--m-avatar-body-x",
|
|
257
|
+
"--m-avatar-body-y",
|
|
258
|
+
"--m-avatar-eye-top",
|
|
259
|
+
"--m-avatar-eye-left",
|
|
260
|
+
"--m-avatar-eye-width",
|
|
261
|
+
"--m-avatar-eye-height",
|
|
262
|
+
"--m-avatar-eye-gap",
|
|
263
|
+
"--m-avatar-eye-drop"
|
|
264
|
+
];
|
|
265
|
+
var percent = (value) => `${Math.round(value * 100) / 100}%`;
|
|
266
|
+
|
|
267
|
+
class MAvatar extends HTMLElement {
|
|
268
|
+
static observedAttributes = ["blobatar"];
|
|
269
|
+
connectedCallback() {
|
|
270
|
+
this.#update();
|
|
271
|
+
}
|
|
272
|
+
attributeChangedCallback() {
|
|
273
|
+
if (this.isConnected)
|
|
274
|
+
this.#update();
|
|
275
|
+
}
|
|
276
|
+
#update() {
|
|
277
|
+
const seed = this.getAttribute("blobatar");
|
|
278
|
+
if (seed === null) {
|
|
279
|
+
for (const property of PROPERTIES)
|
|
280
|
+
this.style.removeProperty(property);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const t = A(seed);
|
|
284
|
+
const colors = w(t.num("hue", 0, 360), true, t("tone"));
|
|
285
|
+
this.style.setProperty("--m-avatar-content-color", "transparent");
|
|
286
|
+
this.style.setProperty("--m-avatar-body-color", colors.head);
|
|
287
|
+
this.style.setProperty("--m-avatar-eye-color", colors.eye);
|
|
288
|
+
this.style.setProperty("--m-avatar-body-inset-block", percent(t.num("mica.body.block", 11, 16)));
|
|
289
|
+
this.style.setProperty("--m-avatar-body-inset-inline", percent(t.num("mica.body.inline", 10, 16)));
|
|
290
|
+
this.style.setProperty("--m-avatar-body-x", percent(t.jitter("mica.body.x", 2)));
|
|
291
|
+
this.style.setProperty("--m-avatar-body-y", percent(t.jitter("mica.body.y", 2)));
|
|
292
|
+
this.style.setProperty("--m-avatar-eye-top", percent(t.num("mica.eye.top", 42, 49)));
|
|
293
|
+
this.style.setProperty("--m-avatar-eye-left", percent(t.num("mica.eye.left", 36, 40)));
|
|
294
|
+
this.style.setProperty("--m-avatar-eye-width", percent(t.num("mica.eye.width", 2.5, 3.5)));
|
|
295
|
+
this.style.setProperty("--m-avatar-eye-height", percent(t.num("mica.eye.height", 7.5, 10)));
|
|
296
|
+
this.style.setProperty("--m-avatar-eye-gap", percent(t.num("mica.eye.gap", 19, 23)));
|
|
297
|
+
this.style.setProperty("--m-avatar-eye-drop", percent(t.jitter("mica.eye.drop", 2)));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (!customElements.get("m-avatar"))
|
|
301
|
+
customElements.define("m-avatar", MAvatar);
|
package/drawer.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Type declarations for mica/drawer.js — side-effect module.
|
|
2
|
-
* Swipe-to-dismiss drag physics
|
|
3
|
-
*
|
|
2
|
+
* Swipe-to-dismiss drag physics and a managed overlay scrim for
|
|
3
|
+
* dialog[data-drawer] bottom sheets. No exports; sets
|
|
4
|
+
* :root[data-drawer-gestures] when active.
|
|
4
5
|
*/
|
|
5
6
|
export {};
|
package/drawer.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
/* mica/drawer.js — JS-enhanced: swipe-to-dismiss for
|
|
2
|
-
* bottom sheets on small screens.
|
|
1
|
+
/* mica/drawer.js — JS-enhanced: swipe-to-dismiss and a managed scrim for
|
|
2
|
+
* dialog[data-drawer] bottom sheets on small screens.
|
|
3
3
|
*
|
|
4
4
|
* Enhances working markup, never replaces it: without this module the
|
|
5
5
|
* drawer opens/closes via buttons and Esc; with it, the mobile sheet
|
|
6
6
|
* grows a grab handle (CSS keys off :root[data-drawer-gestures] — an
|
|
7
7
|
* affordance may only appear when the behavior exists) and vaul-style
|
|
8
8
|
* drag physics: the sheet follows a downward drag, resists upward,
|
|
9
|
-
* and dismisses past 1/3 of its height or on a flick.
|
|
9
|
+
* and dismisses past 1/3 of its height or on a flick. A fixed visual
|
|
10
|
+
* overlay replaces the CSS-only scrim so its dimming can follow the drag
|
|
11
|
+
* and fade on iOS, where ::backdrop disappears immediately on close.
|
|
10
12
|
*
|
|
11
13
|
* Drag surface: the handle / header strip. Drag-from-content is
|
|
12
14
|
* deliberately out of scope — it requires scroll-vs-drag heuristics
|
|
@@ -18,7 +20,47 @@ const CLOSE_DISTANCE = 1 / 3; // of sheet height
|
|
|
18
20
|
const CLOSE_VELOCITY = 0.6; // px/ms, downward flick
|
|
19
21
|
const DRAWER = "dialog[data-drawer]";
|
|
20
22
|
|
|
23
|
+
let overlay;
|
|
24
|
+
const dismissCleanup = new WeakMap();
|
|
25
|
+
|
|
26
|
+
const overlayElement = () => {
|
|
27
|
+
if (overlay) return overlay;
|
|
28
|
+
overlay = document.createElement("div");
|
|
29
|
+
overlay.setAttribute("data-mica-drawer-overlay", "");
|
|
30
|
+
overlay.setAttribute("aria-hidden", "true");
|
|
31
|
+
overlay.inert = true;
|
|
32
|
+
overlay.style.setProperty("--m-drawer-overlay-opacity", "0");
|
|
33
|
+
document.body.append(overlay);
|
|
34
|
+
// Commit the transparent state before the first open transition.
|
|
35
|
+
overlay.getBoundingClientRect();
|
|
36
|
+
return overlay;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const setOverlayOpacity = (value) => {
|
|
40
|
+
if (!overlay && value === 0) return;
|
|
41
|
+
overlayElement().style.setProperty(
|
|
42
|
+
"--m-drawer-overlay-opacity",
|
|
43
|
+
String(Math.max(0, Math.min(1, value))),
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const syncOverlay = () => {
|
|
48
|
+
const openDrawer = document.querySelector(`${DRAWER}[open]`);
|
|
49
|
+
// iOS can drop the closed dialog before its translate transition runs,
|
|
50
|
+
// so transitionend never clears the drag-dismiss inline style. Clear a
|
|
51
|
+
// pending exit synchronously on reopen, before the next frame paints.
|
|
52
|
+
if (openDrawer) dismissCleanup.get(openDrawer)?.();
|
|
53
|
+
setOverlayOpacity(openDrawer ? 1 : 0);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
new MutationObserver(syncOverlay).observe(document.documentElement, {
|
|
57
|
+
attributes: true,
|
|
58
|
+
attributeFilter: ["open"],
|
|
59
|
+
subtree: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
21
62
|
document.documentElement.setAttribute("data-drawer-gestures", "");
|
|
63
|
+
syncOverlay();
|
|
22
64
|
|
|
23
65
|
document.addEventListener("pointerdown", (down) => {
|
|
24
66
|
if (!SHEET.matches) return;
|
|
@@ -40,7 +82,10 @@ document.addEventListener("pointerdown", (down) => {
|
|
|
40
82
|
|
|
41
83
|
dialog.setPointerCapture(down.pointerId);
|
|
42
84
|
const baseTransition = dialog.style.transition;
|
|
85
|
+
const scrim = overlayElement();
|
|
86
|
+
const baseOverlayTransition = scrim.style.transition;
|
|
43
87
|
dialog.style.transition = "none";
|
|
88
|
+
scrim.style.transition = "none";
|
|
44
89
|
|
|
45
90
|
const move = (e) => {
|
|
46
91
|
dy = e.clientY - down.clientY;
|
|
@@ -49,6 +94,9 @@ document.addEventListener("pointerdown", (down) => {
|
|
|
49
94
|
lastT = e.timeStamp;
|
|
50
95
|
// follow downward; resist upward
|
|
51
96
|
dialog.style.translate = `0 ${dy > 0 ? dy : dy / 4}px`;
|
|
97
|
+
// Clear by half a sheet: a one-to-one distance mapping was too subtle
|
|
98
|
+
// against dark surfaces (a quarter pull only moved alpha .4 → .3).
|
|
99
|
+
setOverlayOpacity(1 - Math.max(0, dy) / (rect.height / 2));
|
|
52
100
|
};
|
|
53
101
|
|
|
54
102
|
const up = () => {
|
|
@@ -56,22 +104,38 @@ document.addEventListener("pointerdown", (down) => {
|
|
|
56
104
|
dialog.removeEventListener("pointerup", up);
|
|
57
105
|
dialog.removeEventListener("pointercancel", up);
|
|
58
106
|
dialog.style.transition = baseTransition;
|
|
107
|
+
scrim.style.transition = baseOverlayTransition;
|
|
59
108
|
if (dy > rect.height * CLOSE_DISTANCE || velocity > CLOSE_VELOCITY) {
|
|
60
109
|
// continue the motion DOWNWARD off-screen. close() fires its exit
|
|
61
|
-
// (
|
|
110
|
+
// (scrim fade + display flip at the end via allow-discrete);
|
|
62
111
|
// the inline translate overrides the small CSS exit offset so the
|
|
63
112
|
// sheet keeps sliding down instead of snapping back up into view
|
|
64
113
|
// (the flash). Clear it only at transitionend — display:none by
|
|
65
|
-
// then, so clearing is invisible
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
114
|
+
// then, so clearing is invisible. iOS may never fire that event,
|
|
115
|
+
// so a timeout clears it while closed and syncOverlay clears it if
|
|
116
|
+
// the drawer reopens first.
|
|
117
|
+
let cleanupTimer;
|
|
118
|
+
const onTransitionEnd = (e) => {
|
|
119
|
+
if (e.propertyName === "translate") clearDismiss();
|
|
120
|
+
};
|
|
121
|
+
const clearDismiss = () => {
|
|
122
|
+
if (dismissCleanup.get(dialog) !== clearDismiss) return;
|
|
123
|
+
clearTimeout(cleanupTimer);
|
|
124
|
+
dialog.removeEventListener("transitionend", onTransitionEnd);
|
|
125
|
+
dialog.style.translate = "";
|
|
126
|
+
dismissCleanup.delete(dialog);
|
|
127
|
+
};
|
|
128
|
+
dismissCleanup.set(dialog, clearDismiss);
|
|
129
|
+
dialog.addEventListener("transitionend", onTransitionEnd);
|
|
130
|
+
cleanupTimer = setTimeout(() => {
|
|
131
|
+
if (!dialog.open) clearDismiss();
|
|
132
|
+
}, 200);
|
|
133
|
+
setOverlayOpacity(0);
|
|
71
134
|
requestAnimationFrame(() => { dialog.style.translate = "0 100dvh"; });
|
|
72
135
|
dialog.close();
|
|
73
136
|
} else {
|
|
74
137
|
dialog.style.translate = ""; // spring back
|
|
138
|
+
setOverlayOpacity(1);
|
|
75
139
|
}
|
|
76
140
|
};
|
|
77
141
|
|
package/mica.css
CHANGED
|
@@ -771,6 +771,161 @@
|
|
|
771
771
|
--m-badge-numerals: tabular-nums;
|
|
772
772
|
}
|
|
773
773
|
|
|
774
|
+
/* --- avatar: authored initials, optionally Blobatar-painted -------- */
|
|
775
|
+
:where(m-avatar) {
|
|
776
|
+
display: inline-grid;
|
|
777
|
+
place-items: center;
|
|
778
|
+
position: relative;
|
|
779
|
+
flex: none;
|
|
780
|
+
box-sizing: border-box;
|
|
781
|
+
inline-size: var(--avatar-size, 3rem);
|
|
782
|
+
block-size: var(--avatar-size, 3rem);
|
|
783
|
+
overflow: hidden;
|
|
784
|
+
border-radius: var(--radius-md);
|
|
785
|
+
background: var(--color-surface-raised);
|
|
786
|
+
color: var(--m-avatar-content-color, var(--color-text));
|
|
787
|
+
font-size: calc(var(--avatar-size, 3rem) * 0.5);
|
|
788
|
+
font-weight: 600;
|
|
789
|
+
line-height: 1;
|
|
790
|
+
text-transform: uppercase;
|
|
791
|
+
vertical-align: middle;
|
|
792
|
+
white-space: nowrap;
|
|
793
|
+
user-select: none;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
:where(m-avatar > img) {
|
|
797
|
+
position: absolute;
|
|
798
|
+
inset: 0;
|
|
799
|
+
z-index: 1;
|
|
800
|
+
inline-size: 100%;
|
|
801
|
+
block-size: 100%;
|
|
802
|
+
object-fit: cover;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/* Attribute selectors only transport values. The pseudo rules below
|
|
806
|
+
render them, so missing avatar.js leaves authored initials untouched. */
|
|
807
|
+
:where(m-avatar[blobatar]) {
|
|
808
|
+
--m-avatar-decoration: "";
|
|
809
|
+
--m-avatar-render-inset-block: 0;
|
|
810
|
+
--m-avatar-render-inset-inline: 0;
|
|
811
|
+
--m-avatar-render-x: 0;
|
|
812
|
+
--m-avatar-render-y: 0;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
:where(m-avatar[blobatar][contained]) {
|
|
816
|
+
--m-avatar-render-inset-block: var(--m-avatar-body-inset-block);
|
|
817
|
+
--m-avatar-render-inset-inline: var(--m-avatar-body-inset-inline);
|
|
818
|
+
--m-avatar-render-x: var(--m-avatar-body-x);
|
|
819
|
+
--m-avatar-render-y: var(--m-avatar-body-y);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
:where(m-avatar)::before {
|
|
823
|
+
content: var(--m-avatar-decoration, none);
|
|
824
|
+
position: absolute;
|
|
825
|
+
inset-block: var(--m-avatar-render-inset-block);
|
|
826
|
+
inset-inline: var(--m-avatar-render-inset-inline);
|
|
827
|
+
border-radius: inherit;
|
|
828
|
+
background: var(--m-avatar-body-color);
|
|
829
|
+
transform: translate(var(--m-avatar-render-x), var(--m-avatar-render-y));
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
:where(m-avatar)::after {
|
|
833
|
+
content: var(--m-avatar-decoration, none);
|
|
834
|
+
position: absolute;
|
|
835
|
+
inset: 0;
|
|
836
|
+
background:
|
|
837
|
+
radial-gradient(
|
|
838
|
+
ellipse var(--m-avatar-eye-width) var(--m-avatar-eye-height)
|
|
839
|
+
at var(--m-avatar-eye-left) var(--m-avatar-eye-top),
|
|
840
|
+
var(--m-avatar-eye-color) 98%, transparent 100%
|
|
841
|
+
),
|
|
842
|
+
radial-gradient(
|
|
843
|
+
ellipse var(--m-avatar-eye-width) var(--m-avatar-eye-height)
|
|
844
|
+
at calc(var(--m-avatar-eye-left) + var(--m-avatar-eye-gap))
|
|
845
|
+
calc(var(--m-avatar-eye-top) + var(--m-avatar-eye-drop)),
|
|
846
|
+
var(--m-avatar-eye-color) 98%, transparent 100%
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/* --- skeleton: quiet CSS-only loading placeholder ---------------- */
|
|
851
|
+
:where(m-skeleton) {
|
|
852
|
+
display: block;
|
|
853
|
+
flex: none;
|
|
854
|
+
inline-size: var(--skeleton-width, 100%);
|
|
855
|
+
block-size: var(--skeleton-height, 0.75rem);
|
|
856
|
+
overflow: hidden;
|
|
857
|
+
border-radius: var(--skeleton-radius, var(--radius-md));
|
|
858
|
+
background-color: var(--color-surface-raised);
|
|
859
|
+
animation: mica-skeleton-pulse 1.6s ease-in-out infinite alternate;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
@keyframes mica-skeleton-pulse {
|
|
863
|
+
to {
|
|
864
|
+
background-color: var(--color-border);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
@media (prefers-reduced-motion: reduce) {
|
|
869
|
+
:where(m-skeleton) {
|
|
870
|
+
animation: none;
|
|
871
|
+
background-color: var(--color-border);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/* --- stat grid: semantic summaries in a fused frame -------------- */
|
|
876
|
+
:where(dl.stat-grid) {
|
|
877
|
+
display: flex;
|
|
878
|
+
flex-wrap: wrap;
|
|
879
|
+
gap: 1px;
|
|
880
|
+
margin-block: 0;
|
|
881
|
+
padding: 1px;
|
|
882
|
+
overflow: hidden;
|
|
883
|
+
border-radius: var(--radius-md);
|
|
884
|
+
background: var(--color-border);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
:where(dl.stat-grid) > :where(div.stat) {
|
|
888
|
+
display: flex;
|
|
889
|
+
flex-direction: column;
|
|
890
|
+
flex: 1 1 min(100%, 8rem);
|
|
891
|
+
gap: var(--space-sm);
|
|
892
|
+
min-inline-size: 0;
|
|
893
|
+
padding: var(--space-md);
|
|
894
|
+
background: var(--color-surface);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
:where(dl.stat-grid) > :where(div.stat) > :where(dt) {
|
|
898
|
+
color: var(--color-text-muted);
|
|
899
|
+
font-size: 0.75rem;
|
|
900
|
+
font-weight: 500;
|
|
901
|
+
line-height: 1.4;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
:where(dl.stat-grid) > :where(div.stat) > :where(dd) {
|
|
905
|
+
display: flex;
|
|
906
|
+
flex-direction: column;
|
|
907
|
+
flex: 1 1 auto;
|
|
908
|
+
gap: var(--space-2xs);
|
|
909
|
+
min-inline-size: 0;
|
|
910
|
+
margin-block: 0;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
:where(dl.stat-grid) > :where(div.stat) > :where(dd) > :first-child {
|
|
914
|
+
color: var(--color-text);
|
|
915
|
+
font-size: 1.5rem;
|
|
916
|
+
font-weight: 600;
|
|
917
|
+
font-variant-numeric: tabular-nums;
|
|
918
|
+
line-height: 1.2;
|
|
919
|
+
overflow-wrap: anywhere;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
:where(dl.stat-grid) > :where(div.stat) > :where(dd) > :where(small) {
|
|
923
|
+
margin-block-start: auto;
|
|
924
|
+
color: var(--color-text-muted);
|
|
925
|
+
font-size: 0.75rem;
|
|
926
|
+
line-height: 1.4;
|
|
927
|
+
}
|
|
928
|
+
|
|
774
929
|
/* --- content: tables, lists, code, quotes ------------------------ */
|
|
775
930
|
:where(table) {
|
|
776
931
|
inline-size: 100%;
|
|
@@ -907,6 +1062,71 @@
|
|
|
907
1062
|
background: var(--color-danger);
|
|
908
1063
|
}
|
|
909
1064
|
|
|
1065
|
+
/* --- stepper: framed sequence controls --------------------------- *
|
|
1066
|
+
* Mica owns only the fused layout. Consumers own labels, disabled
|
|
1067
|
+
* state, events, routing, and value changes. The frame deliberately does
|
|
1068
|
+
* not clip overflow: native focus rings must remain fully visible.
|
|
1069
|
+
* ------------------------------------------------------------------ */
|
|
1070
|
+
:where(m-stepper) {
|
|
1071
|
+
display: grid;
|
|
1072
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
1073
|
+
align-items: stretch;
|
|
1074
|
+
min-inline-size: 0;
|
|
1075
|
+
border: 1px solid var(--color-border-strong);
|
|
1076
|
+
border-radius: var(--radius-md);
|
|
1077
|
+
background: var(--color-surface);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
:where(m-stepper:has(> [data-reset])) {
|
|
1081
|
+
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
:where(m-stepper) > :where(button) {
|
|
1085
|
+
border: 0;
|
|
1086
|
+
border-radius: 0;
|
|
1087
|
+
background: transparent;
|
|
1088
|
+
|
|
1089
|
+
&:focus-visible {
|
|
1090
|
+
z-index: 1;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
:where(m-stepper) > :where(button:first-child) {
|
|
1095
|
+
border-start-start-radius: inherit;
|
|
1096
|
+
border-end-start-radius: inherit;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
:where(m-stepper) > :where(button:last-child) {
|
|
1100
|
+
border-start-end-radius: inherit;
|
|
1101
|
+
border-end-end-radius: inherit;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
:where(m-stepper) > :where([data-step]) {
|
|
1105
|
+
inline-size: var(--control-height);
|
|
1106
|
+
padding-inline: 0;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
:where(m-stepper) > :where(:nth-child(2)) {
|
|
1110
|
+
display: grid;
|
|
1111
|
+
place-items: center;
|
|
1112
|
+
min-inline-size: 0;
|
|
1113
|
+
block-size: var(--control-height);
|
|
1114
|
+
padding-inline: var(--space-sm);
|
|
1115
|
+
overflow: hidden;
|
|
1116
|
+
border-inline: 1px solid var(--color-border);
|
|
1117
|
+
background: var(--color-surface-raised);
|
|
1118
|
+
color: var(--color-text);
|
|
1119
|
+
font-size: 0.875rem;
|
|
1120
|
+
font-weight: 600;
|
|
1121
|
+
text-align: center;
|
|
1122
|
+
text-overflow: ellipsis;
|
|
1123
|
+
white-space: nowrap;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
:where(m-stepper) > :where([data-reset]) {
|
|
1127
|
+
border-inline-start: 1px solid var(--color-border);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
910
1130
|
/* --- dialog: modal + drawer --------------------------------------- *
|
|
911
1131
|
* The browser brings focus trapping, Esc, top layer, ::backdrop, and
|
|
912
1132
|
* form[method=dialog] closing. Invoker commands (commandfor/command)
|
|
@@ -1132,6 +1352,14 @@
|
|
|
1132
1352
|
border-block-start: 0;
|
|
1133
1353
|
}
|
|
1134
1354
|
|
|
1355
|
+
/* Internal visual layer injected by drawer.js. It stays ordinary page
|
|
1356
|
+
content beneath the dialog's transparent top-layer ::backdrop, so it
|
|
1357
|
+
survives iOS removing that backdrop on close. Hidden outside the
|
|
1358
|
+
mobile-sheet breakpoint; aria-hidden + inert are set by the module. */
|
|
1359
|
+
:where([data-mica-drawer-overlay]) {
|
|
1360
|
+
display: none;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1135
1363
|
/* small screens: the drawer becomes a bottom sheet — shadcn's own
|
|
1136
1364
|
mobile drawer (vaul/radix) is exactly this. Anchored to the bottom
|
|
1137
1365
|
edge via auto top margin: bottom attachment is the one thing iOS
|
|
@@ -1148,7 +1376,7 @@
|
|
|
1148
1376
|
max-inline-size: none;
|
|
1149
1377
|
border-inline-start: 0;
|
|
1150
1378
|
border-block-start: 1px solid var(--color-border);
|
|
1151
|
-
/*
|
|
1379
|
+
/* CSS-only fallback: two shadows painted by the SHEET so they
|
|
1152
1380
|
persist through the exit (display:allow-discrete keeps the box
|
|
1153
1381
|
rendered) — unlike ::backdrop, which iOS drops from the top
|
|
1154
1382
|
layer the instant you close, cutting the scrim. So the dim
|
|
@@ -1184,8 +1412,28 @@
|
|
|
1184
1412
|
0 0 0 100vmax oklch(0 0 0 / 0);
|
|
1185
1413
|
}
|
|
1186
1414
|
}
|
|
1187
|
-
/*
|
|
1188
|
-
|
|
1415
|
+
/* With drawer.js, keep only the bleed slab; its ordinary fixed
|
|
1416
|
+
overlay owns the visible scrim and follows drag progress. */
|
|
1417
|
+
:where(:root[data-drawer-gestures])
|
|
1418
|
+
:where(dialog[data-drawer]) {
|
|
1419
|
+
box-shadow: 0 4rem 0 0 var(--color-surface-overlay);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
:where([data-mica-drawer-overlay]) {
|
|
1423
|
+
display: block;
|
|
1424
|
+
position: fixed;
|
|
1425
|
+
inset: 0;
|
|
1426
|
+
z-index: 50;
|
|
1427
|
+
inline-size: 100lvw;
|
|
1428
|
+
block-size: 100lvh;
|
|
1429
|
+
background: oklch(0 0 0 / 0.4);
|
|
1430
|
+
opacity: var(--m-drawer-overlay-opacity, 0);
|
|
1431
|
+
pointer-events: none;
|
|
1432
|
+
transition: opacity 0.15s;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/* The fallback shadow or managed overlay replaces ::backdrop here;
|
|
1436
|
+
keep it clear so the scrims don't double up or flash on iOS. */
|
|
1189
1437
|
:where(dialog[data-drawer][open])::backdrop {
|
|
1190
1438
|
background: oklch(0 0 0 / 0);
|
|
1191
1439
|
}
|
|
@@ -1431,38 +1679,136 @@
|
|
|
1431
1679
|
/* --- tabs: m-tabs + tabs.js (JS-enhanced) ------------------------------ *
|
|
1432
1680
|
* without the module: inert nav, all panels visible in order —
|
|
1433
1681
|
* complete content. with it: tablist semantics + roving focus.
|
|
1434
|
-
*
|
|
1682
|
+
* default: muted track + surface active tab. variant="underline":
|
|
1683
|
+
* quiet rail + accent selection line; behavior stays identical.
|
|
1435
1684
|
* ------------------------------------------------------------------ */
|
|
1436
1685
|
:where(m-tabs) {
|
|
1686
|
+
--m-tabs-rail-gap: var(--space-2xs);
|
|
1687
|
+
--m-tabs-rail-padding: var(--space-2xs);
|
|
1688
|
+
--m-tabs-rail-border: none;
|
|
1689
|
+
--m-tabs-rail-radius: var(--radius-md);
|
|
1690
|
+
--m-tabs-rail-background: var(--neutral-3);
|
|
1691
|
+
--m-tabs-overflow-cover: var(--neutral-3);
|
|
1692
|
+
--m-tabs-overflow-duration: 0.15s;
|
|
1693
|
+
--m-tabs-overflow-marker: color-mix(in oklch, var(--color-text-muted) 70%, transparent);
|
|
1694
|
+
--m-tabs-overflow-start-gutter: 0px;
|
|
1695
|
+
--m-tabs-overflow-end-gutter: 0px;
|
|
1696
|
+
--m-tabs-overflow-start: 0;
|
|
1697
|
+
--m-tabs-overflow-end: 0;
|
|
1698
|
+
--m-tabs-tab-radius: var(--radius-sm);
|
|
1699
|
+
--m-tabs-tab-edge-padding: var(--space-sm);
|
|
1700
|
+
--m-tabs-tab-focus-offset: var(--focus-ring-offset);
|
|
1701
|
+
--m-tabs-tab-selected-background: var(--color-surface);
|
|
1702
|
+
--m-tabs-tab-selected-shadow: 0 1px 2px oklch(0 0 0 / 0.08);
|
|
1437
1703
|
display: block;
|
|
1438
1704
|
}
|
|
1439
1705
|
|
|
1706
|
+
:where(m-tabs[variant="underline"]) {
|
|
1707
|
+
--m-tabs-rail-gap: 0;
|
|
1708
|
+
--m-tabs-rail-padding: 0;
|
|
1709
|
+
--m-tabs-rail-border: 1px solid var(--color-border);
|
|
1710
|
+
--m-tabs-rail-radius: 0;
|
|
1711
|
+
--m-tabs-rail-background: transparent;
|
|
1712
|
+
--m-tabs-overflow-cover: var(--color-surface);
|
|
1713
|
+
--m-tabs-tab-radius: 0;
|
|
1714
|
+
--m-tabs-tab-edge-padding: 0;
|
|
1715
|
+
--m-tabs-tab-focus-offset: -2px;
|
|
1716
|
+
--m-tabs-tab-selected-background: transparent;
|
|
1717
|
+
--m-tabs-tab-selected-shadow: inset 0 -2px var(--color-accent);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1440
1720
|
:where(m-tabs) > :where(nav) {
|
|
1441
1721
|
display: inline-flex;
|
|
1442
|
-
gap: var(--
|
|
1443
|
-
padding: var(--
|
|
1444
|
-
|
|
1445
|
-
border-radius: var(--radius
|
|
1722
|
+
gap: var(--m-tabs-rail-gap);
|
|
1723
|
+
padding: var(--m-tabs-rail-padding);
|
|
1724
|
+
border-block-end: var(--m-tabs-rail-border);
|
|
1725
|
+
border-radius: var(--m-tabs-rail-radius);
|
|
1726
|
+
background: var(--m-tabs-rail-background);
|
|
1446
1727
|
|
|
1447
1728
|
/* Graceful collapse: labels never wrap inside a tab — when the rail
|
|
1448
|
-
is narrower than its tabs it scrolls horizontally instead.
|
|
1449
|
-
|
|
1450
|
-
|
|
1729
|
+
is narrower than its tabs it scrolls horizontally instead. Before
|
|
1730
|
+
enhancement, the native scrollbar remains the overflow affordance.
|
|
1731
|
+
tabs.js hides it, activates sticky direction markers in clear
|
|
1732
|
+
gutters, and reveals a selected tab within those gutters. Scroll
|
|
1733
|
+
chaining stays contained. */
|
|
1451
1734
|
max-inline-size: 100%;
|
|
1452
1735
|
overflow-x: auto;
|
|
1453
1736
|
overscroll-behavior-x: contain;
|
|
1454
1737
|
scrollbar-width: thin;
|
|
1455
1738
|
|
|
1739
|
+
&[role="tablist"] {
|
|
1740
|
+
scrollbar-width: none;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
&::before,
|
|
1744
|
+
&::after {
|
|
1745
|
+
content: "";
|
|
1746
|
+
position: sticky;
|
|
1747
|
+
z-index: 1;
|
|
1748
|
+
align-self: stretch;
|
|
1749
|
+
background-color: var(--m-tabs-overflow-cover);
|
|
1750
|
+
background-position: center;
|
|
1751
|
+
background-size: 6px 10px;
|
|
1752
|
+
background-repeat: no-repeat;
|
|
1753
|
+
pointer-events: none;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
&::before {
|
|
1757
|
+
inset-inline-start: 0;
|
|
1758
|
+
flex: 0 0 var(--space-md);
|
|
1759
|
+
margin-inline-end: calc(var(--m-tabs-overflow-start-gutter) - var(--space-md));
|
|
1760
|
+
opacity: var(--m-tabs-overflow-start);
|
|
1761
|
+
background-image: conic-gradient(from 45deg at left center,
|
|
1762
|
+
var(--m-tabs-overflow-marker) 0 25%, transparent 0);
|
|
1763
|
+
transition:
|
|
1764
|
+
opacity var(--m-tabs-overflow-duration),
|
|
1765
|
+
margin-inline-end var(--m-tabs-overflow-duration);
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
&::after {
|
|
1769
|
+
inset-inline-end: 0;
|
|
1770
|
+
flex: 0 0 var(--space-md);
|
|
1771
|
+
margin-inline-start: calc(var(--m-tabs-overflow-end-gutter) - var(--space-md));
|
|
1772
|
+
opacity: var(--m-tabs-overflow-end);
|
|
1773
|
+
background-image: conic-gradient(from 225deg at right center,
|
|
1774
|
+
var(--m-tabs-overflow-marker) 0 25%, transparent 0);
|
|
1775
|
+
transition:
|
|
1776
|
+
opacity var(--m-tabs-overflow-duration),
|
|
1777
|
+
margin-inline-start var(--m-tabs-overflow-duration);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
&:dir(rtl)::before {
|
|
1781
|
+
background-image: conic-gradient(from 225deg at right center,
|
|
1782
|
+
var(--m-tabs-overflow-marker) 0 25%, transparent 0);
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
&:dir(rtl)::after {
|
|
1786
|
+
background-image: conic-gradient(from 45deg at left center,
|
|
1787
|
+
var(--m-tabs-overflow-marker) 0 25%, transparent 0);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1456
1790
|
& > :where(button) {
|
|
1457
1791
|
flex-shrink: 0;
|
|
1458
1792
|
white-space: nowrap;
|
|
1459
1793
|
block-size: var(--control-height-sm);
|
|
1460
1794
|
padding-inline: var(--space-sm);
|
|
1461
1795
|
border: 0;
|
|
1462
|
-
border-radius: var(--radius
|
|
1796
|
+
border-radius: var(--m-tabs-tab-radius);
|
|
1463
1797
|
background: transparent;
|
|
1464
1798
|
color: var(--color-text-muted);
|
|
1465
1799
|
|
|
1800
|
+
&:first-of-type {
|
|
1801
|
+
padding-inline-start: var(--m-tabs-tab-edge-padding);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
&:last-of-type {
|
|
1805
|
+
padding-inline-end: var(--m-tabs-tab-edge-padding);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
&:focus-visible {
|
|
1809
|
+
outline-offset: var(--m-tabs-tab-focus-offset);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1466
1812
|
/* :not() guards mirror the base button states (house rule) */
|
|
1467
1813
|
&:hover:not(:disabled, [aria-disabled="true"]),
|
|
1468
1814
|
&:active:not(:disabled, [aria-disabled="true"]) {
|
|
@@ -1471,19 +1817,25 @@
|
|
|
1471
1817
|
}
|
|
1472
1818
|
|
|
1473
1819
|
&[aria-selected="true"] {
|
|
1474
|
-
background: var(--
|
|
1820
|
+
background: var(--m-tabs-tab-selected-background);
|
|
1475
1821
|
color: var(--color-text);
|
|
1476
|
-
box-shadow:
|
|
1822
|
+
box-shadow: var(--m-tabs-tab-selected-shadow);
|
|
1477
1823
|
|
|
1478
1824
|
&:hover:not(:disabled, [aria-disabled="true"]),
|
|
1479
1825
|
&:active:not(:disabled, [aria-disabled="true"]) {
|
|
1480
|
-
background: var(--
|
|
1826
|
+
background: var(--m-tabs-tab-selected-background);
|
|
1481
1827
|
color: var(--color-text);
|
|
1482
1828
|
}
|
|
1483
1829
|
}
|
|
1484
1830
|
}
|
|
1485
1831
|
}
|
|
1486
1832
|
|
|
1833
|
+
/* Vendor pseudo gets its own rule: one unknown selector in a list
|
|
1834
|
+
invalidates the whole list. */
|
|
1835
|
+
:where(m-tabs) > :where(nav[role="tablist"])::-webkit-scrollbar {
|
|
1836
|
+
display: none;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1487
1839
|
:where(m-tabs) > :where(section) {
|
|
1488
1840
|
padding-block: var(--space-md);
|
|
1489
1841
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akonwi/mica",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Custom elements. Native behavior. Nearly no JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"css",
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
"exports": {
|
|
22
22
|
".": "./mica.css",
|
|
23
23
|
"./mica.css": "./mica.css",
|
|
24
|
+
"./avatar.js": {
|
|
25
|
+
"types": "./avatar.d.ts",
|
|
26
|
+
"default": "./avatar.js"
|
|
27
|
+
},
|
|
24
28
|
"./invoker.js": {
|
|
25
29
|
"types": "./invoker.d.ts",
|
|
26
30
|
"default": "./invoker.js"
|
|
@@ -55,6 +59,8 @@
|
|
|
55
59
|
},
|
|
56
60
|
"files": [
|
|
57
61
|
"mica.css",
|
|
62
|
+
"avatar.js",
|
|
63
|
+
"avatar.d.ts",
|
|
58
64
|
"invoker.js",
|
|
59
65
|
"invoker.d.ts",
|
|
60
66
|
"drawer.js",
|
|
@@ -69,16 +75,22 @@
|
|
|
69
75
|
"toast.d.ts",
|
|
70
76
|
"combobox.js",
|
|
71
77
|
"combobox.d.ts",
|
|
72
|
-
"types/react.d.ts"
|
|
78
|
+
"types/react.d.ts",
|
|
79
|
+
"THIRD_PARTY_LICENSES.md"
|
|
73
80
|
],
|
|
74
81
|
"sideEffects": true,
|
|
75
82
|
"devDependencies": {
|
|
76
83
|
"axe-core": "^4.12.1",
|
|
84
|
+
"blobatar": "2.4.0",
|
|
77
85
|
"pixelmatch": "^7.2.0",
|
|
78
86
|
"playwright": "^1.61.1",
|
|
79
87
|
"pngjs": "^7.0.0"
|
|
80
88
|
},
|
|
81
89
|
"scripts": {
|
|
90
|
+
"build:avatar": "bun tools/build-avatar.ts",
|
|
91
|
+
"docs:build": "bun tools/build-docs.ts",
|
|
92
|
+
"docs:check": "bun tools/build-docs.ts --check",
|
|
93
|
+
"docs:serve": "bun run docs:build && bun tools/serve.ts",
|
|
82
94
|
"snapshot": "bun tools/snapshot.ts",
|
|
83
95
|
"snapshot:check": "bun tools/snapshot.ts --check",
|
|
84
96
|
"snapshot:setup": "playwright install chromium webkit"
|
package/tabs.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Type declarations for mica/tabs.js — side-effect module.
|
|
2
|
-
* Registers <m-tabs>: roving tabindex, arrow keys, aria-selected wiring
|
|
2
|
+
* Registers <m-tabs>: roving tabindex, arrow keys, aria-selected wiring,
|
|
3
|
+
* selected-tab reveal, and directional overflow markers.
|
|
3
4
|
*/
|
|
4
5
|
declare global {
|
|
5
6
|
interface HTMLElementTagNameMap {
|
package/tabs.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Enhances working light-DOM markup; never renders it. Without this
|
|
4
4
|
* module, the nav buttons are inert and every panel is visible in
|
|
5
5
|
* order — complete, readable content. With it: real tablist semantics,
|
|
6
|
-
* roving tabindex, arrow-key navigation, automatic activation
|
|
6
|
+
* roving tabindex, arrow-key navigation, automatic activation, and
|
|
7
|
+
* selected-tab reveal with position-aware overflow markers.
|
|
7
8
|
*
|
|
8
9
|
* <m-tabs>
|
|
9
10
|
* <nav>
|
|
@@ -23,10 +24,43 @@ let uid = 0;
|
|
|
23
24
|
class MTabs extends HTMLElement {
|
|
24
25
|
#tabs = [];
|
|
25
26
|
#panels = [];
|
|
27
|
+
#nav = null;
|
|
28
|
+
#overflowing = false;
|
|
29
|
+
#resizeObserver = null;
|
|
30
|
+
#revealFrame = null;
|
|
31
|
+
#onScroll = () => this.#syncOverflow();
|
|
32
|
+
#cancelReveal = () => {
|
|
33
|
+
if (this.#revealFrame !== null) cancelAnimationFrame(this.#revealFrame);
|
|
34
|
+
this.#revealFrame = null;
|
|
35
|
+
};
|
|
36
|
+
#onClick = (event) => {
|
|
37
|
+
const tab = event.target instanceof Element ? event.target.closest("button") : null;
|
|
38
|
+
const index = this.#tabs.indexOf(tab);
|
|
39
|
+
if (index === -1) return;
|
|
40
|
+
this.select(index);
|
|
41
|
+
this.#reveal(index);
|
|
42
|
+
};
|
|
43
|
+
#onKeyDown = (event) => {
|
|
44
|
+
const n = this.#tabs.length;
|
|
45
|
+
const current = this.#tabs.findIndex(
|
|
46
|
+
(tab) => tab.getAttribute("aria-selected") === "true",
|
|
47
|
+
);
|
|
48
|
+
let next = null;
|
|
49
|
+
if (event.key === "ArrowRight") next = (current + 1) % n;
|
|
50
|
+
else if (event.key === "ArrowLeft") next = (current - 1 + n) % n;
|
|
51
|
+
else if (event.key === "Home") next = 0;
|
|
52
|
+
else if (event.key === "End") next = n - 1;
|
|
53
|
+
if (next === null) return;
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
this.select(next); // automatic activation, radix-style
|
|
56
|
+
this.#tabs[next].focus({ preventScroll: true });
|
|
57
|
+
this.#reveal(next);
|
|
58
|
+
};
|
|
26
59
|
|
|
27
60
|
connectedCallback() {
|
|
28
61
|
const nav = this.querySelector(":scope > nav");
|
|
29
62
|
if (!nav) return;
|
|
63
|
+
this.#nav = nav;
|
|
30
64
|
this.#tabs = [...nav.querySelectorAll("button")];
|
|
31
65
|
const sections = [...this.querySelectorAll(":scope > section")];
|
|
32
66
|
this.#panels = this.#tabs.map((b, i) => {
|
|
@@ -44,29 +78,117 @@ class MTabs extends HTMLElement {
|
|
|
44
78
|
panel.setAttribute("aria-labelledby", tab.id);
|
|
45
79
|
panel.tabIndex = 0;
|
|
46
80
|
}
|
|
47
|
-
tab.addEventListener("click", () => this.select(i));
|
|
48
81
|
});
|
|
49
82
|
|
|
50
|
-
nav.addEventListener("
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
55
|
-
let next = null;
|
|
56
|
-
if (e.key === "ArrowRight") next = (current + 1) % n;
|
|
57
|
-
else if (e.key === "ArrowLeft") next = (current - 1 + n) % n;
|
|
58
|
-
else if (e.key === "Home") next = 0;
|
|
59
|
-
else if (e.key === "End") next = n - 1;
|
|
60
|
-
if (next === null) return;
|
|
61
|
-
e.preventDefault();
|
|
62
|
-
this.select(next); // automatic activation, radix-style
|
|
63
|
-
this.#tabs[next].focus();
|
|
64
|
-
});
|
|
83
|
+
nav.addEventListener("click", this.#onClick);
|
|
84
|
+
nav.addEventListener("keydown", this.#onKeyDown);
|
|
85
|
+
nav.addEventListener("pointerdown", this.#cancelReveal, { passive: true });
|
|
86
|
+
nav.addEventListener("wheel", this.#cancelReveal, { passive: true });
|
|
65
87
|
|
|
66
88
|
const initial = this.#tabs.findIndex(
|
|
67
89
|
(t) => t.hasAttribute("selected") || t.getAttribute("aria-selected") === "true",
|
|
68
90
|
);
|
|
69
91
|
this.select(initial === -1 ? 0 : initial);
|
|
92
|
+
|
|
93
|
+
nav.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
94
|
+
this.#resizeObserver = new ResizeObserver(() => this.#measureOverflow());
|
|
95
|
+
this.#resizeObserver.observe(nav);
|
|
96
|
+
this.#tabs.forEach((tab) => this.#resizeObserver.observe(tab));
|
|
97
|
+
this.#measureOverflow();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
disconnectedCallback() {
|
|
101
|
+
this.#nav?.removeEventListener("click", this.#onClick);
|
|
102
|
+
this.#nav?.removeEventListener("keydown", this.#onKeyDown);
|
|
103
|
+
this.#nav?.removeEventListener("pointerdown", this.#cancelReveal);
|
|
104
|
+
this.#nav?.removeEventListener("wheel", this.#cancelReveal);
|
|
105
|
+
this.#nav?.removeEventListener("scroll", this.#onScroll);
|
|
106
|
+
this.#resizeObserver?.disconnect();
|
|
107
|
+
this.#cancelReveal();
|
|
108
|
+
for (const property of [
|
|
109
|
+
"--m-tabs-overflow-start-gutter",
|
|
110
|
+
"--m-tabs-overflow-end-gutter",
|
|
111
|
+
"--m-tabs-overflow-start",
|
|
112
|
+
"--m-tabs-overflow-end",
|
|
113
|
+
]) this.#nav?.style.removeProperty(property);
|
|
114
|
+
this.#nav = null;
|
|
115
|
+
this.#resizeObserver = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#measureOverflow() {
|
|
119
|
+
const nav = this.#nav;
|
|
120
|
+
if (!nav) return;
|
|
121
|
+
const style = getComputedStyle(nav);
|
|
122
|
+
const gap = Number.parseFloat(style.columnGap) || 0;
|
|
123
|
+
const padding = (Number.parseFloat(style.paddingInlineStart) || 0) +
|
|
124
|
+
(Number.parseFloat(style.paddingInlineEnd) || 0);
|
|
125
|
+
const tabsWidth = this.#tabs.reduce(
|
|
126
|
+
(width, tab) => width + tab.getBoundingClientRect().width,
|
|
127
|
+
padding + gap * Math.max(0, this.#tabs.length - 1),
|
|
128
|
+
);
|
|
129
|
+
this.#overflowing = tabsWidth > nav.clientWidth + 1;
|
|
130
|
+
this.#syncOverflow();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#syncOverflow() {
|
|
134
|
+
const nav = this.#nav;
|
|
135
|
+
if (!nav) return;
|
|
136
|
+
const offset = Math.abs(nav.scrollLeft);
|
|
137
|
+
const max = nav.scrollWidth - nav.clientWidth;
|
|
138
|
+
const start = this.#overflowing && offset > 1;
|
|
139
|
+
const end = this.#overflowing && offset < max - 1;
|
|
140
|
+
nav.style.setProperty("--m-tabs-overflow-start", String(start ? 1 : 0));
|
|
141
|
+
nav.style.setProperty("--m-tabs-overflow-end", String(end ? 1 : 0));
|
|
142
|
+
nav.style.setProperty(
|
|
143
|
+
"--m-tabs-overflow-start-gutter",
|
|
144
|
+
start ? "var(--space-md)" : "0px",
|
|
145
|
+
);
|
|
146
|
+
nav.style.setProperty(
|
|
147
|
+
"--m-tabs-overflow-end-gutter",
|
|
148
|
+
end ? "var(--space-md)" : "0px",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#reveal(index) {
|
|
153
|
+
const nav = this.#nav;
|
|
154
|
+
if (!nav || !this.#tabs[index]) return;
|
|
155
|
+
this.#cancelReveal();
|
|
156
|
+
const durations = getComputedStyle(nav, "::before").transitionDuration.split(",");
|
|
157
|
+
const duration = durations.reduce((longest, value) => {
|
|
158
|
+
const time = Number.parseFloat(value) * (value.trim().endsWith("ms") ? 1 : 1000);
|
|
159
|
+
return Math.max(longest, time);
|
|
160
|
+
}, 0);
|
|
161
|
+
// Follow the moving gutters through their transition, plus two frames
|
|
162
|
+
// for the scroll event that activates the newly exposed marker.
|
|
163
|
+
const settleAt = performance.now() + duration + 34;
|
|
164
|
+
let firstFrame = true;
|
|
165
|
+
const reveal = (now) => {
|
|
166
|
+
const nav = this.#nav;
|
|
167
|
+
const tab = this.#tabs[index];
|
|
168
|
+
if (!nav || !tab) return;
|
|
169
|
+
const navRect = nav.getBoundingClientRect();
|
|
170
|
+
const tabRect = tab.getBoundingClientRect();
|
|
171
|
+
const beforeStyle = getComputedStyle(nav, "::before");
|
|
172
|
+
const afterStyle = getComputedStyle(nav, "::after");
|
|
173
|
+
const before = Math.max(0,
|
|
174
|
+
(Number.parseFloat(beforeStyle.inlineSize) || 0) +
|
|
175
|
+
(Number.parseFloat(beforeStyle.marginInlineEnd) || 0));
|
|
176
|
+
const after = Math.max(0,
|
|
177
|
+
(Number.parseFloat(afterStyle.inlineSize) || 0) +
|
|
178
|
+
(Number.parseFloat(afterStyle.marginInlineStart) || 0));
|
|
179
|
+
const rtl = getComputedStyle(nav).direction === "rtl";
|
|
180
|
+
const left = navRect.left + (rtl ? after : before);
|
|
181
|
+
const right = navRect.right - (rtl ? before : after);
|
|
182
|
+
if (tabRect.left < left) nav.scrollBy({ left: tabRect.left - left });
|
|
183
|
+
else if (tabRect.right > right) nav.scrollBy({ left: tabRect.right - right });
|
|
184
|
+
if (firstFrame) {
|
|
185
|
+
this.#syncOverflow();
|
|
186
|
+
firstFrame = false;
|
|
187
|
+
}
|
|
188
|
+
if (now < settleAt) this.#revealFrame = requestAnimationFrame(reveal);
|
|
189
|
+
else this.#revealFrame = null;
|
|
190
|
+
};
|
|
191
|
+
this.#revealFrame = requestAnimationFrame(reveal);
|
|
70
192
|
}
|
|
71
193
|
|
|
72
194
|
select(index) {
|
package/types/react.d.ts
CHANGED
|
@@ -51,11 +51,17 @@ declare module 'react' {
|
|
|
51
51
|
|
|
52
52
|
// components
|
|
53
53
|
'm-segmented': MicaElement
|
|
54
|
-
'm-
|
|
54
|
+
'm-stepper': MicaElement
|
|
55
|
+
'm-tabs': MicaElement<{ variant?: 'underline' }>
|
|
55
56
|
'm-badge': MicaElement<{
|
|
56
57
|
variant?: 'primary' | 'success' | 'warning' | 'danger'
|
|
57
58
|
count?: boolean
|
|
58
59
|
}>
|
|
60
|
+
'm-avatar': MicaElement<{
|
|
61
|
+
blobatar?: string
|
|
62
|
+
contained?: boolean
|
|
63
|
+
}>
|
|
64
|
+
'm-skeleton': MicaElement
|
|
59
65
|
'm-toast': MicaElement<{
|
|
60
66
|
popover?: 'manual'
|
|
61
67
|
variant?: 'success' | 'warning' | 'danger'
|