@junoput01/junoui 0.3.0 → 0.5.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/README.md +23 -19
  3. package/dist/css/juno-custom-media.css +32 -0
  4. package/dist/css/juno.css +1216 -60
  5. package/dist/icons/inline.js +22 -0
  6. package/dist/icons/juno-icons.svg +1 -0
  7. package/docs/accessibility.md +55 -34
  8. package/docs/boot-shell.md +295 -0
  9. package/docs/components/README.md +46 -43
  10. package/docs/components/dock.md +167 -8
  11. package/docs/components/drawer.md +47 -7
  12. package/docs/components/fold-slot.md +31 -0
  13. package/docs/components/icon-loader.md +85 -18
  14. package/docs/components/icon.md +21 -0
  15. package/docs/components/load-state.md +131 -0
  16. package/docs/components/loader.md +13 -5
  17. package/docs/components/pillbar.md +157 -10
  18. package/docs/components/reload.md +41 -0
  19. package/docs/components/skeleton.md +22 -15
  20. package/docs/components/thumb.md +34 -14
  21. package/docs/design-guidelines.md +26 -0
  22. package/docs/getting-started.md +23 -0
  23. package/docs/icon-subsetting.md +59 -0
  24. package/docs/ios-conformance.md +224 -0
  25. package/docs/layout.md +71 -1
  26. package/docs/web.md +6 -0
  27. package/package.json +7 -2
  28. package/src/css/base.css +169 -6
  29. package/src/css/components/dock.css +322 -0
  30. package/src/css/components/drawer.css +40 -3
  31. package/src/css/components/fold-slot.css +44 -0
  32. package/src/css/components/icon-loader.css +32 -18
  33. package/src/css/components/icon.css +6 -4
  34. package/src/css/components/load-state.css +136 -0
  35. package/src/css/components/loader.css +6 -0
  36. package/src/css/components/menu.css +4 -0
  37. package/src/css/components/modal.css +24 -3
  38. package/src/css/components/navbar.css +5 -1
  39. package/src/css/components/pillbar.css +207 -7
  40. package/src/css/components/reload.css +48 -0
  41. package/src/css/components/skeleton.css +41 -13
  42. package/src/css/components/tabs.css +5 -0
  43. package/src/css/components/thumb.css +63 -1
  44. package/src/css/components/toast.css +5 -1
  45. package/src/css/density.css +22 -0
  46. package/src/css/layout.css +30 -2
  47. package/src/css/utilities.css +4 -1
  48. package/src/icons/cloud-slash.svg +1 -0
  49. package/tools/subset-sprite.mjs +56 -0
@@ -21,6 +21,14 @@
21
21
  .juno-dock {
22
22
  --juno-role: var(--juno-active);
23
23
 
24
+ /* Consumer-driven shrink (e.g. dim/compact the bar on scroll) — default 1,
25
+ a no-op. Applies to the whole bar regardless of variant (plain/--fixed/
26
+ --pill/--float/--icon). Transition duration is authored through the
27
+ motion scale (see base.css's JS-readable --juno-motion contract) so
28
+ prefers-reduced-motion collapses it to 0 without a component-local
29
+ media query. */
30
+ --juno-dock-scale: 1;
31
+
24
32
  position: sticky;
25
33
  inset-block-end: 0;
26
34
  z-index: var(--juno-z-raised);
@@ -29,6 +37,9 @@
29
37
  background: var(--juno-s1);
30
38
  border-block-start: var(--juno-border-width-1) solid var(--juno-border);
31
39
  padding-block-end: env(safe-area-inset-bottom, 0);
40
+ transform: scale(var(--juno-dock-scale));
41
+ transform-origin: bottom center;
42
+ transition: transform calc(var(--juno-motion-duration-quick) * var(--juno-motion-scale)) var(--juno-motion-ease-standard);
32
43
  }
33
44
 
