@akonwi/mica 0.3.0 → 0.4.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 CHANGED
@@ -12,8 +12,8 @@ there's nothing there but HTML and a stylesheet.
12
12
  <m-vstack gap="lg">
13
13
  <h1>That's it</h1>
14
14
  <m-hstack gap="sm">
15
- <button class="primary">Save</button>
16
- <button class="ghost">Cancel</button>
15
+ <button data-variant="primary">Save</button>
16
+ <button data-variant="ghost">Cancel</button>
17
17
  </m-hstack>
18
18
  </m-vstack>
19
19
  ```
package/drawer.js CHANGED
@@ -1,5 +1,5 @@
1
- /* mica/drawer.js — Tier 2: swipe-to-dismiss for dialog.drawer bottom
2
- * sheets on small screens.
1
+ /* mica/drawer.js — Tier 2: swipe-to-dismiss for dialog[data-drawer]
2
+ * 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
@@ -16,6 +16,7 @@
16
16
  const SHEET = matchMedia("(max-width: 40rem)");
17
17
  const CLOSE_DISTANCE = 1 / 3; // of sheet height
18
18
  const CLOSE_VELOCITY = 0.6; // px/ms, downward flick
19
+ const DRAWER = "dialog:is([data-drawer], .drawer)";
19
20
 
20
21
  document.documentElement.setAttribute("data-drawer-gestures", "");
21
22
 
@@ -23,13 +24,13 @@ document.addEventListener("pointerdown", (down) => {
23
24
  if (!SHEET.matches) return;
24
25
  const dialog =
25
26
  down.target instanceof Element &&
26
- down.target.closest("dialog.drawer[open]");
27
+ down.target.closest(`${DRAWER}[open]`);
27
28
  if (!dialog) return;
28
29
  if (down.target.closest("button, a, input, select, textarea")) return;
29
30
 
30
31
  // start only from the header or the top (handle) strip
31
32
  const rect = dialog.getBoundingClientRect();
32
- const inHeader = down.target.closest("dialog.drawer > header");
33
+ const inHeader = down.target.closest(`${DRAWER} > header`);
33
34
  if (!inHeader && down.clientY - rect.top > 32) return;
34
35
 
35
36
  let dy = 0;
package/mica.css CHANGED
@@ -268,8 +268,8 @@
268
268
 
269
269
  /* ------------------------------------------------------------------ *
270
270
  * Elements — Tier 1. Native elements styled directly; behavior is the
271
- * browser's. Variants are classes: any collision with app CSS resolves
272
- * in the user's favor because these live in a layer.
271
+ * browser's. Presentation APIs are attributes; user CSS still wins
272
+ * because these rules live in a layer.
273
273
  * ------------------------------------------------------------------ */
274
274
  @layer mica.elements {
275
275
  /* --- button (and things dressed as buttons) ---------------------- */
@@ -298,8 +298,8 @@
298
298
  background: var(--neutral-4);
299
299
  }
300
300
 
301
- /* variants */
302
- &:where(.primary) {
301
+ /* Class selectors remain aliases for the former class-based API. */
302
+ &:where([data-variant="primary"], .primary) {
303
303
  background: var(--color-primary);
304
304
  border-color: transparent;
305
305
  color: var(--color-on-primary);
@@ -316,7 +316,7 @@
316
316
  secondary button when primary is near-black */
317
317
  }
318
318
 
319
- &:where(.danger) {
319
+ &:where([data-variant="danger"], .danger) {
320
320
  background: var(--color-danger);
321
321
  border-color: transparent;
322
322
  color: var(--color-on-danger);
@@ -333,9 +333,9 @@
333
333
  }
334
334
 
335
335
  /* deliberately zero rest affordance — a ghost is invisible until
336
- hover/focus. use .ghost for toolbars and icon rows, not as the
337
- lone action on a page. */
338
- &:where(.ghost) {
336
+ hover/focus. use data-variant="ghost" for toolbars and icon rows,
337
+ not as the lone action on a page. */
338
+ &:where([data-variant="ghost"], .ghost) {
339
339
  background: transparent;
340
340
  border-color: transparent;
341
341
 
@@ -348,11 +348,11 @@
348
348
  }
349
349
 
350
350
  /* sizes (shadcn: h-8 px-3 / h-10 px-6, text-sm throughout) */
351
- &:where(.small) {
351
+ &:where([data-size="small"], .small) {
352
352
  block-size: var(--control-height-sm);
353
353
  padding-inline: var(--space-sm);
354
354
  }
355
- &:where(.large) {
355
+ &:where([data-size="large"], .large) {
356
356
  block-size: var(--control-height-lg);
357
357
  padding-inline: var(--space-lg);
358
358
  }
@@ -572,7 +572,7 @@
572
572
  }
573
573
  }
574
574
 
575
- :where(input[type="checkbox"]:not(.switch)) {
575
+ :where(input[type="checkbox"]:not(.switch, [role="switch"])) {
576
576
  &:checked {
577
577
  background-image: var(--check-glyph);
578
578
  }
@@ -668,8 +668,9 @@
668
668
 
669
669
  /* switch: a checkbox wearing a track; the thumb is a positioned
670
670
  gradient (pseudo-elements don't render on inputs). square, per the
671
- design language — a solid linear-gradient block */
672
- :where(input[type="checkbox"].switch) {
671
+ design language — a solid linear-gradient block. .switch remains
672
+ an alias for the former class-based API. */
673
+ :where(input[type="checkbox"]:is([role="switch"], .switch)) {
673
674
  inline-size: 2.25rem;
674
675
  border-color: transparent;
675
676
  border-radius: var(--radius-sm);
@@ -1081,8 +1082,9 @@
1081
1082
 
1082
1083
  /* drawer (shadcn sheet anatomy): inline-end panel, fade + 2.5rem
1083
1084
  nudge rather than a full slide; no section dividers; footer pinned
1084
- to the bottom with stacked actions */
1085
- :where(dialog.drawer) {
1085
+ to the bottom with stacked actions. .drawer remains an alias for
1086
+ the former class-based API. */
1087
+ :where(dialog[data-drawer], dialog.drawer) {
1086
1088
  margin-inline: auto 0;
1087
1089
  margin-block: 0;
1088
1090
  /* full height WITHOUT an explicit block-size: the UA gives
@@ -1102,23 +1104,24 @@
1102
1104
  scale: 1;
1103
1105
  translate: 2.5rem 0;
1104
1106
  }
1105
- :where(dialog.drawer[open]) {
1107
+ :where(dialog[data-drawer][open], dialog.drawer[open]) {
1106
1108
  translate: 0 0;
1107
1109
  }
1108
1110
  /* unconditional (survives the exit, like the rest of composition) */
1109
- :where(dialog.drawer) > :where(:not(header, footer, button.close)) {
1111
+ :where(dialog[data-drawer], dialog.drawer)
1112
+ > :where(:not(header, footer, button.close)) {
1110
1113
  font-size: inherit; /* panel runs 12/relaxed throughout */
1111
1114
  }
1112
1115
  @starting-style {
1113
- :where(dialog.drawer[open]) {
1116
+ :where(dialog[data-drawer][open], dialog.drawer[open]) {
1114
1117
  translate: 2.5rem 0;
1115
1118
  }
1116
1119
  }
1117
1120
 
1118
- :where(dialog.drawer) > :where(header) {
1121
+ :where(dialog[data-drawer], dialog.drawer) > :where(header) {
1119
1122
  border-block-end: 0;
1120
1123
  }
1121
- :where(dialog.drawer) > :where(footer) {
1124
+ :where(dialog[data-drawer], dialog.drawer) > :where(footer) {
1122
1125
  margin-block-start: auto;
1123
1126
  flex-direction: column;
1124
1127
  align-items: stretch;
@@ -1133,7 +1136,7 @@
1133
1136
  nav bars). Explicit 100svh gapped behind the collapsed toolbar and
1134
1137
  100dvh lagged in the top layer — don't reintroduce heights here. */
1135
1138
  @media (max-width: 40rem) {
1136
- :where(dialog.drawer) {
1139
+ :where(dialog[data-drawer], dialog.drawer) {
1137
1140
  inset-block: auto 0; /* release the UA's top anchor… */
1138
1141
  margin-block: 0; /* …so height resolves from content */
1139
1142
  block-size: auto; /* content height, like vaul */
@@ -1164,14 +1167,14 @@
1164
1167
  translate: 0 100%;
1165
1168
  opacity: 1;
1166
1169
  }
1167
- :where(dialog.drawer[open]) {
1170
+ :where(dialog[data-drawer][open], dialog.drawer[open]) {
1168
1171
  translate: 0 0;
1169
1172
  box-shadow:
1170
1173
  0 4rem 0 0 var(--color-surface-overlay),
1171
1174
  0 0 0 100vmax oklch(0 0 0 / 0.4);
1172
1175
  }
1173
1176
  @starting-style {
1174
- :where(dialog.drawer[open]) {
1177
+ :where(dialog[data-drawer][open], dialog.drawer[open]) {
1175
1178
  translate: 0 100%;
1176
1179
  box-shadow:
1177
1180
  0 4rem 0 0 var(--color-surface-overlay),
@@ -1180,14 +1183,15 @@
1180
1183
  }
1181
1184
  /* the box-shadow scrim replaces ::backdrop here; keep the native
1182
1185
  backdrop clear so they don't double up or flash on the iOS cut. */
1183
- :where(dialog.drawer[open])::backdrop {
1186
+ :where(dialog[data-drawer][open], dialog.drawer[open])::backdrop {
1184
1187
  background: oklch(0 0 0 / 0);
1185
1188
  }
1186
1189
 
1187
1190
  /* grab handle — only when drawer.js is loaded (an affordance
1188
1191
  without its behavior would fake interactivity; TIERS.md).
1189
1192
  the handle is the first flex item of the open sheet. */
1190
- :where(:root[data-drawer-gestures]) :where(dialog.drawer[open])::before {
1193
+ :where(:root[data-drawer-gestures])
1194
+ :where(dialog[data-drawer][open], dialog.drawer[open])::before {
1191
1195
  content: "";
1192
1196
  align-self: center;
1193
1197
  flex-shrink: 0;
@@ -1197,7 +1201,8 @@
1197
1201
  background: var(--color-border-strong);
1198
1202
  border-radius: var(--radius-full);
1199
1203
  }
1200
- :where(:root[data-drawer-gestures]) :where(dialog.drawer) > :where(header) {
1204
+ :where(:root[data-drawer-gestures])
1205
+ :where(dialog[data-drawer], dialog.drawer) > :where(header) {
1201
1206
  touch-action: none; /* the drag surface must own its touches */
1202
1207
  }
1203
1208
  }
@@ -1358,7 +1363,7 @@
1358
1363
  color: var(--color-on-accent);
1359
1364
  }
1360
1365
 
1361
- &.danger {
1366
+ &:is([data-variant="danger"], .danger) {
1362
1367
  color: var(--color-danger-text);
1363
1368
 
1364
1369
  &:hover:not(:disabled, [aria-disabled="true"]),
@@ -1503,14 +1508,14 @@
1503
1508
  color: var(--color-text-muted);
1504
1509
  }
1505
1510
 
1506
- /* status edge variants */
1507
- &:where(.success) {
1511
+ /* Status classes remain aliases for the former class-based API. */
1512
+ &:where([data-variant="success"], .success) {
1508
1513
  border-inline-start: 2px solid var(--color-success);
1509
1514
  }
1510
- &:where(.danger) {
1515
+ &:where([data-variant="danger"], .danger) {
1511
1516
  border-inline-start: 2px solid var(--color-danger);
1512
1517
  }
1513
- &:where(.warn) {
1518
+ &:where([data-variant="warning"], .warn) {
1514
1519
  border-inline-start: 2px solid var(--color-warn);
1515
1520
  }
1516
1521
  }
@@ -1871,8 +1876,8 @@
1871
1876
 
1872
1877
  /* --- m-cover: fill the viewport, center the principal element ---- *
1873
1878
  * The principal element (h1 by default, or mark one child with
1874
- * .principal) is vertically centered; children before it pin to the
1875
- * top, children after it pin to the bottom. */
1879
+ * data-principal) is vertically centered; children before it pin to
1880
+ * the top, children after it pin to the bottom. */
1876
1881
  m-cover {
1877
1882
  display: flex;
1878
1883
  flex-direction: column;
@@ -1888,9 +1893,11 @@
1888
1893
 
1889
1894
  & > * { margin-block: 0; }
1890
1895
 
1891
- & > :is(h1, .principal) { margin-block: auto; }
1896
+ & > :is(h1, [data-principal], .principal) { margin-block: auto; }
1892
1897
 
1893
- /* an explicit .principal wins over the h1 default */
1894
- &:has(> .principal) > h1:not(.principal) { margin-block: 0; }
1898
+ /* An explicit marker wins over the h1 default. .principal remains
1899
+ an alias for the former class-based API. */
1900
+ &:has(> :is([data-principal], .principal))
1901
+ > h1:not([data-principal], .principal) { margin-block: 0; }
1895
1902
  }
1896
1903
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akonwi/mica",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Custom elements. Native behavior. Nearly no JavaScript.",
5
5
  "keywords": [
6
6
  "css",
@@ -55,4 +55,4 @@
55
55
  "snapshot:check": "bun tools/snapshot.ts --check",
56
56
  "snapshot:setup": "playwright install chromium webkit"
57
57
  }
58
- }
58
+ }
package/toast.js CHANGED
@@ -6,10 +6,11 @@
6
6
  *
7
7
  * - stacking: open toasts stack upward; JS ships one number per toast
8
8
  * (--m-toast-offset), CSS owns the geometry and the reflow motion
9
- * - auto-dismiss: `duration` attribute in ms (default 5000, "0" = sticky),
10
- * paused while hovered
9
+ * - auto-dismiss: `data-duration` attribute in ms (default 5000,
10
+ * "0" = sticky), paused while hovered
11
11
  * - toast(title, { description, variant, duration }): spawns the same
12
- * recipe markup, shows it, and removes it after dismissal
12
+ * recipe markup (`variant`: success, warning, or danger), shows it,
13
+ * and removes it after dismissal
13
14
  */
14
15
 
15
16
  const GAP = 8;
@@ -30,8 +31,10 @@ function restack() {
30
31
  }
31
32
 
32
33
  function startTimer(el) {
33
- const ms = el.hasAttribute("duration")
34
- ? Number(el.getAttribute("duration"))
34
+ const duration =
35
+ el.getAttribute("data-duration") ?? el.getAttribute("duration");
36
+ const ms = duration !== null
37
+ ? Number(duration)
35
38
  : DEFAULT_DURATION;
36
39
  if (!ms) return;
37
40
  timers.set(el, setTimeout(() => el.hidePopover(), ms));
@@ -74,10 +77,15 @@ document.addEventListener(
74
77
  export function toast(title, { description = "", variant = "", duration } = {}) {
75
78
  const el = document.createElement("div");
76
79
  el.popover = "manual";
80
+ // Keep the generated class for consumers of the former API while
81
+ // emitting the attribute vocabulary used by declared toasts.
77
82
  el.className = `toast${variant ? ` ${variant}` : ""}`;
83
+ if (variant) {
84
+ el.setAttribute("data-variant", variant === "warn" ? "warning" : variant);
85
+ }
78
86
  el.setAttribute("role", "status");
79
87
  el.dataset.ephemeral = "";
80
- if (duration !== undefined) el.setAttribute("duration", String(duration));
88
+ if (duration !== undefined) el.setAttribute("data-duration", String(duration));
81
89
 
82
90
  const x = document.createElement("button");
83
91
  x.className = "close";