@junoput01/junoui 0.6.0 → 0.7.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/dist/css/juno.css CHANGED
@@ -398,9 +398,30 @@
398
398
  var(--juno-size-tap-comfortable) + 4 * var(--juno-space-4) + 2 * var(--juno-border-width-1)
399
399
  );
400
400
  --juno-dock-clearance-scale: 1;
401
+
402
+ /* The bar's own offset from the bottom edge — declared ONCE and consumed by
403
+ both the bar's margin (dock.css) and the clearance below, so the two cannot
404
+ disagree about where the bar sits.
405
+
406
+ That is the whole reason this is a token rather than a literal in each
407
+ place. junoui's default is additive, per docs/ios-conformance.md: a
408
+ FLOATING element sits off the edge, so its gap and the inset stack. A
409
+ consumer whose design wants the bar flush above the home indicator instead
410
+ writes `max(8px, env(safe-area-inset-bottom, 0px))` here and BOTH the
411
+ margin and the reservation follow it. Before this token the form was baked
412
+ into each site separately, so a consumer that changed one silently
413
+ disagreed with the other — measured at 16px of dead band at inset 0 and
414
+ 24px at inset 34 (20260815-055), and no single --juno-dock-h could
415
+ reconcile them because one side added the inset and the other maxed it. */
416
+ --juno-dock-edge-offset: calc(var(--juno-space-16) + env(safe-area-inset-bottom, 0px));
417
+
418
+ /* Gap between the bar's top edge and the last row of content. Zero is a
419
+ legitimate setting: a translucent bar that content is MEANT to scroll under
420
+ wants overlap, not breathing room. */
421
+ --juno-dock-clearance-breathing: var(--juno-space-8);
401
422
  --juno-dock-clearance: calc(
402
- var(--juno-dock-h) * var(--juno-dock-clearance-scale) + var(--juno-space-16) +
403
- var(--juno-space-8) + env(safe-area-inset-bottom, 0px)
423
+ var(--juno-dock-h) * var(--juno-dock-clearance-scale) + var(--juno-dock-edge-offset) +
424
+ var(--juno-dock-clearance-breathing)
404
425
  );
405
426
 