34
45
  .juno-dock__item {
@@ -41,6 +52,13 @@
41
52
  justify-content: center;
42
53
  gap: var(--juno-space-2);
43
54
  padding-block: var(--juno-space-4);
55
+
56
+ /* Full reset, not just the top edge: a plain `border-block-start` override
57
+ leaves the UA default border on the other three sides of a <button>
58
+ item (links have none, so this went unnoticed until <button> showed up
59
+ in the pill variant's usage). Re-add only the edge that carries the
60
+ active accent. */
61
+ border: none;
44
62
  border-block-start: var(--juno-border-width-2) solid transparent;
45
63
  font-family: var(--juno-font-family-sans);
46
64
  font-size: var(--juno-font-size-10);
@@ -85,3 +103,307 @@
85
103
  inset-inline: 0;
86
104
  inset-block-end: 0;
87
105
  }
106
+
107
+ /* ── Pill variant — the full-width bar becomes a rounded floating pill with
108
+ big glyphs in circular bubbles. Labels drop; each icon sits in a circular
109
+ .juno-dock__bubble that fills only on the active tab (not the whole item).
110
+ Out of flow (fixed) so the page scrolls UNDER it — reserve room on the
111
+ scroller with padding-block-end: var(--juno-dock-clearance).
112
+ Usage — pair the bubble with .juno-icon-loader, junoui's concentric-stack
113
+ primitive, so a loading arc can be dropped in without re-rolling geometry:
114
+ <nav class="juno-dock juno-dock--pill juno-hide-from-md" aria-label="Primary">
115
+ <a class="juno-dock__item" href="…" aria-current="page" aria-label="Library">
116
+ <span class="juno-dock__bubble juno-icon-loader">
117
+ <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-squares-four" /></svg>
118
+ </span>
119
+ </a>
120
+ </nav>
121
+ Labels are hidden, so every item MUST carry an aria-label (on the <a>/<button>)
122
+ — the glyph alone is not an accessible name. */
123
+ .juno-dock--pill {
124
+ --juno-role: var(--juno-active);
125
+
126
+ position: fixed;
127
+ inset-inline: 0;
128
+ inset-block-end: 0;
129
+ z-index: var(--juno-z-raised);
130
+ margin: 0 var(--juno-space-12) calc(var(--juno-space-16) + env(safe-area-inset-bottom, 0px));
131
+ padding: var(--juno-space-4);
132
+ background: color-mix(in srgb, var(--juno-s1) 88%, transparent);
133
+ -webkit-backdrop-filter: blur(12px);
134
+ backdrop-filter: blur(12px);
135
+ border: var(--juno-border-width-1) solid var(--juno-border);
136
+ border-radius: 999px;
137
+ box-shadow: var(--juno-shadow-2);
138
+ }
139
+
140
+ .juno-dock--pill .juno-dock__item {
141
+ min-block-size: 0;
142
+ gap: 0;
143
+ padding-block: var(--juno-space-4);
144
+
145
+ /* full reset: the base only zeroes the block-start border, but the pill has
146
+ no top edge to accent — the active state moves to the bubble. Also kill the
147
+ UA tap-highlight so no square flashes past the round bubble on tap. */
148
+ border: none;
149
+ background: none;
150
+ -webkit-tap-highlight-color: transparent;
151
+ }
152
+
153
+ .juno-dock--pill .juno-dock__label { display: none; }
154
+
155
+ /* Focus lands on the bubble so the ring hugs the round target, not the wide
156
+ item box. */
157
+ .juno-dock--pill .juno-dock__item:focus-visible { outline: none; }
158
+
159
+ .juno-dock--pill .juno-dock__item:focus-visible .juno-dock__bubble {
160
+ outline: var(--juno-border-width-2) solid var(--juno-active);
161
+ outline-offset: var(--juno-space-2);
162
+ }
163
+
164
+ /* The bubble is a definite-size circular surface that centres its glyph. It
165
+ declares the same single-cell grid as .juno-icon-loader so composing the
166
+ two (see the usage block above) can't fight over `display`, and so the
167
+ ring — when one is present — is concentric with the glyph. */
168
+ .juno-dock__bubble {
169
+ /* Section-loading ring — with .juno-icon-loader on the same element, an
170
+ indeterminate .juno-arc dropped inside rings the bubble's EDGE (diameter
171
+ = the bubble, thin stroke) instead of the glyph. Because the bubble's box
172
+ is definite, adding or removing the arc never resizes it. */
173
+ --juno-icon-loader-ring: var(--juno-size-tap-comfortable);
174
+ --juno-icon-loader-ring-width: 2px;
175
+
176
+ display: grid;
177
+ place-items: center;
178
+ inline-size: var(--juno-size-tap-comfortable);
179
+ block-size: var(--juno-size-tap-comfortable);
180
+ border-radius: 50%;
181
+ transition: background-color var(--juno-motion-duration-quick) var(--juno-motion-ease-standard);
182
+ }
183
+
184
+ /* Explicit big glyph so the pill reads at a glance. Set on the glyph itself so
185
+ it wins regardless of context (the base .juno-icon no longer pins a size —
186
+ see 20260727-011). */
187
+ .juno-dock--pill .juno-icon { --juno-icon-size: var(--juno-space-32); }
188
+
189
+ /* Active reads on the bubble alone — suppress the base full-item fill so only
190
+ the circular highlight shows. */
191
+ .juno-dock--pill .juno-dock__item[aria-current] {
192
+ background: none;
193
+ color: var(--juno-role);
194
+ }
195
+
196
+ .juno-dock--pill .juno-dock__item[aria-current] .juno-dock__bubble {
197
+ background: var(--juno-s3);
198
+ }
199
+
200
+ /* reduced-transparency → solid surface, no blur (matches the pillbar) */
201
+ @media (prefers-reduced-transparency: reduce) {
202
+ .juno-dock--pill {
203
+ background: var(--juno-s1);
204
+ -webkit-backdrop-filter: none;
205
+ backdrop-filter: none;
206
+ }
207
+ }
208
+
209
+ /* ── Float variant — the pillbar's floating capsule chrome (fixed, rounded
210
+ shell, blur, shadow, off-edge margin) applied to the dock's OWN item model:
211
+ unlike --pill it keeps icon+label items and the full-item active fill —
212
+ only the exterior changes. Combine with --icon below for the bubble
213
+ treatment inside a capsule (the old --pill look, decomposed). `border`
214
+ fully replaces the base's `border-block-start` (no top seam on a capsule),
215
+ and `padding` fully replaces the base's safe-area padding (the margin
216
+ below carries the safe area instead — additive offset off the edge, not
217
+ `max()`: see docs/ios-conformance.md). `overflow: hidden` clips the
218
+ items' square corners to the capsule's rounded shape.
219
+ Usage:
220
+ <nav class="juno-dock juno-dock--float juno-hide-from-md" aria-label="Primary">
221
+ <a class="juno-dock__item" href="…" aria-current="page">
222
+ <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-squares-four" /></svg>
223
+ <span class="juno-dock__label">Library</span>
224
+ </a>
225
+ </nav>
226
+ Out of flow (fixed) so the page scrolls UNDER it — reserve room on the
227
+ scroller with padding-block-end: var(--juno-dock-clearance). */
228
+ .juno-dock--float {
229
+ position: fixed;
230
+ inset-inline: 0;
231
+ inset-block-end: 0;
232
+ z-index: var(--juno-z-raised);
233
+ margin: 0 var(--juno-space-12) calc(var(--juno-space-16) + env(safe-area-inset-bottom, 0px));
234
+ padding: var(--juno-space-4);
235
+ overflow: hidden;
236
+ background: color-mix(in srgb, var(--juno-s1) 88%, transparent);
237
+ -webkit-backdrop-filter: blur(12px);
238
+ backdrop-filter: blur(12px);
239
+ border: var(--juno-border-width-1) solid var(--juno-border);
240
+ border-radius: 999px;
241
+ box-shadow: var(--juno-shadow-2);
242
+ }
243
+
244
+ /* reduced-transparency → solid surface, no blur (matches the pillbar / --pill) */
245
+ @media (prefers-reduced-transparency: reduce) {
246
+ .juno-dock--float {
247
+ background: var(--juno-s1);
248
+ -webkit-backdrop-filter: none;
249
+ backdrop-filter: none;
250
+ }
251
+ }
252
+
253
+ /* ── Icon variant — labels drop, glyphs grow, and the active state moves to
254
+ a circular .juno-dock__bubble instead of filling the whole item, WITHOUT
255
+ forcing the floating capsule chrome (that's --float, above — combine both
256
+ for the old --pill look). Usable standalone on a plain sticky/--fixed bar
257
+ for a compact, icon-only full-width dock.
258
+ Usage — same .juno-dock__bubble contract as --pill (pair with
259
+ .juno-icon-loader; see docs/components/dock.md#pill-variant):
260
+ <nav class="juno-dock juno-dock--icon juno-hide-from-md" aria-label="Primary">
261
+ <a class="juno-dock__item" href="…" aria-current="page" aria-label="Library">
262
+ <span class="juno-dock__bubble juno-icon-loader">
263
+ <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-squares-four" /></svg>
264
+ </span>
265
+ </a>
266
+ </nav>
267
+ Labels are hidden, so every item MUST carry an aria-label — the glyph
268
+ alone is not an accessible name. */
269
+ .juno-dock--icon .juno-dock__item {
270
+ gap: 0;
271
+
272
+ /* accent moves to the bubble; kill the item-edge border in both states */
273
+ border-color: transparent;
274
+ }
275
+
276
+ .juno-dock--icon .juno-dock__label { display: none; }
277
+
278
+ /* Explicit big glyph so the icon-only bar reads at a glance — set on the
279
+ glyph itself so it wins regardless of context (the base .juno-icon no
280
+ longer pins a size — see 20260727-011). */
281
+ .juno-dock--icon .juno-icon { --juno-icon-size: var(--juno-space-32); }
282
+
283
+ /* Focus lands on the bubble so the ring hugs the round target, not the wide
284
+ item box. */
285
+ .juno-dock--icon .juno-dock__item:focus-visible { outline: none; }
286
+
287
+ .juno-dock--icon .juno-dock__item:focus-visible .juno-dock__bubble {
288
+ outline: var(--juno-border-width-2) solid var(--juno-active);
289
+ outline-offset: var(--juno-space-2);
290
+ }
291
+
292
+ /* Active reads on the bubble alone — suppress the base full-item fill so
293
+ only the circular highlight shows. */
294
+ .juno-dock--icon .juno-dock__item[aria-current] {
295
+ background: none;
296
+ color: var(--juno-role);
297
+ }
298
+
299
+ .juno-dock--icon .juno-dock__item[aria-current] .juno-dock__bubble {
300
+ background: var(--juno-s3);
301
+ }
302
+
303
+ /* ── Collapsible variant — the whole bar folds into a single circular
304
+ .juno-dock__knob at the inline-end edge, and unfolds back to full width.
305
+ Driven by ONE inherited custom prop the app writes (junoui ships no JS):
306
+ --juno-dock-fold, 0 = fully open … 1 = the circle. Write it per scroll
307
+ frame for a gesture-tracked fold (set it on the dock, an ancestor, or
308
+ :root — it is only CONSUMED here, never declared, so an inherited value
309
+ always lands), or flip it 0/1 for a plain toggle.
310
+
311
+ The fold has two phases, split at --juno-dock-fold-split: first the bar
312
+ SHRINKS in place to --juno-dock-fold-scale (transform, no relayout),
313
+ then it SLIDES shut to --juno-dock-collapsed-size. Width interpolates
314
+ between two definite lengths because a transition cannot run to or from
315
+ an intrinsic size; --juno-dock-fold-smoothing is smoothing over
316
+ scroll-event discretization, not the fold itself — anything longer lags
317
+ the finger driving it.
318
+
319
+ Compose with a fixed placement (--pill, --float, or --fixed): the fold
320
+ re-anchors the bar to the inline-end edge, which only means something
321
+ out of flow. Transform-origin is the physical bottom right — the fold's
322
+ own scale REPLACES the base --juno-dock-scale hook (one transform slot);
323
+ RTL consumers flip the origin and inset overrides together.
324
+
325
+ Structure — items live in a tray; the knob is the circle's face:
326
+ <nav class="juno-dock juno-dock--pill juno-dock--collapsible …"
327
+ data-juno-collapsed>
328
+ <div class="juno-dock__tray"> …the usual __item children… </div>
329
+ <button class="juno-dock__knob" aria-label="Show navigation">
330
+ <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-dots-three" /></svg>
331
+ </button>
332
+ </nav>
333
+
334
+ data-juno-collapsed is the END state, set by the app only at fold = 1
335
+ (and removed the moment the fold reopens): it flips the tray to
336
+ visibility:hidden — which removes its items from the tab order — and
337
+ reveals the knob. Mid-fold both stay live; the tray's opacity empties
338
+ ahead of the slide (×1.6) so the glyphs are gone before the circle
339
+ closes. The knob MUST carry an aria-label; collapsing while focus is
340
+ inside the tray is the app's edge — move focus to the knob first.
341
+ prefers-reduced-motion needs nothing component-local: the base layer
342
+ zeroes every transition, and the fold value itself is app-driven. */
343
+ .juno-dock--collapsible {
344
+ --juno-dock-fold-split: 0.35;
345
+ --juno-dock-fold-scale: 0.78;
346
+ --juno-dock-collapsed-size: calc(
347
+ var(--juno-size-tap-comfortable) + 2 * var(--juno-space-4) + 2 * var(--juno-border-width-1)
348
+ );
349
+ --juno-dock-edge-gap: calc(2 * var(--juno-space-12));
350
+ --juno-dock-fold-smoothing: 90ms;
351
+
352
+ /* per-phase normalized progress, derived — never set these directly */
353
+ --juno-dock-fold-shrink: min(1, var(--juno-dock-fold, 0) / var(--juno-dock-fold-split));
354
+ --juno-dock-fold-slide: clamp(
355
+ 0,
356
+ (var(--juno-dock-fold, 0) - var(--juno-dock-fold-split)) / (1 - var(--juno-dock-fold-split)),
357
+ 1
358
+ );
359
+
360
+ inset-inline-start: auto;
361
+ inline-size: calc(
362
+ var(--juno-dock-collapsed-size) +
363
+ (100% - var(--juno-dock-edge-gap) - var(--juno-dock-collapsed-size)) *
364
+ (1 - var(--juno-dock-fold-slide))
365
+ );
366
+ overflow: hidden;
367
+ transform: scale(calc(1 - (1 - var(--juno-dock-fold-scale)) * var(--juno-dock-fold-shrink)));
368
+ transform-origin: bottom right;
369
+ transition:
370
+ transform var(--juno-dock-fold-smoothing) linear,
371
+ inline-size var(--juno-dock-fold-smoothing) linear;
372
+ }
373
+
374
+ .juno-dock__tray {
375
+ flex: 1;
376
+ min-inline-size: 0;
377
+ display: flex;
378
+ align-items: stretch;
379
+ opacity: calc(1 - var(--juno-dock-fold-slide, 0) * 1.6);
380
+ transition: opacity var(--juno-dock-fold-smoothing, 90ms) linear;
381
+ }
382
+
383
+ .juno-dock__knob {
384
+ position: absolute;
385
+ inset-block: 0;
386
+ inset-inline-end: var(--juno-space-4);
387
+ margin-block: auto;
388
+ inline-size: var(--juno-size-tap-comfortable);
389
+ block-size: var(--juno-size-tap-comfortable);
390
+ display: grid;
391
+ place-items: center;
392
+ padding: 0;
393
+ border: none;
394
+ border-radius: 50%;
395
+ background: none;
396
+ color: var(--juno-label);
397
+ cursor: pointer;
398
+ -webkit-tap-highlight-color: transparent;
399
+ opacity: 0;
400
+ visibility: hidden;
401
+ transition: opacity var(--juno-motion-duration-quick) var(--juno-motion-ease-standard);
402
+ }
403
+
404
+ .juno-dock--collapsible[data-juno-collapsed] .juno-dock__tray { visibility: hidden; }
405
+
406
+ .juno-dock--collapsible[data-juno-collapsed] .juno-dock__knob {
407
+ opacity: 1;
408
+ visibility: visible;
409
+ }
@@ -10,6 +10,12 @@
10
10
  * Reuses .juno-modal for the surface + scrim; this file re-pins + re-slides.
