@akonwi/mica 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/drawer.js +80 -0
- package/invoker.js +25 -0
- package/mica.css +254 -41
- package/package.json +26 -4
package/README.md
CHANGED
|
@@ -70,11 +70,21 @@ CSS by spec — overriding mica never requires specificity games. Theming is
|
|
|
70
70
|
swapping token values (`--hue`, semantic color roles, spacing scale); dark
|
|
71
71
|
mode is `light-dark()` — automatic, no classes.
|
|
72
72
|
|
|
73
|
+
## Browser support
|
|
74
|
+
|
|
75
|
+
Mica targets [Baseline](https://web.dev/baseline) widely-available.
|
|
76
|
+
Newer features (anchor positioning, customizable select) are used behind
|
|
77
|
+
graceful fallbacks — each component's docs page notes what degrades and
|
|
78
|
+
how. Nothing breaks; some things get plainer.
|
|
79
|
+
|
|
73
80
|
## Docs
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
**[akonwi.io/mica](https://akonwi.io/mica/)** — every component, built with
|
|
83
|
+
mica itself (view source). The [demo](https://akonwi.io/mica/demo.html) is
|
|
84
|
+
the whole library on one page.
|
|
85
|
+
|
|
86
|
+
Read [VISION.md](VISION.md) for the philosophy and [TIERS.md](TIERS.md)
|
|
87
|
+
for the tier system.
|
|
78
88
|
|
|
79
89
|
## License
|
|
80
90
|
|
package/drawer.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* mica/drawer.js — Tier 2: swipe-to-dismiss for dialog.drawer bottom
|
|
2
|
+
* sheets on small screens.
|
|
3
|
+
*
|
|
4
|
+
* Enhances working markup, never replaces it: without this module the
|
|
5
|
+
* drawer opens/closes via buttons and Esc; with it, the mobile sheet
|
|
6
|
+
* grows a grab handle (CSS keys off :root[data-drawer-gestures] — an
|
|
7
|
+
* affordance may only appear when the behavior exists) and vaul-style
|
|
8
|
+
* drag physics: the sheet follows a downward drag, resists upward,
|
|
9
|
+
* and dismisses past 1/3 of its height or on a flick.
|
|
10
|
+
*
|
|
11
|
+
* Drag surface: the handle / header strip. Drag-from-content is
|
|
12
|
+
* deliberately out of scope — it requires scroll-vs-drag heuristics
|
|
13
|
+
* (vaul's hardest code) for little gain over a clear grab target.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const SHEET = matchMedia("(max-width: 40rem)");
|
|
17
|
+
const CLOSE_DISTANCE = 1 / 3; // of sheet height
|
|
18
|
+
const CLOSE_VELOCITY = 0.6; // px/ms, downward flick
|
|
19
|
+
|
|
20
|
+
document.documentElement.setAttribute("data-drawer-gestures", "");
|
|
21
|
+
|
|
22
|
+
document.addEventListener("pointerdown", (down) => {
|
|
23
|
+
if (!SHEET.matches) return;
|
|
24
|
+
const dialog =
|
|
25
|
+
down.target instanceof Element &&
|
|
26
|
+
down.target.closest("dialog.drawer[open]");
|
|
27
|
+
if (!dialog) return;
|
|
28
|
+
if (down.target.closest("button, a, input, select, textarea")) return;
|
|
29
|
+
|
|
30
|
+
// start only from the header or the top (handle) strip
|
|
31
|
+
const rect = dialog.getBoundingClientRect();
|
|
32
|
+
const inHeader = down.target.closest("dialog.drawer > header");
|
|
33
|
+
if (!inHeader && down.clientY - rect.top > 32) return;
|
|
34
|
+
|
|
35
|
+
let dy = 0;
|
|
36
|
+
let lastY = down.clientY;
|
|
37
|
+
let lastT = down.timeStamp;
|
|
38
|
+
let velocity = 0;
|
|
39
|
+
|
|
40
|
+
dialog.setPointerCapture(down.pointerId);
|
|
41
|
+
const baseTransition = dialog.style.transition;
|
|
42
|
+
dialog.style.transition = "none";
|
|
43
|
+
|
|
44
|
+
const move = (e) => {
|
|
45
|
+
dy = e.clientY - down.clientY;
|
|
46
|
+
velocity = (e.clientY - lastY) / Math.max(1, e.timeStamp - lastT);
|
|
47
|
+
lastY = e.clientY;
|
|
48
|
+
lastT = e.timeStamp;
|
|
49
|
+
// follow downward; resist upward
|
|
50
|
+
dialog.style.translate = `0 ${dy > 0 ? dy : dy / 4}px`;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const up = () => {
|
|
54
|
+
dialog.removeEventListener("pointermove", move);
|
|
55
|
+
dialog.removeEventListener("pointerup", up);
|
|
56
|
+
dialog.removeEventListener("pointercancel", up);
|
|
57
|
+
dialog.style.transition = baseTransition;
|
|
58
|
+
if (dy > rect.height * CLOSE_DISTANCE || velocity > CLOSE_VELOCITY) {
|
|
59
|
+
// continue the motion DOWNWARD off-screen. close() fires its exit
|
|
60
|
+
// (backdrop fade + display flip at the end via allow-discrete);
|
|
61
|
+
// the inline translate overrides the small CSS exit offset so the
|
|
62
|
+
// sheet keeps sliding down instead of snapping back up into view
|
|
63
|
+
// (the flash). Clear it only at transitionend — display:none by
|
|
64
|
+
// then, so clearing is invisible and the next open starts clean.
|
|
65
|
+
dialog.addEventListener(
|
|
66
|
+
"transitionend",
|
|
67
|
+
(e) => { if (e.propertyName === "translate") dialog.style.translate = ""; },
|
|
68
|
+
{ once: true },
|
|
69
|
+
);
|
|
70
|
+
requestAnimationFrame(() => { dialog.style.translate = "0 100dvh"; });
|
|
71
|
+
dialog.close();
|
|
72
|
+
} else {
|
|
73
|
+
dialog.style.translate = ""; // spring back
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
dialog.addEventListener("pointermove", move);
|
|
78
|
+
dialog.addEventListener("pointerup", up);
|
|
79
|
+
dialog.addEventListener("pointercancel", up);
|
|
80
|
+
});
|
package/invoker.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* mica/invoker.js — temporary shim for <button commandfor command>.
|
|
2
|
+
*
|
|
3
|
+
* Invoker commands are Baseline newly-available (Chrome 135+, Firefox
|
|
4
|
+
* 144+, Safari/iOS 26.2+). Mica's dialog recipes use them; include this
|
|
5
|
+
* shim until support is widely available. Where the browser handles
|
|
6
|
+
* commands natively, this module installs nothing.
|
|
7
|
+
*
|
|
8
|
+
* Scope: the dialog commands mica's recipes use — show-modal and close.
|
|
9
|
+
* (Popover recipes use popovertarget, which is already widely available.)
|
|
10
|
+
* Note: in shimmed browsers a commandfor button inside a <form> would
|
|
11
|
+
* still submit it — mica's recipes never place one there.
|
|
12
|
+
*/
|
|
13
|
+
if (!("commandForElement" in HTMLButtonElement.prototype)) {
|
|
14
|
+
document.addEventListener("click", (event) => {
|
|
15
|
+
const button =
|
|
16
|
+
event.target instanceof Element &&
|
|
17
|
+
event.target.closest("button[commandfor][command]");
|
|
18
|
+
if (!button || button.disabled) return;
|
|
19
|
+
const dialog = document.getElementById(button.getAttribute("commandfor"));
|
|
20
|
+
if (!(dialog instanceof HTMLDialogElement)) return;
|
|
21
|
+
const command = button.getAttribute("command");
|
|
22
|
+
if (command === "show-modal" && !dialog.open) dialog.showModal();
|
|
23
|
+
else if (command === "close" && dialog.open) dialog.close();
|
|
24
|
+
});
|
|
25
|
+
}
|
package/mica.css
CHANGED
|
@@ -44,11 +44,17 @@
|
|
|
44
44
|
--focus-ring-offset: 2px;
|
|
45
45
|
--focus-ring-color: var(--color-accent);
|
|
46
46
|
|
|
47
|
-
/* checkmark glyph — SVG URIs can't read custom properties,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
/* checkmark glyph — SVG URIs can't read custom properties, and
|
|
48
|
+
light-dark() only accepts <color>s: feeding it url()s is invalid
|
|
49
|
+
(Chromium computes it to none; iOS Safari drops the glyph at
|
|
50
|
+
paint). So: light glyph by default, dark swapped in by the media
|
|
51
|
+
query after this block. The pair tracks on-primary (white /
|
|
52
|
+
near-black, since primary inverts in dark).
|
|
53
|
+
Forcing a scheme against the OS? Also pin the glyph:
|
|
54
|
+
:root { color-scheme: dark; --check-glyph: var(--check-glyph-dark); } */
|
|
55
|
+
--check-glyph-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E");
|
|
56
|
+
--check-glyph-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23151922' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E");
|
|
57
|
+
--check-glyph: var(--check-glyph-light);
|
|
52
58
|
|
|
53
59
|
/* -------------------------------------------------------------- *
|
|
54
60
|
* Color. Two knobs — change the theme by changing the knobs:
|
|
@@ -151,6 +157,14 @@
|
|
|
151
157
|
--color-on-warn: oklch(20% 0.03 var(--warn-hue));
|
|
152
158
|
--color-warn-text: light-dark(oklch(42% calc(var(--status-chroma) * 0.9) var(--warn-hue)), oklch(80% calc(var(--status-chroma) * 0.9) var(--warn-hue)));
|
|
153
159
|
}
|
|
160
|
+
|
|
161
|
+
/* scheme swap for image-valued tokens — the one place light-dark()
|
|
162
|
+
can't help (colors only). See --check-glyph above. */
|
|
163
|
+
@media (prefers-color-scheme: dark) {
|
|
164
|
+
:root {
|
|
165
|
+
--check-glyph: var(--check-glyph-dark);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
154
168
|
}
|
|
155
169
|
|
|
156
170
|
/* ------------------------------------------------------------------ *
|
|
@@ -241,6 +255,14 @@
|
|
|
241
255
|
transition-duration: 0.01ms !important;
|
|
242
256
|
scroll-behavior: auto !important;
|
|
243
257
|
}
|
|
258
|
+
/* `*` never matches pseudo-elements — ::backdrop needs its own rule
|
|
259
|
+
(and per convention gets its own block: an unrecognized selector
|
|
260
|
+
in the list above would kill the whole kill-switch). Found via
|
|
261
|
+
snapshot flakiness: the backdrop fade survived reduced-motion. */
|
|
262
|
+
*::backdrop {
|
|
263
|
+
animation-duration: 0.01ms !important;
|
|
264
|
+
transition-duration: 0.01ms !important;
|
|
265
|
+
}
|
|
244
266
|
}
|
|
245
267
|
}
|
|
246
268
|
|
|
@@ -808,25 +830,49 @@
|
|
|
808
830
|
/* composition: dialog > header (title + description) / body / footer.
|
|
809
831
|
layout lives on [open] — unconditional display would defeat the
|
|
810
832
|
UA's display:none on closed dialogs */
|
|
811
|
-
:
|
|
812
|
-
|
|
833
|
+
/* Composition is UNCONDITIONAL so it survives the exit: the dialog
|
|
834
|
+
stays rendered (display transitions via allow-discrete) for 0.15s
|
|
835
|
+
after [open] is removed. Structure gated on [open] snaps back to
|
|
836
|
+
defaults the instant [open] goes — flex-direction reverts to row,
|
|
837
|
+
children lose their sizing — and the content visibly deforms while
|
|
838
|
+
fading out. Only `display` stays conditional (unconditional flex
|
|
839
|
+
would defeat the UA's display:none on a closed dialog); opacity and
|
|
840
|
+
scale are the open visual state that transitions on exit. */
|
|
841
|
+
:where(dialog) {
|
|
813
842
|
flex-direction: column;
|
|
814
|
-
opacity: 1;
|
|
815
|
-
scale: 1;
|
|
816
843
|
|
|
817
844
|
& > :where(*) {
|
|
818
845
|
margin-block: 0; /* the dialog owns spacing, like a stack */
|
|
819
846
|
}
|
|
820
847
|
|
|
821
848
|
/* the body: whatever sits between header and footer. one wrapper
|
|
822
|
-
element; it pads itself and is the scrollable region
|
|
849
|
+
element; it pads itself and is the scrollable region.
|
|
850
|
+
flex-basis must stay `auto` (not `flex: 1`'s 0%): iOS Safari sizes
|
|
851
|
+
the dialog's fit-content height from the basis, so 0% collapses
|
|
852
|
+
the body to a one-line scroll strip (iOS 26.0; Chromium and newer
|
|
853
|
+
WebKit use the content contribution either way). */
|
|
823
854
|
& > :where(:not(header, footer, button.close)) {
|
|
824
855
|
padding: var(--space-md);
|
|
825
|
-
flex: 1;
|
|
856
|
+
flex: 1 1 auto;
|
|
826
857
|
min-block-size: 0;
|
|
827
858
|
overflow-y: auto;
|
|
859
|
+
/* break the scroll chain: at the region's scroll limit iOS
|
|
860
|
+
otherwise hands the gesture to the page behind the modal —
|
|
861
|
+
the root's overflow:hidden can't stop a chained scroll. */
|
|
862
|
+
overscroll-behavior: contain;
|
|
828
863
|
font-size: 0.875rem; /* body must not outweigh the 14px title */
|
|
829
864
|
}
|
|
865
|
+
|
|
866
|
+
/* when the dialog hits its max height, only the body compresses */
|
|
867
|
+
& > :where(header, footer) {
|
|
868
|
+
flex-shrink: 0;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
:where(dialog[open]) {
|
|
873
|
+
display: flex;
|
|
874
|
+
opacity: 1;
|
|
875
|
+
scale: 1;
|
|
830
876
|
}
|
|
831
877
|
|
|
832
878
|
/* corner close button — <button class="close" commandfor="…"
|
|
@@ -906,7 +952,18 @@
|
|
|
906
952
|
}
|
|
907
953
|
|
|
908
954
|
:where(dialog)::backdrop {
|
|
909
|
-
|
|
955
|
+
/* explicit oklch alpha, not the `transparent` keyword: iOS won't
|
|
956
|
+
interpolate oklch()→transparent and cuts the scrim instantly,
|
|
957
|
+
desyncing it from the sheet. same-space endpoints animate. */
|
|
958
|
+
background: oklch(0 0 0 / 0);
|
|
959
|
+
/* size to the LARGE viewport, don't trust inset: shipped iOS sizes
|
|
960
|
+
the top layer to the small viewport, so an inset-based backdrop
|
|
961
|
+
ends above the real bottom edge when the toolbar collapses —
|
|
962
|
+
page content peeked through the strip. (Radix/vaul overlays are
|
|
963
|
+
ordinary fixed divs and don't have this problem; ::backdrop
|
|
964
|
+
lives in the top layer, so we overpaint instead.) */
|
|
965
|
+
inline-size: 100lvw;
|
|
966
|
+
block-size: 100lvh;
|
|
910
967
|
transition:
|
|
911
968
|
background-color 0.15s,
|
|
912
969
|
overlay 0.15s allow-discrete,
|
|
@@ -917,7 +974,7 @@
|
|
|
917
974
|
}
|
|
918
975
|
@starting-style {
|
|
919
976
|
:where(dialog[open])::backdrop {
|
|
920
|
-
background:
|
|
977
|
+
background: oklch(0 0 0 / 0);
|
|
921
978
|
}
|
|
922
979
|
}
|
|
923
980
|
|
|
@@ -927,8 +984,14 @@
|
|
|
927
984
|
:where(dialog.drawer) {
|
|
928
985
|
margin-inline: auto 0;
|
|
929
986
|
margin-block: 0;
|
|
930
|
-
block-size:
|
|
931
|
-
|
|
987
|
+
/* full height WITHOUT an explicit block-size: the UA gives
|
|
988
|
+
dialog:modal `position: fixed; inset-block: 0` — leaving height
|
|
989
|
+
auto stretches between both edges, and on iOS that bottom edge
|
|
990
|
+
tracks toolbar collapse like any bottom-fixed bar. Explicit
|
|
991
|
+
100svh left a gap behind the collapsed toolbar; 100dvh lagged
|
|
992
|
+
in the top layer on shipped iOS. Don't reintroduce either. */
|
|
993
|
+
block-size: auto;
|
|
994
|
+
max-block-size: none;
|
|
932
995
|
inline-size: 75vw;
|
|
933
996
|
max-inline-size: 24rem; /* sheet's max-w-sm */
|
|
934
997
|
border: 0;
|
|
@@ -940,10 +1003,10 @@
|
|
|
940
1003
|
}
|
|
941
1004
|
:where(dialog.drawer[open]) {
|
|
942
1005
|
translate: 0 0;
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1006
|
+
}
|
|
1007
|
+
/* unconditional (survives the exit, like the rest of composition) */
|
|
1008
|
+
:where(dialog.drawer) > :where(:not(header, footer, button.close)) {
|
|
1009
|
+
font-size: inherit; /* panel runs 12/relaxed throughout */
|
|
947
1010
|
}
|
|
948
1011
|
@starting-style {
|
|
949
1012
|
:where(dialog.drawer[open]) {
|
|
@@ -962,6 +1025,90 @@
|
|
|
962
1025
|
border-block-start: 0;
|
|
963
1026
|
}
|
|
964
1027
|
|
|
1028
|
+
/* small screens: the drawer becomes a bottom sheet — shadcn's own
|
|
1029
|
+
mobile drawer (vaul/radix) is exactly this. Anchored to the bottom
|
|
1030
|
+
edge via auto top margin: bottom attachment is the one thing iOS
|
|
1031
|
+
tracks reliably through toolbar collapse (same mechanism as bottom
|
|
1032
|
+
nav bars). Explicit 100svh gapped behind the collapsed toolbar and
|
|
1033
|
+
100dvh lagged in the top layer — don't reintroduce heights here. */
|
|
1034
|
+
@media (max-width: 40rem) {
|
|
1035
|
+
:where(dialog.drawer) {
|
|
1036
|
+
inset-block: auto 0; /* release the UA's top anchor… */
|
|
1037
|
+
margin-block: 0; /* …so height resolves from content */
|
|
1038
|
+
block-size: auto; /* content height, like vaul */
|
|
1039
|
+
max-block-size: calc(100dvh - 3rem);
|
|
1040
|
+
inline-size: 100vw;
|
|
1041
|
+
max-inline-size: none;
|
|
1042
|
+
border-inline-start: 0;
|
|
1043
|
+
border-block-start: 1px solid var(--color-border);
|
|
1044
|
+
/* Two box-shadows, both painted by the SHEET element so they
|
|
1045
|
+
persist through the exit (display:allow-discrete keeps the box
|
|
1046
|
+
rendered) — unlike ::backdrop, which iOS drops from the top
|
|
1047
|
+
layer the instant you close, cutting the scrim. So the dim
|
|
1048
|
+
rides the sheet here and stays in sync with the slide:
|
|
1049
|
+
1) bleed slab below the edge (vaul's trick) — fills any strip
|
|
1050
|
+
iOS exposes under the top layer when the toolbar collapses;
|
|
1051
|
+
2) full-screen scrim via 100vmax spread — the visible dim,
|
|
1052
|
+
its alpha animating with the sheet. 100vmax keeps coverage
|
|
1053
|
+
even as the shadow rides the sliding sheet down. */
|
|
1054
|
+
box-shadow:
|
|
1055
|
+
0 4rem 0 0 var(--color-surface-overlay),
|
|
1056
|
+
0 0 0 100vmax oklch(0 0 0 / 0);
|
|
1057
|
+
transition:
|
|
1058
|
+
box-shadow 0.15s, translate 0.15s,
|
|
1059
|
+
overlay 0.15s allow-discrete, display 0.15s allow-discrete;
|
|
1060
|
+
/* the sheet SLIDES fully off the bottom and stays opaque — only
|
|
1061
|
+
the scrim fades. A small nudge + opacity fade read as a
|
|
1062
|
+
dissolve-in-place, wrong for a bottom sheet. */
|
|
1063
|
+
translate: 0 100%;
|
|
1064
|
+
opacity: 1;
|
|
1065
|
+
}
|
|
1066
|
+
:where(dialog.drawer[open]) {
|
|
1067
|
+
translate: 0 0;
|
|
1068
|
+
box-shadow:
|
|
1069
|
+
0 4rem 0 0 var(--color-surface-overlay),
|
|
1070
|
+
0 0 0 100vmax oklch(0 0 0 / 0.4);
|
|
1071
|
+
}
|
|
1072
|
+
@starting-style {
|
|
1073
|
+
:where(dialog.drawer[open]) {
|
|
1074
|
+
translate: 0 100%;
|
|
1075
|
+
box-shadow:
|
|
1076
|
+
0 4rem 0 0 var(--color-surface-overlay),
|
|
1077
|
+
0 0 0 100vmax oklch(0 0 0 / 0);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
/* the box-shadow scrim replaces ::backdrop here; keep the native
|
|
1081
|
+
backdrop clear so they don't double up or flash on the iOS cut. */
|
|
1082
|
+
:where(dialog.drawer[open])::backdrop {
|
|
1083
|
+
background: oklch(0 0 0 / 0);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/* grab handle — only when drawer.js is loaded (an affordance
|
|
1087
|
+
without its behavior would fake interactivity; TIERS.md).
|
|
1088
|
+
the handle is the first flex item of the open sheet. */
|
|
1089
|
+
:where(:root[data-drawer-gestures]) :where(dialog.drawer[open])::before {
|
|
1090
|
+
content: "";
|
|
1091
|
+
align-self: center;
|
|
1092
|
+
flex-shrink: 0;
|
|
1093
|
+
inline-size: 2.5rem;
|
|
1094
|
+
block-size: 0.25rem;
|
|
1095
|
+
margin-block-start: var(--space-sm);
|
|
1096
|
+
background: var(--color-border-strong);
|
|
1097
|
+
border-radius: var(--radius-full);
|
|
1098
|
+
}
|
|
1099
|
+
:where(:root[data-drawer-gestures]) :where(dialog.drawer) > :where(header) {
|
|
1100
|
+
touch-action: none; /* the drag surface must own its touches */
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/* scroll lock: the page must not scroll behind any modal dialog.
|
|
1105
|
+
CSS-only best effort (→ no JS tier violation); iOS honors
|
|
1106
|
+
overflow:hidden on the root for modern versions. */
|
|
1107
|
+
:where(html):has(:where(dialog:modal)) {
|
|
1108
|
+
overflow: hidden;
|
|
1109
|
+
overscroll-behavior: none;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
965
1112
|
/* --- accordion / disclosure: details + summary -------------------- *
|
|
966
1113
|
* shadcn accordion look; exclusivity is <details name="group">.
|
|
967
1114
|
* open/close animation via ::details-content + interpolate-size,
|
|
@@ -1042,18 +1189,27 @@
|
|
|
1042
1189
|
opacity: 1;
|
|
1043
1190
|
scale: 1;
|
|
1044
1191
|
}
|
|
1045
|
-
/* enter animates; close is instant (house rule for transient layers)
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
@
|
|
1192
|
+
/* enter animates; close is instant (house rule for transient layers).
|
|
1193
|
+
Gated on `overlay` support: iOS lacks it, and its top-layer
|
|
1194
|
+
transitions are broken — a popover with an @starting-style entry
|
|
1195
|
+
freezes at the start (opacity:0 = invisible) with no reflow to kick
|
|
1196
|
+
it, and there's no CSS way to force one. Where overlay is
|
|
1197
|
+
unsupported, skip the entrance so popovers render at their resting
|
|
1198
|
+
opacity:1 instead of vanishing. (This is why toasts "didn't work"
|
|
1199
|
+
on iOS.) */
|
|
1200
|
+
@supports (overlay: auto) {
|
|
1054
1201
|
:where([popover]:popover-open) {
|
|
1055
|
-
|
|
1056
|
-
|
|
1202
|
+
transition:
|
|
1203
|
+
opacity 0.1s,
|
|
1204
|
+
scale 0.1s,
|
|
1205
|
+
overlay 0.1s allow-discrete,
|
|
1206
|
+
display 0.1s allow-discrete;
|
|
1207
|
+
}
|
|
1208
|
+
@starting-style {
|
|
1209
|
+
:where([popover]:popover-open) {
|
|
1210
|
+
opacity: 0;
|
|
1211
|
+
scale: 0.95;
|
|
1212
|
+
}
|
|
1057
1213
|
}
|
|
1058
1214
|
}
|
|
1059
1215
|
/* anchor to the invoker where the platform can */
|
|
@@ -1246,18 +1402,24 @@
|
|
|
1246
1402
|
border-inline-start: 2px solid var(--color-warn);
|
|
1247
1403
|
}
|
|
1248
1404
|
}
|
|
1249
|
-
/*
|
|
1405
|
+
/* restacking motion always (inset-block-end); the entrance slide/fade
|
|
1406
|
+
only where overlay transitions work — see the base popover note. */
|
|
1250
1407
|
:where([popover].toast:popover-open) {
|
|
1251
|
-
transition:
|
|
1252
|
-
opacity 0.1s,
|
|
1253
|
-
translate 0.1s,
|
|
1254
|
-
inset-block-end 0.15s,
|
|
1255
|
-
overlay 0.1s allow-discrete,
|
|
1256
|
-
display 0.1s allow-discrete;
|
|
1408
|
+
transition: inset-block-end 0.15s;
|
|
1257
1409
|
}
|
|
1258
|
-
@
|
|
1410
|
+
@supports (overlay: auto) {
|
|
1259
1411
|
:where([popover].toast:popover-open) {
|
|
1260
|
-
|
|
1412
|
+
transition:
|
|
1413
|
+
opacity 0.1s,
|
|
1414
|
+
translate 0.1s,
|
|
1415
|
+
inset-block-end 0.15s,
|
|
1416
|
+
overlay 0.1s allow-discrete,
|
|
1417
|
+
display 0.1s allow-discrete;
|
|
1418
|
+
}
|
|
1419
|
+
@starting-style {
|
|
1420
|
+
:where([popover].toast:popover-open) {
|
|
1421
|
+
translate: 0 0.75rem;
|
|
1422
|
+
}
|
|
1261
1423
|
}
|
|
1262
1424
|
}
|
|
1263
1425
|
|
|
@@ -1348,13 +1510,15 @@
|
|
|
1348
1510
|
m-grid,
|
|
1349
1511
|
m-sidecar,
|
|
1350
1512
|
m-switcher,
|
|
1351
|
-
m-reel
|
|
1513
|
+
m-reel,
|
|
1514
|
+
m-frame,
|
|
1515
|
+
m-cover {
|
|
1352
1516
|
display: block;
|
|
1353
1517
|
box-sizing: border-box;
|
|
1354
1518
|
}
|
|
1355
1519
|
|
|
1356
1520
|
/* Shared attribute vocabulary -> custom properties */
|
|
1357
|
-
:is(m-vstack, m-hstack, m-grid, m-sidecar, m-switcher, m-reel) {
|
|
1521
|
+
:is(m-vstack, m-hstack, m-grid, m-sidecar, m-switcher, m-reel, m-cover) {
|
|
1358
1522
|
&[gap="none"] { --gap: 0; }
|
|
1359
1523
|
&[gap="2xs"] { --gap: var(--space-2xs); }
|
|
1360
1524
|
&[gap="xs"] { --gap: var(--space-xs); }
|
|
@@ -1533,4 +1697,53 @@
|
|
|
1533
1697
|
&[snap="none"] { scroll-snap-type: none; }
|
|
1534
1698
|
&[snap="mandatory"] { scroll-snap-type: inline mandatory; }
|
|
1535
1699
|
}
|
|
1700
|
+
|
|
1701
|
+
/* --- m-frame: hold an aspect ratio, crop media to fit ------------ */
|
|
1702
|
+
m-frame {
|
|
1703
|
+
aspect-ratio: var(--ratio, 16 / 9);
|
|
1704
|
+
overflow: hidden;
|
|
1705
|
+
display: flex;
|
|
1706
|
+
align-items: center;
|
|
1707
|
+
justify-content: center;
|
|
1708
|
+
|
|
1709
|
+
&[ratio="square"] { --ratio: 1; }
|
|
1710
|
+
&[ratio="16:9"] { --ratio: 16 / 9; }
|
|
1711
|
+
&[ratio="4:3"] { --ratio: 4 / 3; }
|
|
1712
|
+
&[ratio="3:2"] { --ratio: 3 / 2; }
|
|
1713
|
+
&[ratio="2:1"] { --ratio: 2 / 1; }
|
|
1714
|
+
&[ratio="9:16"] { --ratio: 9 / 16; }
|
|
1715
|
+
|
|
1716
|
+
/* media fills and crops; anything else just gets centered */
|
|
1717
|
+
& > :is(img, video),
|
|
1718
|
+
& > picture > img {
|
|
1719
|
+
inline-size: 100%;
|
|
1720
|
+
block-size: 100%;
|
|
1721
|
+
object-fit: cover;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
/* --- m-cover: fill the viewport, center the principal element ---- *
|
|
1726
|
+
* The principal element (h1 by default, or mark one child with
|
|
1727
|
+
* .principal) is vertically centered; children before it pin to the
|
|
1728
|
+
* top, children after it pin to the bottom. */
|
|
1729
|
+
m-cover {
|
|
1730
|
+
display: flex;
|
|
1731
|
+
flex-direction: column;
|
|
1732
|
+
min-block-size: var(--min-height, 100svh);
|
|
1733
|
+
gap: var(--gap, var(--space-md));
|
|
1734
|
+
padding: var(--pad, var(--space-md));
|
|
1735
|
+
|
|
1736
|
+
&[pad="none"] { --pad: 0; }
|
|
1737
|
+
&[pad="sm"] { --pad: var(--space-sm); }
|
|
1738
|
+
&[pad="md"] { --pad: var(--space-md); }
|
|
1739
|
+
&[pad="lg"] { --pad: var(--space-lg); }
|
|
1740
|
+
&[pad="xl"] { --pad: var(--space-xl); }
|
|
1741
|
+
|
|
1742
|
+
& > * { margin-block: 0; }
|
|
1743
|
+
|
|
1744
|
+
& > :is(h1, .principal) { margin-block: auto; }
|
|
1745
|
+
|
|
1746
|
+
/* an explicit .principal wins over the h1 default */
|
|
1747
|
+
&:has(> .principal) > h1:not(.principal) { margin-block: 0; }
|
|
1748
|
+
}
|
|
1536
1749
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akonwi/mica",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Custom elements. Native behavior. Nearly no JavaScript.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
|
|
5
|
+
"keywords": [
|
|
6
|
+
"css",
|
|
7
|
+
"custom-elements",
|
|
8
|
+
"web-components",
|
|
9
|
+
"layout",
|
|
10
|
+
"no-build",
|
|
11
|
+
"html-first"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://akonwi.io/mica/",
|
|
7
14
|
"repository": {
|
|
8
15
|
"type": "git",
|
|
9
16
|
"url": "git+https://github.com/akonwi/mica.git"
|
|
@@ -14,6 +21,8 @@
|
|
|
14
21
|
"exports": {
|
|
15
22
|
".": "./mica.css",
|
|
16
23
|
"./mica.css": "./mica.css",
|
|
24
|
+
"./invoker.js": "./invoker.js",
|
|
25
|
+
"./drawer.js": "./drawer.js",
|
|
17
26
|
"./field.js": "./field.js",
|
|
18
27
|
"./select.js": "./select.js",
|
|
19
28
|
"./tabs.js": "./tabs.js",
|
|
@@ -22,11 +31,24 @@
|
|
|
22
31
|
},
|
|
23
32
|
"files": [
|
|
24
33
|
"mica.css",
|
|
34
|
+
"invoker.js",
|
|
35
|
+
"drawer.js",
|
|
25
36
|
"field.js",
|
|
26
37
|
"select.js",
|
|
27
38
|
"tabs.js",
|
|
28
39
|
"toast.js",
|
|
29
40
|
"combobox.js"
|
|
30
41
|
],
|
|
31
|
-
"sideEffects": true
|
|
42
|
+
"sideEffects": true,
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"axe-core": "^4.12.1",
|
|
45
|
+
"pixelmatch": "^7.2.0",
|
|
46
|
+
"playwright": "^1.61.1",
|
|
47
|
+
"pngjs": "^7.0.0"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"snapshot": "bun tools/snapshot.ts",
|
|
51
|
+
"snapshot:check": "bun tools/snapshot.ts --check",
|
|
52
|
+
"snapshot:setup": "playwright install chromium webkit"
|
|
53
|
+
}
|
|
32
54
|
}
|