406
427
  /* The pillbar is the same shape one size down: no bubble, so its height is
@@ -409,8 +430,8 @@
409
430
  var(--juno-size-tap-comfortable) + 2 * var(--juno-space-4) + 2 * var(--juno-border-width-1)
410
431
  );
411
432
  --juno-pillbar-clearance: calc(
412
- var(--juno-pillbar-h) * var(--juno-dock-clearance-scale) + var(--juno-space-16) +
413
- var(--juno-space-8) + env(safe-area-inset-bottom, 0px)
433
+ var(--juno-pillbar-h) * var(--juno-dock-clearance-scale) + var(--juno-dock-edge-offset) +
434
+ var(--juno-dock-clearance-breathing)
414
435
  );
415
436
 
416
437
  /* JS-readable motion contract. CSS-only `prefers-reduced-motion` never reaches
@@ -527,7 +548,7 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
527
548
  .juno-dock__item,
528
549
  .juno-pillbar__item,
529
550
  .juno-tabs__tab,
530
- .juno-list__item,
551
+ .juno-list__row,
531
552
  .juno-menu__item
532
553
  ) {
533
554
  -webkit-tap-highlight-color: transparent;
@@ -552,9 +573,9 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
552
573
  .juno-pillbar__item,
553
574
  .juno-pillbar__overflow,
554
575
  .juno-tabs__tab,
555
- .juno-list__item,
576
+ .juno-list__row,
556
577
  .juno-menu__item,
557
- .juno-seg__option,
578
+ .juno-seg__opt,
558
579
  .juno-chip,
559
580
  .juno-toggle-btn
560
581
  ) {
@@ -1401,8 +1422,10 @@ details.juno-accordion__item:only-child {
1401
1422
  .juno-btn--ghost:hover { background: var(--juno-s2); filter: none; }
1402
1423
 
1403
1424
  /* Small — for dense desktop toolbars (40–46px bars) where the default
1404
- control would not fit. Deliberately under the WCAG tap-min target:
1405
- keep the default size for touch surfaces. Combines with --ghost + roles. */
1425
+ control would not fit. Combines with --ghost + roles.
1426
+ On a FINE pointer this is 24px: the WCAG 2.2 AA floor (2.5.8) exactly, and
1427
+ no more. On a coarse pointer it promotes to the comfortable target — see
1428
+ below. */
1406
1429
  .juno-btn--sm {
1407
1430
  min-height: var(--juno-space-24);
1408
1431
  font-size: var(--juno-font-size-11);
@@ -1411,6 +1434,44 @@ details.juno-accordion__item:only-child {
1411
1434
  border-radius: var(--juno-radius-3);
1412
1435
  }
1413
1436
 
1437
+ /* --sm names a DENSITY, and consumers reach for it as a SEMANTIC. Audited
1438
+ across one app: 40 call sites, nearly all of them `--sm --ghost` meaning
1439
+ "secondary", shipping a 24px target on a phone — and junoui's own showcase
1440
+ does it twice in a navbar action slot. A size modifier should not quietly
1441
+ become a tap-target decision, so on touch it holds the same floor every
1442
+ other control does. Type and padding still shrink: --sm stays a density
1443
+ modifier, it just stops being a touch-target one (20260826-026).
1444
+
1445
+ THE RULE LIVES HERE, not in base.css's (pointer: coarse) block: a media
1446
+ query adds no specificity, so a `.juno-btn--sm` there would lose to this
1447
+ file's own `.juno-btn--sm` later in the bundle. That is not a guess — it is
1448
+ what silently happened to the .juno-input font floor until 20260815-006
1449
+ measured it.
1450
+
1451
+ --dense is the opt-out, and it is deliberately only meaningful ON --sm:
1452
+ a dense touch toolbar is a real thing (a media scrubber, an editor rail),
1453
+ but it is a decision someone has to make by name rather than inherit from a
1454
+ size.
1455
+
1456
+ The two rules below are REDUNDANT ON PURPOSE, and mutation testing is how
1457
+ that became a decision rather than an accident: deleting either one leaves
1458
+ --dense at 24px. The `:not()` excludes a dense button from the promotion, so
1459
+ the pairing below it is dead weight — until the `:not()` names the wrong
1460
+ class, at which point the pairing (also (0,2,0), and later in the file) is
1461
+ the only thing holding the opt-out. Each covers the other's failure, so a
1462
+ single edit to either cannot silently promote a --dense button. Both
1463
+ selectors are (0,2,0) and mutually exclusive in the correct state, so they
1464
+ never race. */
1465
+ @media (pointer: coarse) {
1466
+ .juno-btn--sm:not(.juno-btn--dense) {
1467
+ min-height: var(--juno-size-tap-min);
1468
+ }
1469
+
1470
+ .juno-btn--sm.juno-btn--dense {
1471
+ min-height: var(--juno-space-24);
1472
+ }
1473
+ }
1474
+
1414
1475
  .juno-btn:disabled {
1415
1476
  background: var(--juno-muted);
1416
1477
  color: var(--juno-label);
@@ -1750,6 +1811,55 @@ button.juno-chip {
1750
1811
  media query. */
1751
1812
  --juno-dock-scale: 1;
1752
1813
 
1814
+ /* ── Horizontal budget contract ──────────────────────────────────────
1815
+ Published so a consumer's capacity planner ("how many items fit, and
1816
+ do they still hold a tap target?") reads junoui's real geometry
1817
+ instead of re-deriving it from the numbers in this file. Two
1818
+ consumers had derived it by hand, twice, in prose — and both had
1819
+ drifted: they subtracted 12px of inline padding where the pill
1820
+ actually spends 8, so their per-item width came out ~0.8px low at
1821
+ every viewport (20260826-027).
1822
+
1823
+ --juno-dock-items the item budget. junoui does not enforce it —
1824
+ __item is `flex: 1 1 0`, so the layout divides
1825
+ by however many items are present — it is the
1826
+ number the DERIVATIONS below are about, and the
1827
+ consumer sets it to what it renders.
1828
+ --juno-dock-item-inline the width one item gets. A prediction of what
1829
+ the flex layout produces, not an input to it;
1830
+ asserted against the measured box in the test
1831
+ suite so it cannot drift from the layout.
1832
+ --juno-dock-fit-inline the narrowest viewport at which every item
1833
+ still holds --juno-size-tap-comfortable. Use it
1834
+ in a media query and DROP an item; that is the
1835
+ answer junoui can give exactly.
1836
+
1837
+ A SCALE floor (44px / the item's width) is deliberately not published:
1838
+ it is a ratio of two lengths, and CSS cannot divide by a length. A
1839
+ consumer that must scale rather than drop compares
1840
+ --juno-dock-item-inline against --juno-size-tap-comfortable itself.
1841
+
1842
+ The three inline terms are declared ONCE here and consumed by both the
1843
+ variants' own margin/padding/border and the sums below, so the budget
1844
+ cannot disagree with the bar it describes — the same reason
1845
+ --juno-dock-edge-offset exists (20260815-055). The base bar is
1846
+ full-bleed with a top-edge border only, so all three are zero; --pill
1847
+ and --float set them. */
1848
+ --juno-dock-items: 5;
1849
+ --juno-dock-margin-inline: 0px;
1850
+ --juno-dock-pad-inline: 0px;
1851
+ --juno-dock-border-inline: 0px;
1852
+ --juno-dock-chrome-inline: calc(
1853
+ 2 * (var(--juno-dock-margin-inline) + var(--juno-dock-pad-inline) + var(--juno-dock-border-inline))
1854
+ );
1855
+ --juno-dock-avail: 100vw;
1856
+ --juno-dock-item-inline: calc(
1857
+ (var(--juno-dock-avail) - var(--juno-dock-chrome-inline)) / var(--juno-dock-items)
1858
+ );
1859
+ --juno-dock-fit-inline: calc(
1860
+ var(--juno-dock-items) * var(--juno-size-tap-comfortable) + var(--juno-dock-chrome-inline)
1861
+ );
1862
+
1753
1863
  position: sticky;
1754
1864
  inset-block-end: 0;
1755
1865
  z-index: var(--juno-z-raised);
@@ -1844,16 +1954,21 @@ button.juno-chip {
1844
1954
  .juno-dock--pill {
1845
1955
  --juno-role: var(--juno-active);
1846
1956
 
1957
+ /* the three inline terms the budget contract sums — see .juno-dock */
1958
+ --juno-dock-margin-inline: var(--juno-space-12);
1959
+ --juno-dock-pad-inline: var(--juno-space-4);
1960
+ --juno-dock-border-inline: var(--juno-border-width-1);
1961
+
1847
1962
  position: fixed;
1848
1963
  inset-inline: 0;
1849
1964
  inset-block-end: 0;
1850
1965
  z-index: var(--juno-z-raised);
1851
- margin: 0 var(--juno-space-12) calc(var(--juno-space-16) + env(safe-area-inset-bottom, 0px));
1852
- padding: var(--juno-space-4);
1966
+ margin: 0 var(--juno-dock-margin-inline) var(--juno-dock-edge-offset);
1967
+ padding: var(--juno-dock-pad-inline);
1853
1968
  background: color-mix(in srgb, var(--juno-s1) 88%, transparent);
1854
1969
  -webkit-backdrop-filter: blur(12px);
1855
1970
  backdrop-filter: blur(12px);
1856
- border: var(--juno-border-width-1) solid var(--juno-border);
1971
+ border: var(--juno-dock-border-inline) solid var(--juno-border);
1857
1972
  border-radius: 999px;
1858
1973
  box-shadow: var(--juno-shadow-2);
1859
1974
  }
@@ -1947,17 +2062,22 @@ button.juno-chip {
1947
2062
  Out of flow (fixed) so the page scrolls UNDER it — reserve room on the
1948
2063
  scroller with padding-block-end: var(--juno-dock-clearance). */
1949
2064
  .juno-dock--float {
2065
+ /* the three inline terms the budget contract sums — see .juno-dock */
2066
+ --juno-dock-margin-inline: var(--juno-space-12);
2067
+ --juno-dock-pad-inline: var(--juno-space-4);
2068
+ --juno-dock-border-inline: var(--juno-border-width-1);
2069
+
1950
2070
  position: fixed;
1951
2071
  inset-inline: 0;
1952
2072
  inset-block-end: 0;
1953
2073
  z-index: var(--juno-z-raised);
1954
- margin: 0 var(--juno-space-12) calc(var(--juno-space-16) + env(safe-area-inset-bottom, 0px));
1955
- padding: var(--juno-space-4);
2074
+ margin: 0 var(--juno-dock-margin-inline) var(--juno-dock-edge-offset);
2075
+ padding: var(--juno-dock-pad-inline);
1956
2076
  overflow: hidden;
1957
2077
  background: color-mix(in srgb, var(--juno-s1) 88%, transparent);
1958
2078
  -webkit-backdrop-filter: blur(12px);
1959
2079
  backdrop-filter: blur(12px);
1960
- border: var(--juno-border-width-1) solid var(--juno-border);
2080
+ border: var(--juno-dock-border-inline) solid var(--juno-border);
1961
2081
  border-radius: 999px;
1962
2082
  box-shadow: var(--juno-shadow-2);
1963
2083
  }
@@ -2335,17 +2455,44 @@ button.juno-chip {
2335
2455
  * gap, the folded slot still occupies one gap — name it in
2336
2456
  * --juno-fold-gap and the fold swallows it with a negative margin.
2337
2457
  *
2458
+ * COMPOSITION. The canonical use puts .juno-fold on an element that is
2459
+ * already a component — a .juno-pillbar__item, a .juno-btn, a .juno-chip
2460
+ * — because that is where the capsule chrome (tap floor, padding, hover,
2461
+ * focus ring, aria-pressed) lives. Two things follow, and both are
2462
+ * load-bearing rather than defensive:
2463
+ *
2464
+ * 1. The fold's own declarations are stated at ATTRIBUTE specificity
2465
+ * (0,2,0), on both branches of data-juno-in, i.e. always. A component
2466
+ * class is (0,1,0), and `transition` is a SHORTHAND: a later rule does
2467
+ * not add to the list, it replaces it. Measured in Chromium against the
2468
+ * built bundle, before this was raised: on a
2469
+ * .juno-fold.juno-pillbar__item, transition-property resolved to
2470
+ * `color, background-color` — pillbar.css sorts after this file, so the
2471
+ * fold had no transition at all and jumped shut instead of folding.
2472
+ * inline-size and overflow would lose the same way to a component that
2473
+ * sets either.
2474
+ *
2475
+ * 2. Owning `transition` means owning ALL of it, so the capsule chrome
2476
+ * properties are in the fold's list too, at the quick duration the
2477
+ * components themselves use. Dropping them would trade a broken fold
2478
+ * for a broken hover.
2479
+ *
2480
+ * Which floors the fold RELEASES when folded — see the folded rule below:
2481
+ * min-inline-size, padding-inline, border-inline-width. Those are exactly
2482
+ * the three inputs that can hold a border-box inline size above zero.
2483
+ *
2338
2484
  * Canonical use: a transient action in a toolbar/pill row (a
2339
2485
  * scroll-to-top arrow, a contextual button) whose arrival should slide
2340
2486
  * the row open rather than jump it.
2341
2487
  * Usage:
2342
2488
  * <div style="display:flex; gap:4px"> (any row)
2343
- * <button class="juno-fold" data-juno-in aria-label="Scroll to top">…</button>
2489
+ * <button class="juno-fold juno-pillbar__item" data-juno-in aria-label="Scroll to top">…</button>
2344
2490
  * …other members…
2345
2491
  * </div>
2346
2492
  * prefers-reduced-motion needs nothing component-local — the base
2347
2493
  * layer zeroes every transition duration. */
2348
- .juno-fold {
2494
+ .juno-fold[data-juno-in],
2495
+ .juno-fold:not([data-juno-in]) {
2349
2496
  --juno-fold-size: var(--juno-size-tap-comfortable);
2350
2497
  --juno-fold-gap: 0px;
2351
2498
 
@@ -2354,13 +2501,32 @@ button.juno-chip {
2354
2501
  overflow: hidden;
2355
2502
  transition:
2356
2503
  inline-size var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
2504
+ min-inline-size var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
2505
+ padding var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
2506
+ border-width var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
2357
2507
  margin var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
2358
2508
  opacity var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
2359
- visibility var(--juno-motion-duration-base) var(--juno-motion-ease-standard);
2509
+ visibility var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
2510
+ color var(--juno-motion-duration-quick) var(--juno-motion-ease-standard),
2511
+ background-color var(--juno-motion-duration-quick) var(--juno-motion-ease-standard);
2360
2512
  }
2361
2513
 
2514
+ /* Folded away. A min-* floor clamps the USED value whichever rule won the
2515
+ cascade, and a border-box inline size can never resolve below its own
2516
+ padding plus border — so a composed capsule holds the slot open however
2517
+ hard the fold pushes. Measured against the built bundle, composed with
2518
+ .juno-pillbar__item at a 390px viewport: 44px with both floors in place
2519
+ (the tap target), 20px with min-inline-size released (the padding alone),
2520
+ 0px with all of them released.
2521
+ min-inline-size is in the transition list above for the OPENING half:
2522
+ released to 0 it would otherwise snap back to the capsule's tap floor the
2523
+ instant data-juno-in returns, and the used width — max(inline-size,
2524
+ min-inline-size) — would jump to full width with nothing to animate. */
2362
2525
  .juno-fold:not([data-juno-in]) {
2363
2526
  inline-size: 0;
2527
+ min-inline-size: 0;
2528
+ padding-inline: 0;
2529
+ border-inline-width: 0;
2364
2530
  margin-inline-end: calc(-1 * var(--juno-fold-gap));
2365
2531
  opacity: 0;
2366
2532
  visibility: hidden;
@@ -4187,11 +4353,20 @@ button.juno-list__row:disabled {
4187
4353
  opacity: 0;
4188
4354
  }
4189
4355
 
4190
- /* the pill — a span after the radio, or the aria-pressed button itself */
4356
+ /* the pill — a span after the radio, or the aria-pressed button itself.
4357
+ The tap floor is on the PAINTED box, not on the label that wraps it: the
4358
+ label is a bare inline-flex and takes its height from this. Segmented is a
4359
+ phone-first control and is routinely the only control on a whole settings
4360
+ section, so it holds --juno-size-tap-min like .juno-btn does — 24px (WCAG
4361
+ 2.2 AA, 2.5.8) on a fine pointer, promoted to the 44px comfortable target
4362
+ under (pointer: coarse) by base.css. Without it the pill computed ~25px
4363
+ from its padding alone, which is the AA floor by accident and nothing on
4364
+ touch. See 20260826-025. */
4191
4365
  .juno-seg__opt input + span,
4192
4366
  button.juno-seg__opt {
4193
4367
  display: inline-flex;
4194
4368
  align-items: center;
4369
+ min-block-size: var(--juno-size-tap-min);
4195
4370
  gap: var(--juno-gap-control);
4196
4371
  font-family: var(--juno-font-family-sans);
4197
4372
  font-size: var(--juno-font-size-11);
@@ -4239,7 +4414,11 @@ button.juno-seg__opt:disabled {
4239
4414
  cursor: not-allowed;
4240
4415
  }
4241
4416
 
4242
- /* Small — matches .juno-btn--sm; for dense toolbars */
4417
+ /* Small — dense toolbars: less type, tighter padding. UNLIKE .juno-btn--sm it
4418
+ does NOT drop below the tap floor, which the base rule above holds: --sm is a
4419
+ density modifier here, not an opt-out of the touch target. A segmented row is
4420
+ often the only control on a settings section, so a sub-tap variant of it has
4421
+ no safe use on a phone. */
4243
4422
  .juno-seg--sm .juno-seg__opt input + span,
4244
4423
  .juno-seg--sm > button.juno-seg__opt {
4245
4424
  font-size: var(--juno-font-size-10);
@@ -115,6 +115,12 @@ tokens land, and never removes a guarantee without a semver-major note.
115
115
  base layer raises `--juno-size-tap-min` to the comfortable 44px, so every
116
116
  control sized off the tap minimum grows on touch devices. A cascade override —
117
117
  the token values themselves don't change.
118
+ - **One control grows on one axis only.** `.juno-pagination`'s items read the tap
119
+ minimum for `min-inline-size` but carry a fixed `block-size` of 32px, so on a
120
+ coarse pointer they are 44 × 32 — above the 24px AA floor, below the 44px
121
+ enhanced one. Tracked as ticket 20260815-040; the numeric coarse-pointer check
122
+ in `test/visual/tap-targets.spec.mjs` does not cover pagination yet, which is
123
+ why it went unnoticed.
118
124
  - Hover-revealed affordances get a touch fallback: table row actions stay
119
125
  visible under `@media (hover: none)`.
120
126
 
@@ -166,6 +166,9 @@ declarations in `@supports` costs bytes and buys nothing.
166
166
 
167
167
  ## Related
168
168
 
169
+ - [ios-pwa.md](./ios-pwa.md) — the five-minute version for a consumer about to
170
+ integrate: what these floors buy you, what you must ship yourself, and what
171
+ junoui does not do at all.
169
172
  - [ios-conformance.md](./ios-conformance.md) — iOS metrics and behaviour: safe
170
173
  areas, tap targets, the viewport-unit decision, and the Home-Screen standalone
171
174
  letterbox.
@@ -14,6 +14,7 @@
14
14
  | `.juno-btn` | Base. Primary (filled): background = role, text = `s0`. Default role = `active`. |
15
15
  | `.juno-btn--ghost` | Transparent, `data` text, `border` outline; hover → `s2`. |
16
16
  | `.juno-btn--sm` | Dense-toolbar size: `font.size.11`, `space.4` × `space.10`, `radius.3`. |
17
+ | `.juno-btn--dense` | On `--sm` only: keeps the 24px height on touch as well. Opt out by name. |
17
18
  | `.juno--<role>` | Sets the primary fill color. |
18
19
  | `:disabled` | `muted` fill, `label` text, not-allowed. |
19
20
 
@@ -28,5 +29,13 @@
28
29
  - Primary action filled with `nominal` (confirm) or `active` (apply).
29
30
  - `warning` fill only for genuinely critical/destructive actions.
30
31
  - One primary per group; everything else is ghost.
31
- - `--sm` drops below the WCAG tap-minimum on purpose — it is for dense **desktop**
32
- toolbars (40–46px chrome bars). Keep the default size anywhere touch is expected.
32
+ - `--sm` is a **density**, not a semantic. It is for dense desktop toolbars
33
+ (40–46px chrome bars): 24px on a fine pointer, the WCAG 2.2 AA floor (2.5.8)
34
+ exactly. On a **coarse pointer it promotes to `size.tap.min`** — type and
35
+ padding still shrink, but the tap target does not. Reaching for `--sm` to mean
36
+ "secondary" is the common mistake; `--ghost` is the secondary modifier and
37
+ stands alone at full size.
38
+ - `.juno-btn--dense` opts a `--sm` button back out of that promotion, for a
39
+ toolbar that is genuinely dense on touch (a scrubber, an editor rail). It does
40
+ nothing without `--sm`, on purpose: a dense touch target should be chosen by
41
+ name, never inherited from a size.
@@ -187,6 +187,40 @@ over scroll-event discretization — anything longer visibly lags the finger.
187
187
  plus the pill's padding and border), `--juno-dock-edge-gap` (what the
188
188
  floating margins take from `100%` — default `2 × space.12`).
189
189
 
190
+ ## How many items fit
191
+
192
+ `.juno-dock__item` is `flex: 1 1 0`, so the bar divides its inner width by
193
+ however many items are present. A consumer deciding **how many to render** — and
194
+ whether they still hold a tap target — reads that budget from junoui rather than
195
+ re-deriving it from the numbers in `dock.css`:
196
+
197
+ | Custom property | What it is |
198
+ | --------------------------- | --------------------------------------------------------------------------------------------------------------------- |
199
+ | `--juno-dock-items` | The item budget. **You set it** to what you render; junoui does not enforce it, it derives from it. |
200
+ | `--juno-dock-item-inline` | The width one item gets. A prediction of what the flex layout produces — asserted against the measured box in CI. |
201
+ | `--juno-dock-fit-inline` | The narrowest viewport at which every item still holds `--juno-size-tap-comfortable`. Below it, drop an item. |
202
+ | `--juno-dock-chrome-inline` | The bar's total inline chrome (margin + padding + border, both sides). `0` on the full-bleed bar, `34px` on the pill. |
203
+ | `--juno-dock-avail` | The width the budget divides. Defaults to `100vw`; override it when the bar is not viewport-wide. |
204
+
205
+ ```css
206
+ /* five destinations need 254px; below that, render four */
207
+ @media (max-width: 253px) {
208
+ .my-dock__item--secondary {
209
+ display: none;
210
+ }
211
+ }
212
+ ```
213
+
214
+ The margin, padding and border terms are declared **once** and consumed by both
215
+ the variant's own box and the sum above, so the budget cannot disagree with the
216
+ bar it describes — the same construction as `--juno-dock-edge-offset`.
217
+
218
+ **There is deliberately no scale floor.** "What scale keeps a 44px target?" is
219
+ `44px / --juno-dock-item-inline`, a ratio of two lengths, and CSS cannot divide
220
+ by a length. A consumer that must scale rather than drop compares those two
221
+ values itself. Prefer dropping an item: scaling a bar scales its hit areas with
222
+ it, which is the problem the floor was being computed to avoid.
223
+
190
224
  ## Anatomy (any platform)
191
225
 
192
226
  - Full-width bar on `s1`, hairline seam on the block-start edge; items split the
@@ -10,7 +10,7 @@ coming in, end-of-fade going out). Keep the element mounted; flip
10
10
 
11
11
  ```html
12
12
  <div class="juno-pillbar">
13
- <button class="juno-fold" data-juno-in aria-label="Scroll to top">
13
+ <button class="juno-fold juno-pillbar__item" data-juno-in aria-label="Scroll to top">
14
14
  <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-arrow-up" /></svg>
15
15
  </button>
16
16
  …other members…
@@ -24,6 +24,31 @@ coming in, end-of-fade going out). Keep the element mounted; flip
24
24
  | `--juno-fold-size` | The slot's open width (default `size.tap.comfortable`) — must be definite, transitions cannot run to an intrinsic size. |
25
25
  | `--juno-fold-gap` | The row's flex/grid gap (default `0px`) — the folded slot swallows one gap with a negative margin so the row closes completely. |
26
26
 
27
+ ## Composing it with a component class
28
+
29
+ The canonical use puts `.juno-fold` on an element that already carries a
30
+ component class — `.juno-pillbar__item`, `.juno-btn`, `.juno-chip` — because that
31
+ is where the capsule chrome (tap target, padding, hover, focus ring,
32
+ `aria-pressed`) lives. The fold is built for that, and two things about it are
33
+ worth knowing rather than rediscovering:
34
+
35
+ **Which floors it releases.** A border-box inline size has exactly three inputs
36
+ that can hold it above zero, and the folded state releases all three:
37
+ `min-inline-size` (a component's tap floor), `padding-inline`, and
38
+ `border-inline-width`. Each is in the fold's transition list too, so the content
39
+ neither snaps sideways as the fold starts nor jumps to full width when the slot
40
+ opens. Composed with `.juno-pillbar__item` and none of them released, the folded
41
+ slot lays out at 44px — the tap target — and the row never closes.
42
+
43
+ **It takes over the element's `transition`.** `transition` is a shorthand: two
44
+ rules setting it do not merge, the winner replaces the loser's whole list. The
45
+ fold's declarations are stated at attribute specificity so it wins that, and its
46
+ list therefore also carries the chrome properties (`color`, `background-color`)
47
+ the components animate — composing costs the capsule nothing. A component of your
48
+ own that composes with `.juno-fold` and needs a third property transitioned
49
+ should state it at higher specificity than `.juno-fold[data-juno-in]` (0,2,0),
50
+ repeating the fold's own entries, or the fold's list wins and yours is dropped.
51
+
27
52
  - The row's own width follows the slot, so the whole row slides open/closed.
28
53
  - `prefers-reduced-motion`: handled by the base layer (states still apply,
29
54
  transition durations collapse).