11
11
  * Selectors are compound (.juno-modal.juno-drawer) so they override the
12
12
  * modal's centered transform regardless of bundle order.
13
+ *
14
+ * --bottom is a real bottom sheet: a grab handle (.juno-sheet__handle,
15
+ * decorative) plus a CSS height knob (--juno-sheet-h, default 60dvh —
16
+ * apps swap it for peek/half/full snap points, capped by --juno-sheet-max).
17
+ * No drag gesture ships here: resizing/dismissing by drag is stateful and
18
+ * stays in the app. See docs/components/drawer.md and 20260802-019.
13
19
  * ════════════════════════════════════════════════════════════════════ */
14
20
 
15
21
  .juno-modal.juno-drawer {
@@ -55,15 +61,24 @@
55
61
  .juno-modal.juno-drawer--bottom {
56
62
  inline-size: 100%;
57
63
  max-inline-size: 100%;
58
- block-size: 60dvh;
64
+
65
+ /* height knob: swap --juno-sheet-h for snap points (peek/half/full).
66
+ --juno-sheet-max is the hard ceiling so a swapped value can't run past
67
+ it. CSS-only — the drag gesture a real snap-point picker needs is
68
+ stateful and stays in the app. */
69
+ block-size: var(--juno-sheet-h, 60dvh);
70
+ max-block-size: var(--juno-sheet-max, 92dvh);
59
71
  margin-block: auto 0;
60
72
  margin-inline: 0;
61
73
  border: none;
62
74
  border-block-start: var(--juno-border-width-1) solid var(--juno-border);
75
+ border-start-start-radius: var(--juno-radius-8);
76
+ border-start-end-radius: var(--juno-radius-8);
63
77
  transform: translateY(100%);
64
78
 
65
- /* keep content above the home indicator */
66
- padding-block-end: env(safe-area-inset-bottom, 0);
79
+ /* safe area moves to .juno-modal__body below (the scroll port) — padding
80
+ here would just be dead space under the grab handle. See 20260802-019. */
81
+ padding-block-end: 0;
67
82
  }
68
83
 
69
84
  .juno-modal.juno-drawer--bottom[open] { transform: translateY(0); }
@@ -72,6 +87,28 @@
72
87
  .juno-modal.juno-drawer--bottom[open] { transform: translateY(100%); }
73
88
  }
74
89
 
90
+ /* grab handle — visual affordance only. aria-hidden in markup: the dialog's
91
+ own aria-labelledby (→ the title) stays the accessible name. Dragging it
92
+ to resize/dismiss is stateful, so junoui draws it but never wires a
93
+ gesture to it — see docs/accessibility.md. */
94
+ .juno-sheet__handle {
95
+ inline-size: var(--juno-space-40);
96
+ block-size: var(--juno-space-4);
97
+ margin: var(--juno-space-8) auto;
98
+ border-radius: 999px;
99
+ background: var(--juno-border-strong);
100
+ }
101
+
102
+ /* the scrolling body owns the safe area now, mirroring modal.css's mobile
103
+ sheet. Compound selector for specificity: drawer.css sorts before
104
+ modal.css in the bundle (scripts/bundle-css.mjs), so a bare
105
+ .juno-modal__body override here would lose to modal.css's own rule at
106
+ equal specificity. env() fallback keeps its unit inside calc() — see
107
+ docs/ios-conformance.md. */
108
+ .juno-modal.juno-drawer--bottom .juno-modal__body {
109
+ padding-block-end: calc(var(--juno-pad-surface-inline) + env(safe-area-inset-bottom, 0px));
110
+ }
111
+
75
112
  /* ── narrow viewports ────────────────────────────────────────────────────
76
113
  Side drawers cap below the full width so a sliver of scrim survives —
77
114
  the visual cue (and tap target) that "outside" closes the panel. */
@@ -0,0 +1,44 @@
1
+ /* ════════════════════════════════════════════════════════════════════
2
+ * Component — Fold slot (animated presence for a row member)
3
+ * A slot inside any flex/grid row that animates between PRESENT and
4
+ * FOLDED-AWAY instead of popping in and out of the DOM: its inline-size
5
+ * folds to zero, it fades, and at the end of the fold it leaves the
6
+ * tab order via visibility (discrete transition: instant coming in,
7
+ * end-of-fade going out). Keep the element mounted; drive the state
8
+ * with the data-juno-in attribute — zero JS here, the app owns when.
9
+ *
10
+ * The size must be DEFINITE (--juno-fold-size) because a transition
11
+ * cannot run to or from an intrinsic width. If the row uses a flex/grid
12
+ * gap, the folded slot still occupies one gap — name it in
13
+ * --juno-fold-gap and the fold swallows it with a negative margin.
14
+ *
15
+ * Canonical use: a transient action in a toolbar/pill row (a
16
+ * scroll-to-top arrow, a contextual button) whose arrival should slide
17
+ * the row open rather than jump it.
18
+ * Usage:
19
+ * <div style="display:flex; gap:4px"> (any row)
20
+ * <button class="juno-fold" data-juno-in aria-label="Scroll to top">…</button>
21
+ * …other members…
22
+ * </div>
23
+ * prefers-reduced-motion needs nothing component-local — the base
24
+ * layer zeroes every transition duration. */
25
+ .juno-fold {
26
+ --juno-fold-size: var(--juno-size-tap-comfortable);
27
+ --juno-fold-gap: 0px;
28
+
29
+ flex-shrink: 0;
30
+ inline-size: var(--juno-fold-size);
31
+ overflow: hidden;
32
+ transition:
33
+ inline-size var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
34
+ margin var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
35
+ opacity var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
36
+ visibility var(--juno-motion-duration-base) var(--juno-motion-ease-standard);
37
+ }
38
+
39
+ .juno-fold:not([data-juno-in]) {
40
+ inline-size: 0;
41
+ margin-inline-end: calc(-1 * var(--juno-fold-gap));
42
+ opacity: 0;
43
+ visibility: hidden;
44
+ }
@@ -1,9 +1,24 @@
1
1
  /* ════════════════════════════════════════════════════════════════════
2
- * Component — Icon loader (nav loading ring)
3
- * A destination icon ringed by the spinning arc while that section loads —
4
- * the "this is loading" affordance for a rail / dock / pillbar item. The
5
- * icon stays static on top; the arc sizes in em around it and eats no
6
- * pointer events, so the nav item still clicks through.
2
+ * Component — Icon loader (the ring-a-control primitive)
3
+ * A control ringed by the spinning arc while its section loads — the
4
+ * "this is loading" affordance for a rail / dock / pillbar item, but also
5
+ * any other control an app needs to ring: an icon button, a badge, an
6
+ * avatar. What it wraps stays static on top; the arc rings it and eats no
7
+ * pointer events, so the wrapped control still clicks through.
8
+ *
9
+ * This is the ONLY concentric-ring mechanism in junoui: anything that
10
+ * wants an arc around it — a nav glyph, a dock bubble, an arbitrary
11
+ * button or badge — composes this class instead of re-rolling absolute
12
+ * positioning. Both ring dimensions are custom props, so one wrapper
13
+ * serves a 14px glyph and a 44px bubble:
14
+ * --juno-icon-loader-ring diameter (default 1.9em — em, so it
15
+ * tracks the glyph it wraps)
16
+ * --juno-icon-loader-ring-width stroke (default 0.14em)
17
+ * A host with a definite box (a dock bubble, an icon button) sets the
18
+ * diameter to that box's size, so the ring hugs the edge and the box does
19
+ * not resize when the arc appears or leaves — ring it without changing
20
+ * its box. Below ~24px, add .juno-arc--smooth (loader.css) alongside
21
+ * --indeterminate: the default 12-step sweep reads as jitter that small.
7
22
  *
8
23
  * The app owns the state: add .juno-arc--indeterminate to spin, drop it
9
24
  * when loaded (gate the blink to first-load, not every refetch). For a
@@ -18,36 +33,35 @@
18
33
  * </a>
19
34
  *
20
35
  * Gotcha encoded here: the arc's rotate animation overrides `transform`, so
21
- * the ring is centred with inset:0 + margin:auto — never translate(-50%,-50%),
36
+ * the ring is centred with a single-cell grid — never translate(-50%,-50%),
22
37
  * which the spin would clobber.
23
38
  * ════════════════════════════════════════════════════════════════════ */
24
39
 
25
40
  .juno-icon-loader {
26
41
  --juno-role: var(--juno-active);
27
42
 
28
- /* Single-cell grid: the icon and the ring occupy the SAME cell and are
29
- both centred in it, so they're guaranteed concentric — no transform (the
30
- arc's rotation would clobber a translate) and no fragile margin math. The
31
- ring is the larger child, so the cell sizes to it (--juno-icon-loader-ring). */
43
+ /* Single-cell grid: every child occupies the SAME cell and is centred in
44
+ it, so the ring and what it rings are guaranteed concentric — no
45
+ transform (the arc's rotation would clobber a translate) and no fragile
46
+ margin math. With no explicit size the cell sizes to its largest child
47
+ (normally the ring, --juno-icon-loader-ring); give the host a definite
48
+ inline/block size to pin the cell and keep the box jitter-free. */
32
49
  display: inline-grid;
33
50
  place-items: center;
34
51
  line-height: 0; /* drop the inline strut so the box hugs the ring */
35
52
  }
36
53
 
37
- .juno-icon-loader > .juno-icon,
38
- .juno-icon-loader > .juno-arc {
54
+ /* every child shares the one cell; the ringed content paints above the ring */
55
+ .juno-icon-loader > * {
39
56
  grid-area: 1 / 1;
40
- }
41
-
42
- /* icon sits above the ring */
43
- .juno-icon-loader > .juno-icon {
44
57
  z-index: 1;
45
58
  }
46
59
 
47
- /* the ring: sized in em (relative to the icon), transparent to clicks */
60
+ /* the ring: sized from the two custom props, transparent to clicks */
48
61
  .juno-icon-loader > .juno-arc {
49
62
  --juno-arc-size: var(--juno-icon-loader-ring, 1.9em);
50
- --juno-arc-width: 0.14em;
63
+ --juno-arc-width: var(--juno-icon-loader-ring-width, 0.14em);
51
64
 
65
+ z-index: 0;
52
66
  pointer-events: none;
53
67
  }
@@ -16,11 +16,13 @@
16
16
  * ════════════════════════════════════════════════════════════════════ */
17
17
 
18
18
  .juno-icon {
19
- --juno-icon-size: 1.25em;
20
-
19
+ /* Default lives in the var() fallback, NOT as an element-level declaration:
20
+ declaring --juno-icon-size on the element would shadow an ancestor that
21
+ sets it for contextual sizing (e.g. .juno-list__chevron). Explicit
22
+ modifiers below still set it on the element and win. See 20260727-011. */
21
23
  display: inline-block;
22
- inline-size: var(--juno-icon-size);
23
- block-size: var(--juno-icon-size);
24
+ inline-size: var(--juno-icon-size, 1.25em);
25
+ block-size: var(--juno-icon-size, 1.25em);
24
26
  flex-shrink: 0;
25
27
  vertical-align: -0.2em; /* optical baseline alignment next to text */
26
28
  fill: currentcolor;