@junoput01/junoui 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/CHANGELOG.md +219 -0
- package/README.md +23 -19
- package/dist/css/juno-custom-media.css +32 -0
- package/dist/css/juno.css +1216 -60
- package/dist/icons/inline.js +22 -0
- package/docs/accessibility.md +55 -34
- package/docs/boot-shell.md +295 -0
- package/docs/components/README.md +46 -43
- package/docs/components/dock.md +167 -8
- package/docs/components/drawer.md +47 -7
- package/docs/components/fold-slot.md +31 -0
- package/docs/components/icon-loader.md +85 -18
- package/docs/components/icon.md +21 -0
- package/docs/components/load-state.md +131 -0
- package/docs/components/loader.md +13 -5
- package/docs/components/pillbar.md +157 -10
- package/docs/components/reload.md +41 -0
- package/docs/components/skeleton.md +22 -15
- package/docs/components/thumb.md +34 -14
- package/docs/design-guidelines.md +26 -0
- package/docs/getting-started.md +23 -0
- package/docs/ios-conformance.md +224 -0
- package/docs/layout.md +71 -1
- package/docs/web.md +6 -0
- package/package.json +5 -2
- package/src/css/base.css +169 -6
- package/src/css/components/dock.css +322 -0
- package/src/css/components/drawer.css +40 -3
- package/src/css/components/fold-slot.css +44 -0
- package/src/css/components/icon-loader.css +32 -18
- package/src/css/components/icon.css +6 -4
- package/src/css/components/load-state.css +136 -0
- package/src/css/components/loader.css +6 -0
- package/src/css/components/menu.css +4 -0
- package/src/css/components/modal.css +24 -3
- package/src/css/components/navbar.css +5 -1
- package/src/css/components/pillbar.css +207 -7
- package/src/css/components/reload.css +48 -0
- package/src/css/components/skeleton.css +41 -13
- package/src/css/components/tabs.css +5 -0
- package/src/css/components/thumb.css +63 -1
- package/src/css/components/toast.css +5 -1
- package/src/css/density.css +22 -0
- package/src/css/layout.css +30 -2
- package/src/css/utilities.css +4 -1
package/dist/css/juno.css
CHANGED
|
@@ -364,7 +364,37 @@
|
|
|
364
364
|
/* System color scheme. Without data-juno-mode the theme follows the OS
|
|
365
365
|
(`prefers-color-scheme`; see juno-tokens.css) — `dark light` here lets the
|
|
366
366
|
UA pick matching scrollbars/form chrome. An explicit mode pins both. */
|
|
367
|
-
:root {
|
|
367
|
+
:root {
|
|
368
|
+
color-scheme: dark light;
|
|
369
|
+
|
|
370
|
+
/* Floating-nav scroll clearance — a page/scroller that a fixed dock or
|
|
371
|
+
pillbar floats over reserves this much room at its foot so the last row
|
|
372
|
+
clears the overlay. Consumers write `padding-block-end:
|
|
373
|
+
var(--juno-dock-clearance)` and stay correct when the dock geometry
|
|
374
|
+
changes — no hand-rolled calc() per app. Web-only (they carry env()), so
|
|
375
|
+
they live in the CSS layer, not the cross-platform token set.
|
|
376
|
+
--juno-dock-clearance ≈ pill height + bottom margin + breathing room;
|
|
377
|
+
--juno-pillbar-clearance suits the shorter centered pill. */
|
|
378
|
+
--juno-dock-clearance: calc(var(--juno-space-72) + var(--juno-space-20) + env(safe-area-inset-bottom, 0px));
|
|
379
|
+
--juno-pillbar-clearance: calc(var(--juno-space-72) + env(safe-area-inset-bottom, 0px));
|
|
380
|
+
|
|
381
|
+
/* JS-readable motion contract. CSS-only `prefers-reduced-motion` never reaches
|
|
382
|
+
imperative JS (scrollTo/scrollIntoView smooth behavior, rAF-driven transforms,
|
|
383
|
+
an "animate or not" branch) — so the preference is also exposed as a custom
|
|
384
|
+
property script can read with one getComputedStyle call, no matchMedia
|
|
385
|
+
listener required for a one-shot check:
|
|
386
|
+
getComputedStyle(document.documentElement)
|
|
387
|
+
.getPropertyValue('--juno-motion').trim() !== 'none'
|
|
388
|
+
A component MAY author its durations through the scale rather than a raw
|
|
389
|
+
duration token — `calc(var(--juno-motion-duration-base) * var(--juno-motion-scale))`
|
|
390
|
+
— so the reduced-motion override below collapses them to 0 without its own
|
|
391
|
+
per-component media query. Web-only DOM contract, so it lives
|
|
392
|
+
here rather than in the cross-platform token set (see --juno-dock-clearance
|
|
393
|
+
above for the same reasoning). */
|
|
394
|
+
--juno-motion: auto;
|
|
395
|
+
--juno-motion-scale: 1;
|
|
396
|
+
}
|
|
397
|
+
|
|
368
398
|
:root[data-juno-mode='dark'] { color-scheme: dark; }
|
|
369
399
|
|
|
370
400
|
/* Control surfaces. On dark panels the hairline border (~23% L) disappears, so
|
|
@@ -441,17 +471,102 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
441
471
|
@media (pointer: coarse) {
|
|
442
472
|
:root { --juno-size-tap-min: var(--juno-size-tap-comfortable); }
|
|
443
473
|
|
|
444
|
-
/* iOS Safari
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
474
|
+
/* iOS Safari is widely observed to zoom the page onto a focused text field
|
|
475
|
+
whose font-size is under 16px. NOTE: no primary WebKit/Apple source states
|
|
476
|
+
this — it is empirical behavior, not published spec, so don't cite it as
|
|
477
|
+
one (see docs/ios-conformance.md and ticket 20260803-032). The mitigation
|
|
478
|
+
is harmless either way: hold text-entry controls (.juno-input covers input,
|
|
479
|
+
textarea and the select's inner control) at a 16px floor on touch. max()
|
|
480
|
+
keeps the floor even under a scaled-down --juno-font-scale, and still grows
|
|
481
|
+
when scaled up. */
|
|
449
482
|
.juno-input { font-size: max(16px, var(--juno-font-size-16)); }
|
|
483
|
+
|
|
484
|
+
/* Kill the UA tap-highlight square on the interactive surfaces so it never
|
|
485
|
+
flashes past a rounded control on tap. Consumers were adding this by hand
|
|
486
|
+
per component; make it a first-class touch default. See 20260802-020. */
|
|
487
|
+
:where(
|
|
488
|
+
.juno-btn,
|
|
489
|
+
.juno-dock__item,
|
|
490
|
+
.juno-pillbar__item,
|
|
491
|
+
.juno-tabs__tab,
|
|
492
|
+
.juno-list__item,
|
|
493
|
+
.juno-menu__item
|
|
494
|
+
) {
|
|
495
|
+
-webkit-tap-highlight-color: transparent;
|
|
496
|
+
}
|
|
450
497
|
}
|
|
451
498
|
|
|
499
|
+
/* Tappable primitives opt out of double-tap-to-zoom. A browser that still
|
|
500
|
+
recognises that gesture has to WAIT after the first tap to see whether a
|
|
501
|
+
second one is coming, which reads as a late, mushy tap on exactly the
|
|
502
|
+
surfaces a phone UI is built from. `manipulation` keeps panning and
|
|
503
|
+
pinch-zoom (so the page stays zoomable — never `none` here, that would be an
|
|
504
|
+
a11y regression) and drops only the double-tap.
|
|
505
|
+
NOT inside the pointer:coarse block above: a hybrid device (touch laptop,
|
|
506
|
+
iPad with a trackpad) reports a fine primary pointer while still taking
|
|
507
|
+
touch input, and the property is inert on a mouse anyway.
|
|
508
|
+
Community convention — no primary Apple/WebKit source names it; see
|
|
509
|
+
docs/ios-conformance.md. Named components only, so a consumer's own elements
|
|
510
|
+
are untouched. See 20260803-038. */
|
|
511
|
+
:where(
|
|
512
|
+
.juno-btn,
|
|
513
|
+
.juno-dock__item,
|
|
514
|
+
.juno-pillbar__item,
|
|
515
|
+
.juno-pillbar__overflow,
|
|
516
|
+
.juno-tabs__tab,
|
|
517
|
+
.juno-list__item,
|
|
518
|
+
.juno-menu__item,
|
|
519
|
+
.juno-seg__option,
|
|
520
|
+
.juno-chip,
|
|
521
|
+
.juno-toggle-btn
|
|
522
|
+
) {
|
|
523
|
+
touch-action: manipulation;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/* Gesture-owned surfaces — for an element whose pointer events are fully
|
|
527
|
+
driven by app JS (drag-pan, pinch-zoom, swipe classification: a state
|
|
528
|
+
machine, not a native scroller). The UA must get out of the way: no
|
|
529
|
+
scroll/zoom gesture recognition, no callout menu on long-press, no text
|
|
530
|
+
selection, no tap-highlight flash. Community convention — no primary
|
|
531
|
+
Apple/WebKit source names any of these four properties for iOS, see
|
|
532
|
+
docs/ios-conformance.md; do not attribute them to Apple. Opt-in utility
|
|
533
|
+
class (unlike the pointer:coarse reset above, which targets junoui's own
|
|
534
|
+
named components), so it isn't gated behind a pointer-type query — apply
|
|
535
|
+
it only to elements a gesture handler actually owns.
|
|
536
|
+
--juno-touch-action narrows the axis without overriding the rest of the
|
|
537
|
+
block; default `none` hands every axis to JS. */
|
|
538
|
+
.juno-gesture-surface {
|
|
539
|
+
touch-action: var(--juno-touch-action, none);
|
|
540
|
+
-webkit-touch-callout: none;
|
|
541
|
+
user-select: none;
|
|
542
|
+
-webkit-tap-highlight-color: transparent;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/* Single-axis escape hatches: the UA keeps native scrolling on one axis
|
|
546
|
+
while JS reads gestures on the other (e.g. a vertical list row that also
|
|
547
|
+
supports horizontal swipe-to-reveal). Equivalent to
|
|
548
|
+
`--juno-touch-action: pan-x|pan-y` on .juno-gesture-surface, offered as
|
|
549
|
+
standalone classes for elements that only need the axis lock and none of
|
|
550
|
+
the callout/selection/tap-highlight resets. */
|
|
551
|
+
.juno-pan-x { touch-action: pan-x; }
|
|
552
|
+
.juno-pan-y { touch-action: pan-y; }
|
|
553
|
+
|
|
452
554
|
@keyframes juno-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
|
453
555
|
|
|
556
|
+
/* Softer opacity pulse — a "working / refreshing" cadence that dims rather
|
|
557
|
+
than fully blinks out (used by the reload indicator and any live dot that
|
|
558
|
+
wants a gentler beat than juno-blink). */
|
|
559
|
+
@keyframes juno-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
|
|
560
|
+
|
|
454
561
|
@media (prefers-reduced-motion: reduce) {
|
|
562
|
+
/* Flip the JS-readable contract declared at the top of this file. Without
|
|
563
|
+
this the property would always report `auto` and script could never see
|
|
564
|
+
the preference — the whole point of exposing it. */
|
|
565
|
+
:root {
|
|
566
|
+
--juno-motion: none;
|
|
567
|
+
--juno-motion-scale: 0;
|
|
568
|
+
}
|
|
569
|
+
|
|
455
570
|
*, *::before, *::after {
|
|
456
571
|
animation-duration: 0.01ms !important;
|
|
457
572
|
animation-iteration-count: 1 !important;
|
|
@@ -477,6 +592,54 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
477
592
|
.juno-badge { forced-color-adjust: none; } /* keep status fill meaningful */
|
|
478
593
|
}
|
|
479
594
|
|
|
595
|
+
/* ── iOS standalone letterbox unlock ─────────────────────────────────────────
|
|
596
|
+
iOS WebKit (observed through 26.6, iPhone 16 Pro, iOS 18.7) sizes a
|
|
597
|
+
Home-Screen standalone window whose DOCUMENT cannot scroll as if a
|
|
598
|
+
retractable browser toolbar existed: `screen.height − status bar`, pinned to
|
|
599
|
+
the top. The reserved strip — exactly env(safe-area-inset-top) — surfaces at
|
|
600
|
+
the BOTTOM of the glass, outside the window, painting black on every screen.
|
|
601
|
+
The same install with a scrollable document gets the whole screen.
|
|
602
|
+
|
|
603
|
+
Established by measurement, not inference (nexora 20260812-006): 201 device
|
|
604
|
+
readings; a four-structure A/B on one install where doc-scroll = full screen
|
|
605
|
+
and fixed-shell = letterboxed on every cold launch; and this exact mechanism
|
|
606
|
+
— the shell kept, the document left scrollable behind it by an invisible
|
|
607
|
+
in-flow spacer — verified full-screen on the device before landing here.
|
|
608
|
+
Transient scrollability is NOT enough (seven timed interventions, five
|
|
609
|
+
controlled runs, all negative): iOS samples the resting structure.
|
|
610
|
+
|
|
611
|
+
So: keep the document scrollable behind the app. The spacer is body::after
|
|
612
|
+
(a consumer that needs body::after for itself overrides this at the same
|
|
613
|
+
gate — none of ours do), invisible, 1px wide, and taller than the LARGE
|
|
614
|
+
viewport so the document always overflows whatever window iOS grants.
|
|
615
|
+
`overscroll-behavior: none` keeps the ghost scroller from rubber-banding;
|
|
616
|
+
apps put `overscroll-behavior: contain` on their real scrollers so an
|
|
617
|
+
inner fling never chains into it.
|
|
618
|
+
|
|
619
|
+
Gate, all three required:
|
|
620
|
+
display-mode: standalone only installed apps are letterboxed
|
|
621
|
+
pointer: coarse touch devices — keeps macOS Dock apps out
|
|
622
|
+
-webkit-touch-callout support iOS/iPadOS WebKit only
|
|
623
|
+
Selectors carry `html:root` (0,1,2) on purpose: app resets commonly declare
|
|
624
|
+
`body { overflow: hidden }` at (0,0,1) AFTER this sheet, and the unlock must
|
|
625
|
+
win the cascade without !important. */
|
|
626
|
+
@media (display-mode: standalone) and (pointer: coarse) {
|
|
627
|
+
@supports (-webkit-touch-callout: none) {
|
|
628
|
+
html:root,
|
|
629
|
+
html:root body { overflow: visible; }
|
|
630
|
+
html:root body { overscroll-behavior: none; }
|
|
631
|
+
|
|
632
|
+
html:root body::after {
|
|
633
|
+
content: '';
|
|
634
|
+
display: block;
|
|
635
|
+
width: 1px;
|
|
636
|
+
height: calc(100lvh + 80px);
|
|
637
|
+
visibility: hidden;
|
|
638
|
+
pointer-events: none;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
480
643
|
/* ════════════════════════════════════════════════════════════════════
|
|
481
644
|
* junoui — layout layer (intrinsic & responsive)
|
|
482
645
|
*
|
|
@@ -561,13 +724,41 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
561
724
|
flex-basis: calc((var(--juno-switcher-threshold, var(--juno-bp-sm)) - 100%) * 999);
|
|
562
725
|
}
|
|
563
726
|
|
|
564
|
-
/*
|
|
727
|
+
/* Scroller — bare scroll-container primitive underneath every scrolling
|
|
728
|
+
region in the library (.juno-reel, .juno-app-shell__main, tab strips…).
|
|
729
|
+
Three knobs, each a custom prop with the sane-default fallback: overflow
|
|
730
|
+
axis, overscroll containment, and snap type — so a consumer opts into
|
|
731
|
+
containment/snap without re-deriving them, and opts OUT (snap: none is
|
|
732
|
+
the default here) without fighting a hardcoded value. `--x`/`--y` pick a
|
|
733
|
+
single scroll axis; `--bare` hides the scrollbar cross-browser; a
|
|
734
|
+
`.juno-snap` child opts into the snap stop once the container snaps.
|
|
735
|
+
<div class="juno-scroller juno-scroller--y">…</div>
|
|
736
|
+
<div class="juno-scroller juno-scroller--x juno-scroller--bare"
|
|
737
|
+
style="--juno-scroller-snap:x proximity">
|
|
738
|
+
<div class="juno-snap">…</div>
|
|
739
|
+
</div> */
|
|
740
|
+
.juno-scroller {
|
|
741
|
+
overflow: var(--juno-scroller-overflow, auto);
|
|
742
|
+
overscroll-behavior: var(--juno-scroller-overscroll, contain);
|
|
743
|
+
scroll-snap-type: var(--juno-scroller-snap, none);
|
|
744
|
+
}
|
|
745
|
+
.juno-scroller--x { overflow: auto hidden; }
|
|
746
|
+
.juno-scroller--y { overflow: hidden auto; }
|
|
747
|
+
.juno-scroller--bare { scrollbar-width: none; }
|
|
748
|
+
.juno-scroller--bare::-webkit-scrollbar { display: none; }
|
|
749
|
+
.juno-scroller > .juno-snap { scroll-snap-align: var(--juno-snap-align, start); }
|
|
750
|
+
|
|
751
|
+
/* Reel — horizontal scroll row (overflowing chips, timelines). A
|
|
752
|
+
.juno-scroller preset: single inline axis, mandatory snap by default —
|
|
753
|
+
override the mode per instance with --juno-scroller-snap (e.g. `inline
|
|
754
|
+
proximity` for a softer magnet, or `none` to opt out and keep only the
|
|
755
|
+
containment + layout). Default output is unchanged from before. */
|
|
565
756
|
.juno-reel {
|
|
566
757
|
display: flex;
|
|
567
758
|
gap: var(--juno-reel-space, var(--juno-space-12));
|
|
568
759
|
overflow-x: auto;
|
|
569
760
|
overscroll-behavior-inline: contain;
|
|
570
|
-
scroll-snap-type: inline mandatory;
|
|
761
|
+
scroll-snap-type: var(--juno-scroller-snap, inline mandatory);
|
|
571
762
|
}
|
|
572
763
|
.juno-reel > * { scroll-snap-align: start; flex: 0 0 auto; }
|
|
573
764
|
|
|
@@ -680,6 +871,28 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
680
871
|
--juno-gap-content: var(--juno-space-4);
|
|
681
872
|
}
|
|
682
873
|
|
|
874
|
+
/* auto — OPT-IN. `comfortable`/`compact` never change meaning; a consumer
|
|
875
|
+
* must explicitly write `data-juno-density="auto"` to get this behavior.
|
|
876
|
+
* Baseline (any pointer, any viewport ≥ 640px) is comfortable — no rule
|
|
877
|
+
* needed here, it inherits the :root defaults above by not matching
|
|
878
|
+
* `[data-juno-density='compact']`. Only a small-viewport COARSE pointer
|
|
879
|
+
* (phone, not a touch laptop) re-densifies, mirroring the precedent already
|
|
880
|
+
* set by base.css's own `@media (pointer: coarse)` tap-target rule — this is
|
|
881
|
+
* a viewport-level document concern (set on <html>/<body>), not a component
|
|
882
|
+
* in an unknown layout, so a viewport query is the right tool here (container
|
|
883
|
+
* queries can't see pointer type anyway).
|
|
884
|
+
*
|
|
885
|
+
* NEVER shrinks a control's min-height — --juno-size-tap-min stays governed
|
|
886
|
+
* solely by base.css's coarse-pointer rule (20260802-020 / WCAG 2.5.5).
|
|
887
|
+
* This block only shrinks padding/gap/tile sizing, same contract as compact. */
|
|
888
|
+
@media (pointer: coarse) and (width <= 639.98px) {
|
|
889
|
+
[data-juno-density='auto'] {
|
|
890
|
+
--juno-tile-min: 104px;
|
|
891
|
+
--juno-gap-content: var(--juno-space-2);
|
|
892
|
+
--juno-pad-surface-inline: var(--juno-space-12);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
683
896
|
/* ════════════════════════════════════════════════════════════════════
|
|
684
897
|
* junoui — type-scale layer
|
|
685
898
|
* One multiplier, --juno-font-scale, scales every --juno-font-size-*
|
|
@@ -1470,6 +1683,14 @@ button.juno-chip {
|
|
|
1470
1683
|
.juno-dock {
|
|
1471
1684
|
--juno-role: var(--juno-active);
|
|
1472
1685
|
|
|
1686
|
+
/* Consumer-driven shrink (e.g. dim/compact the bar on scroll) — default 1,
|
|
1687
|
+
a no-op. Applies to the whole bar regardless of variant (plain/--fixed/
|
|
1688
|
+
--pill/--float/--icon). Transition duration is authored through the
|
|
1689
|
+
motion scale (see base.css's JS-readable --juno-motion contract) so
|
|
1690
|
+
prefers-reduced-motion collapses it to 0 without a component-local
|
|
1691
|
+
media query. */
|
|
1692
|
+
--juno-dock-scale: 1;
|
|
1693
|
+
|
|
1473
1694
|
position: sticky;
|
|
1474
1695
|
inset-block-end: 0;
|
|
1475
1696
|
z-index: var(--juno-z-raised);
|
|
@@ -1478,6 +1699,9 @@ button.juno-chip {
|
|
|
1478
1699
|
background: var(--juno-s1);
|
|
1479
1700
|
border-block-start: var(--juno-border-width-1) solid var(--juno-border);
|
|
1480
1701
|
padding-block-end: env(safe-area-inset-bottom, 0);
|
|
1702
|
+
transform: scale(var(--juno-dock-scale));
|
|
1703
|
+
transform-origin: bottom center;
|
|
1704
|
+
transition: transform calc(var(--juno-motion-duration-quick) * var(--juno-motion-scale)) var(--juno-motion-ease-standard);
|
|
1481
1705
|
}
|
|
1482
1706
|
|
|
1483
1707
|
.juno-dock__item {
|
|
@@ -1490,6 +1714,13 @@ button.juno-chip {
|
|
|
1490
1714
|
justify-content: center;
|
|
1491
1715
|
gap: var(--juno-space-2);
|
|
1492
1716
|
padding-block: var(--juno-space-4);
|
|
1717
|
+
|
|
1718
|
+
/* Full reset, not just the top edge: a plain `border-block-start` override
|
|
1719
|
+
leaves the UA default border on the other three sides of a <button>
|
|
1720
|
+
item (links have none, so this went unnoticed until <button> showed up
|
|
1721
|
+
in the pill variant's usage). Re-add only the edge that carries the
|
|
1722
|
+
active accent. */
|
|
1723
|
+
border: none;
|
|
1493
1724
|
border-block-start: var(--juno-border-width-2) solid transparent;
|
|
1494
1725
|
font-family: var(--juno-font-family-sans);
|
|
1495
1726
|
font-size: var(--juno-font-size-10);
|
|
@@ -1535,6 +1766,310 @@ button.juno-chip {
|
|
|
1535
1766
|
inset-block-end: 0;
|
|
1536
1767
|
}
|
|
1537
1768
|
|
|
1769
|
+
/* ── Pill variant — the full-width bar becomes a rounded floating pill with
|
|
1770
|
+
big glyphs in circular bubbles. Labels drop; each icon sits in a circular
|
|
1771
|
+
.juno-dock__bubble that fills only on the active tab (not the whole item).
|
|
1772
|
+
Out of flow (fixed) so the page scrolls UNDER it — reserve room on the
|
|
1773
|
+
scroller with padding-block-end: var(--juno-dock-clearance).
|
|
1774
|
+
Usage — pair the bubble with .juno-icon-loader, junoui's concentric-stack
|
|
1775
|
+
primitive, so a loading arc can be dropped in without re-rolling geometry:
|
|
1776
|
+
<nav class="juno-dock juno-dock--pill juno-hide-from-md" aria-label="Primary">
|
|
1777
|
+
<a class="juno-dock__item" href="…" aria-current="page" aria-label="Library">
|
|
1778
|
+
<span class="juno-dock__bubble juno-icon-loader">
|
|
1779
|
+
<svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-squares-four" /></svg>
|
|
1780
|
+
</span>
|
|
1781
|
+
</a>
|
|
1782
|
+
</nav>
|
|
1783
|
+
Labels are hidden, so every item MUST carry an aria-label (on the <a>/<button>)
|
|
1784
|
+
— the glyph alone is not an accessible name. */
|
|
1785
|
+
.juno-dock--pill {
|
|
1786
|
+
--juno-role: var(--juno-active);
|
|
1787
|
+
|
|
1788
|
+
position: fixed;
|
|
1789
|
+
inset-inline: 0;
|
|
1790
|
+
inset-block-end: 0;
|
|
1791
|
+
z-index: var(--juno-z-raised);
|
|
1792
|
+
margin: 0 var(--juno-space-12) calc(var(--juno-space-16) + env(safe-area-inset-bottom, 0px));
|
|
1793
|
+
padding: var(--juno-space-4);
|
|
1794
|
+
background: color-mix(in srgb, var(--juno-s1) 88%, transparent);
|
|
1795
|
+
-webkit-backdrop-filter: blur(12px);
|
|
1796
|
+
backdrop-filter: blur(12px);
|
|
1797
|
+
border: var(--juno-border-width-1) solid var(--juno-border);
|
|
1798
|
+
border-radius: 999px;
|
|
1799
|
+
box-shadow: var(--juno-shadow-2);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
.juno-dock--pill .juno-dock__item {
|
|
1803
|
+
min-block-size: 0;
|
|
1804
|
+
gap: 0;
|
|
1805
|
+
padding-block: var(--juno-space-4);
|
|
1806
|
+
|
|
1807
|
+
/* full reset: the base only zeroes the block-start border, but the pill has
|
|
1808
|
+
no top edge to accent — the active state moves to the bubble. Also kill the
|
|
1809
|
+
UA tap-highlight so no square flashes past the round bubble on tap. */
|
|
1810
|
+
border: none;
|
|
1811
|
+
background: none;
|
|
1812
|
+
-webkit-tap-highlight-color: transparent;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
.juno-dock--pill .juno-dock__label { display: none; }
|
|
1816
|
+
|
|
1817
|
+
/* Focus lands on the bubble so the ring hugs the round target, not the wide
|
|
1818
|
+
item box. */
|
|
1819
|
+
.juno-dock--pill .juno-dock__item:focus-visible { outline: none; }
|
|
1820
|
+
|
|
1821
|
+
.juno-dock--pill .juno-dock__item:focus-visible .juno-dock__bubble {
|
|
1822
|
+
outline: var(--juno-border-width-2) solid var(--juno-active);
|
|
1823
|
+
outline-offset: var(--juno-space-2);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/* The bubble is a definite-size circular surface that centres its glyph. It
|
|
1827
|
+
declares the same single-cell grid as .juno-icon-loader so composing the
|
|
1828
|
+
two (see the usage block above) can't fight over `display`, and so the
|
|
1829
|
+
ring — when one is present — is concentric with the glyph. */
|
|
1830
|
+
.juno-dock__bubble {
|
|
1831
|
+
/* Section-loading ring — with .juno-icon-loader on the same element, an
|
|
1832
|
+
indeterminate .juno-arc dropped inside rings the bubble's EDGE (diameter
|
|
1833
|
+
= the bubble, thin stroke) instead of the glyph. Because the bubble's box
|
|
1834
|
+
is definite, adding or removing the arc never resizes it. */
|
|
1835
|
+
--juno-icon-loader-ring: var(--juno-size-tap-comfortable);
|
|
1836
|
+
--juno-icon-loader-ring-width: 2px;
|
|
1837
|
+
|
|
1838
|
+
display: grid;
|
|
1839
|
+
place-items: center;
|
|
1840
|
+
inline-size: var(--juno-size-tap-comfortable);
|
|
1841
|
+
block-size: var(--juno-size-tap-comfortable);
|
|
1842
|
+
border-radius: 50%;
|
|
1843
|
+
transition: background-color var(--juno-motion-duration-quick) var(--juno-motion-ease-standard);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/* Explicit big glyph so the pill reads at a glance. Set on the glyph itself so
|
|
1847
|
+
it wins regardless of context (the base .juno-icon no longer pins a size —
|
|
1848
|
+
see 20260727-011). */
|
|
1849
|
+
.juno-dock--pill .juno-icon { --juno-icon-size: var(--juno-space-32); }
|
|
1850
|
+
|
|
1851
|
+
/* Active reads on the bubble alone — suppress the base full-item fill so only
|
|
1852
|
+
the circular highlight shows. */
|
|
1853
|
+
.juno-dock--pill .juno-dock__item[aria-current] {
|
|
1854
|
+
background: none;
|
|
1855
|
+
color: var(--juno-role);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
.juno-dock--pill .juno-dock__item[aria-current] .juno-dock__bubble {
|
|
1859
|
+
background: var(--juno-s3);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/* reduced-transparency → solid surface, no blur (matches the pillbar) */
|
|
1863
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
1864
|
+
.juno-dock--pill {
|
|
1865
|
+
background: var(--juno-s1);
|
|
1866
|
+
-webkit-backdrop-filter: none;
|
|
1867
|
+
backdrop-filter: none;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
/* ── Float variant — the pillbar's floating capsule chrome (fixed, rounded
|
|
1872
|
+
shell, blur, shadow, off-edge margin) applied to the dock's OWN item model:
|
|
1873
|
+
unlike --pill it keeps icon+label items and the full-item active fill —
|
|
1874
|
+
only the exterior changes. Combine with --icon below for the bubble
|
|
1875
|
+
treatment inside a capsule (the old --pill look, decomposed). `border`
|
|
1876
|
+
fully replaces the base's `border-block-start` (no top seam on a capsule),
|
|
1877
|
+
and `padding` fully replaces the base's safe-area padding (the margin
|
|
1878
|
+
below carries the safe area instead — additive offset off the edge, not
|
|
1879
|
+
`max()`: see docs/ios-conformance.md). `overflow: hidden` clips the
|
|
1880
|
+
items' square corners to the capsule's rounded shape.
|
|
1881
|
+
Usage:
|
|
1882
|
+
<nav class="juno-dock juno-dock--float juno-hide-from-md" aria-label="Primary">
|
|
1883
|
+
<a class="juno-dock__item" href="…" aria-current="page">
|
|
1884
|
+
<svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-squares-four" /></svg>
|
|
1885
|
+
<span class="juno-dock__label">Library</span>
|
|
1886
|
+
</a>
|
|
1887
|
+
</nav>
|
|
1888
|
+
Out of flow (fixed) so the page scrolls UNDER it — reserve room on the
|
|
1889
|
+
scroller with padding-block-end: var(--juno-dock-clearance). */
|
|
1890
|
+
.juno-dock--float {
|
|
1891
|
+
position: fixed;
|
|
1892
|
+
inset-inline: 0;
|
|
1893
|
+
inset-block-end: 0;
|
|
1894
|
+
z-index: var(--juno-z-raised);
|
|
1895
|
+
margin: 0 var(--juno-space-12) calc(var(--juno-space-16) + env(safe-area-inset-bottom, 0px));
|
|
1896
|
+
padding: var(--juno-space-4);
|
|
1897
|
+
overflow: hidden;
|
|
1898
|
+
background: color-mix(in srgb, var(--juno-s1) 88%, transparent);
|
|
1899
|
+
-webkit-backdrop-filter: blur(12px);
|
|
1900
|
+
backdrop-filter: blur(12px);
|
|
1901
|
+
border: var(--juno-border-width-1) solid var(--juno-border);
|
|
1902
|
+
border-radius: 999px;
|
|
1903
|
+
box-shadow: var(--juno-shadow-2);
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
/* reduced-transparency → solid surface, no blur (matches the pillbar / --pill) */
|
|
1907
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
1908
|
+
.juno-dock--float {
|
|
1909
|
+
background: var(--juno-s1);
|
|
1910
|
+
-webkit-backdrop-filter: none;
|
|
1911
|
+
backdrop-filter: none;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
/* ── Icon variant — labels drop, glyphs grow, and the active state moves to
|
|
1916
|
+
a circular .juno-dock__bubble instead of filling the whole item, WITHOUT
|
|
1917
|
+
forcing the floating capsule chrome (that's --float, above — combine both
|
|
1918
|
+
for the old --pill look). Usable standalone on a plain sticky/--fixed bar
|
|
1919
|
+
for a compact, icon-only full-width dock.
|
|
1920
|
+
Usage — same .juno-dock__bubble contract as --pill (pair with
|
|
1921
|
+
.juno-icon-loader; see docs/components/dock.md#pill-variant):
|
|
1922
|
+
<nav class="juno-dock juno-dock--icon juno-hide-from-md" aria-label="Primary">
|
|
1923
|
+
<a class="juno-dock__item" href="…" aria-current="page" aria-label="Library">
|
|
1924
|
+
<span class="juno-dock__bubble juno-icon-loader">
|
|
1925
|
+
<svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-squares-four" /></svg>
|
|
1926
|
+
</span>
|
|
1927
|
+
</a>
|
|
1928
|
+
</nav>
|
|
1929
|
+
Labels are hidden, so every item MUST carry an aria-label — the glyph
|
|
1930
|
+
alone is not an accessible name. */
|
|
1931
|
+
.juno-dock--icon .juno-dock__item {
|
|
1932
|
+
gap: 0;
|
|
1933
|
+
|
|
1934
|
+
/* accent moves to the bubble; kill the item-edge border in both states */
|
|
1935
|
+
border-color: transparent;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
.juno-dock--icon .juno-dock__label { display: none; }
|
|
1939
|
+
|
|
1940
|
+
/* Explicit big glyph so the icon-only bar reads at a glance — set on the
|
|
1941
|
+
glyph itself so it wins regardless of context (the base .juno-icon no
|
|
1942
|
+
longer pins a size — see 20260727-011). */
|
|
1943
|
+
.juno-dock--icon .juno-icon { --juno-icon-size: var(--juno-space-32); }
|
|
1944
|
+
|
|
1945
|
+
/* Focus lands on the bubble so the ring hugs the round target, not the wide
|
|
1946
|
+
item box. */
|
|
1947
|
+
.juno-dock--icon .juno-dock__item:focus-visible { outline: none; }
|
|
1948
|
+
|
|
1949
|
+
.juno-dock--icon .juno-dock__item:focus-visible .juno-dock__bubble {
|
|
1950
|
+
outline: var(--juno-border-width-2) solid var(--juno-active);
|
|
1951
|
+
outline-offset: var(--juno-space-2);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
/* Active reads on the bubble alone — suppress the base full-item fill so
|
|
1955
|
+
only the circular highlight shows. */
|
|
1956
|
+
.juno-dock--icon .juno-dock__item[aria-current] {
|
|
1957
|
+
background: none;
|
|
1958
|
+
color: var(--juno-role);
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
.juno-dock--icon .juno-dock__item[aria-current] .juno-dock__bubble {
|
|
1962
|
+
background: var(--juno-s3);
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
/* ── Collapsible variant — the whole bar folds into a single circular
|
|
1966
|
+
.juno-dock__knob at the inline-end edge, and unfolds back to full width.
|
|
1967
|
+
Driven by ONE inherited custom prop the app writes (junoui ships no JS):
|
|
1968
|
+
--juno-dock-fold, 0 = fully open … 1 = the circle. Write it per scroll
|
|
1969
|
+
frame for a gesture-tracked fold (set it on the dock, an ancestor, or
|
|
1970
|
+
:root — it is only CONSUMED here, never declared, so an inherited value
|
|
1971
|
+
always lands), or flip it 0/1 for a plain toggle.
|
|
1972
|
+
|
|
1973
|
+
The fold has two phases, split at --juno-dock-fold-split: first the bar
|
|
1974
|
+
SHRINKS in place to --juno-dock-fold-scale (transform, no relayout),
|
|
1975
|
+
then it SLIDES shut to --juno-dock-collapsed-size. Width interpolates
|
|
1976
|
+
between two definite lengths because a transition cannot run to or from
|
|
1977
|
+
an intrinsic size; --juno-dock-fold-smoothing is smoothing over
|
|
1978
|
+
scroll-event discretization, not the fold itself — anything longer lags
|
|
1979
|
+
the finger driving it.
|
|
1980
|
+
|
|
1981
|
+
Compose with a fixed placement (--pill, --float, or --fixed): the fold
|
|
1982
|
+
re-anchors the bar to the inline-end edge, which only means something
|
|
1983
|
+
out of flow. Transform-origin is the physical bottom right — the fold's
|
|
1984
|
+
own scale REPLACES the base --juno-dock-scale hook (one transform slot);
|
|
1985
|
+
RTL consumers flip the origin and inset overrides together.
|
|
1986
|
+
|
|
1987
|
+
Structure — items live in a tray; the knob is the circle's face:
|
|
1988
|
+
<nav class="juno-dock juno-dock--pill juno-dock--collapsible …"
|
|
1989
|
+
data-juno-collapsed>
|
|
1990
|
+
<div class="juno-dock__tray"> …the usual __item children… </div>
|
|
1991
|
+
<button class="juno-dock__knob" aria-label="Show navigation">
|
|
1992
|
+
<svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-dots-three" /></svg>
|
|
1993
|
+
</button>
|
|
1994
|
+
</nav>
|
|
1995
|
+
|
|
1996
|
+
data-juno-collapsed is the END state, set by the app only at fold = 1
|
|
1997
|
+
(and removed the moment the fold reopens): it flips the tray to
|
|
1998
|
+
visibility:hidden — which removes its items from the tab order — and
|
|
1999
|
+
reveals the knob. Mid-fold both stay live; the tray's opacity empties
|
|
2000
|
+
ahead of the slide (×1.6) so the glyphs are gone before the circle
|
|
2001
|
+
closes. The knob MUST carry an aria-label; collapsing while focus is
|
|
2002
|
+
inside the tray is the app's edge — move focus to the knob first.
|
|
2003
|
+
prefers-reduced-motion needs nothing component-local: the base layer
|
|
2004
|
+
zeroes every transition, and the fold value itself is app-driven. */
|
|
2005
|
+
.juno-dock--collapsible {
|
|
2006
|
+
--juno-dock-fold-split: 0.35;
|
|
2007
|
+
--juno-dock-fold-scale: 0.78;
|
|
2008
|
+
--juno-dock-collapsed-size: calc(
|
|
2009
|
+
var(--juno-size-tap-comfortable) + 2 * var(--juno-space-4) + 2 * var(--juno-border-width-1)
|
|
2010
|
+
);
|
|
2011
|
+
--juno-dock-edge-gap: calc(2 * var(--juno-space-12));
|
|
2012
|
+
--juno-dock-fold-smoothing: 90ms;
|
|
2013
|
+
|
|
2014
|
+
/* per-phase normalized progress, derived — never set these directly */
|
|
2015
|
+
--juno-dock-fold-shrink: min(1, var(--juno-dock-fold, 0) / var(--juno-dock-fold-split));
|
|
2016
|
+
--juno-dock-fold-slide: clamp(
|
|
2017
|
+
0,
|
|
2018
|
+
(var(--juno-dock-fold, 0) - var(--juno-dock-fold-split)) / (1 - var(--juno-dock-fold-split)),
|
|
2019
|
+
1
|
|
2020
|
+
);
|
|
2021
|
+
|
|
2022
|
+
inset-inline-start: auto;
|
|
2023
|
+
inline-size: calc(
|
|
2024
|
+
var(--juno-dock-collapsed-size) +
|
|
2025
|
+
(100% - var(--juno-dock-edge-gap) - var(--juno-dock-collapsed-size)) *
|
|
2026
|
+
(1 - var(--juno-dock-fold-slide))
|
|
2027
|
+
);
|
|
2028
|
+
overflow: hidden;
|
|
2029
|
+
transform: scale(calc(1 - (1 - var(--juno-dock-fold-scale)) * var(--juno-dock-fold-shrink)));
|
|
2030
|
+
transform-origin: bottom right;
|
|
2031
|
+
transition:
|
|
2032
|
+
transform var(--juno-dock-fold-smoothing) linear,
|
|
2033
|
+
inline-size var(--juno-dock-fold-smoothing) linear;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
.juno-dock__tray {
|
|
2037
|
+
flex: 1;
|
|
2038
|
+
min-inline-size: 0;
|
|
2039
|
+
display: flex;
|
|
2040
|
+
align-items: stretch;
|
|
2041
|
+
opacity: calc(1 - var(--juno-dock-fold-slide, 0) * 1.6);
|
|
2042
|
+
transition: opacity var(--juno-dock-fold-smoothing, 90ms) linear;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
.juno-dock__knob {
|
|
2046
|
+
position: absolute;
|
|
2047
|
+
inset-block: 0;
|
|
2048
|
+
inset-inline-end: var(--juno-space-4);
|
|
2049
|
+
margin-block: auto;
|
|
2050
|
+
inline-size: var(--juno-size-tap-comfortable);
|
|
2051
|
+
block-size: var(--juno-size-tap-comfortable);
|
|
2052
|
+
display: grid;
|
|
2053
|
+
place-items: center;
|
|
2054
|
+
padding: 0;
|
|
2055
|
+
border: none;
|
|
2056
|
+
border-radius: 50%;
|
|
2057
|
+
background: none;
|
|
2058
|
+
color: var(--juno-label);
|
|
2059
|
+
cursor: pointer;
|
|
2060
|
+
-webkit-tap-highlight-color: transparent;
|
|
2061
|
+
opacity: 0;
|
|
2062
|
+
visibility: hidden;
|
|
2063
|
+
transition: opacity var(--juno-motion-duration-quick) var(--juno-motion-ease-standard);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
.juno-dock--collapsible[data-juno-collapsed] .juno-dock__tray { visibility: hidden; }
|
|
2067
|
+
|
|
2068
|
+
.juno-dock--collapsible[data-juno-collapsed] .juno-dock__knob {
|
|
2069
|
+
opacity: 1;
|
|
2070
|
+
visibility: visible;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
1538
2073
|
/* ════════════════════════════════════════════════════════════════════
|
|
1539
2074
|
* Component — Status dot + label
|
|
1540
2075
|
* Usage:
|
|
@@ -1583,6 +2118,12 @@ button.juno-chip {
|
|
|
1583
2118
|
* Reuses .juno-modal for the surface + scrim; this file re-pins + re-slides.
|
|
1584
2119
|
* Selectors are compound (.juno-modal.juno-drawer) so they override the
|
|
1585
2120
|
* modal's centered transform regardless of bundle order.
|
|
2121
|
+
*
|
|
2122
|
+
* --bottom is a real bottom sheet: a grab handle (.juno-sheet__handle,
|
|
2123
|
+
* decorative) plus a CSS height knob (--juno-sheet-h, default 60dvh —
|
|
2124
|
+
* apps swap it for peek/half/full snap points, capped by --juno-sheet-max).
|
|
2125
|
+
* No drag gesture ships here: resizing/dismissing by drag is stateful and
|
|
2126
|
+
* stays in the app. See docs/components/drawer.md and 20260802-019.
|
|
1586
2127
|
* ════════════════════════════════════════════════════════════════════ */
|
|
1587
2128
|
|
|
1588
2129
|
.juno-modal.juno-drawer {
|
|
@@ -1628,15 +2169,24 @@ button.juno-chip {
|
|
|
1628
2169
|
.juno-modal.juno-drawer--bottom {
|
|
1629
2170
|
inline-size: 100%;
|
|
1630
2171
|
max-inline-size: 100%;
|
|
1631
|
-
|
|
2172
|
+
|
|
2173
|
+
/* height knob: swap --juno-sheet-h for snap points (peek/half/full).
|
|
2174
|
+
--juno-sheet-max is the hard ceiling so a swapped value can't run past
|
|
2175
|
+
it. CSS-only — the drag gesture a real snap-point picker needs is
|
|
2176
|
+
stateful and stays in the app. */
|
|
2177
|
+
block-size: var(--juno-sheet-h, 60dvh);
|
|
2178
|
+
max-block-size: var(--juno-sheet-max, 92dvh);
|
|
1632
2179
|
margin-block: auto 0;
|
|
1633
2180
|
margin-inline: 0;
|
|
1634
2181
|
border: none;
|
|
1635
2182
|
border-block-start: var(--juno-border-width-1) solid var(--juno-border);
|
|
2183
|
+
border-start-start-radius: var(--juno-radius-8);
|
|
2184
|
+
border-start-end-radius: var(--juno-radius-8);
|
|
1636
2185
|
transform: translateY(100%);
|
|
1637
2186
|
|
|
1638
|
-
/*
|
|
1639
|
-
|
|
2187
|
+
/* safe area moves to .juno-modal__body below (the scroll port) — padding
|
|
2188
|
+
here would just be dead space under the grab handle. See 20260802-019. */
|
|
2189
|
+
padding-block-end: 0;
|
|
1640
2190
|
}
|
|
1641
2191
|
|
|
1642
2192
|
.juno-modal.juno-drawer--bottom[open] { transform: translateY(0); }
|
|
@@ -1645,6 +2195,28 @@ button.juno-chip {
|
|
|
1645
2195
|
.juno-modal.juno-drawer--bottom[open] { transform: translateY(100%); }
|
|
1646
2196
|
}
|
|
1647
2197
|
|
|
2198
|
+
/* grab handle — visual affordance only. aria-hidden in markup: the dialog's
|
|
2199
|
+
own aria-labelledby (→ the title) stays the accessible name. Dragging it
|
|
2200
|
+
to resize/dismiss is stateful, so junoui draws it but never wires a
|
|
2201
|
+
gesture to it — see docs/accessibility.md. */
|
|
2202
|
+
.juno-sheet__handle {
|
|
2203
|
+
inline-size: var(--juno-space-40);
|
|
2204
|
+
block-size: var(--juno-space-4);
|
|
2205
|
+
margin: var(--juno-space-8) auto;
|
|
2206
|
+
border-radius: 999px;
|
|
2207
|
+
background: var(--juno-border-strong);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
/* the scrolling body owns the safe area now, mirroring modal.css's mobile
|
|
2211
|
+
sheet. Compound selector for specificity: drawer.css sorts before
|
|
2212
|
+
modal.css in the bundle (scripts/bundle-css.mjs), so a bare
|
|
2213
|
+
.juno-modal__body override here would lose to modal.css's own rule at
|
|
2214
|
+
equal specificity. env() fallback keeps its unit inside calc() — see
|
|
2215
|
+
docs/ios-conformance.md. */
|
|
2216
|
+
.juno-modal.juno-drawer--bottom .juno-modal__body {
|
|
2217
|
+
padding-block-end: calc(var(--juno-pad-surface-inline) + env(safe-area-inset-bottom, 0px));
|
|
2218
|
+
}
|
|
2219
|
+
|
|
1648
2220
|
/* ── narrow viewports ────────────────────────────────────────────────────
|
|
1649
2221
|
Side drawers cap below the full width so a sliver of scrim survives —
|
|
1650
2222
|
the visual cue (and tap target) that "outside" closes the panel. */
|
|
@@ -1690,6 +2262,51 @@ button.juno-chip {
|
|
|
1690
2262
|
|
|
1691
2263
|
.juno-field__error { font-size: var(--juno-font-size-11); color: var(--juno-warning); }
|
|
1692
2264
|
|
|
2265
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
2266
|
+
* Component — Fold slot (animated presence for a row member)
|
|
2267
|
+
* A slot inside any flex/grid row that animates between PRESENT and
|
|
2268
|
+
* FOLDED-AWAY instead of popping in and out of the DOM: its inline-size
|
|
2269
|
+
* folds to zero, it fades, and at the end of the fold it leaves the
|
|
2270
|
+
* tab order via visibility (discrete transition: instant coming in,
|
|
2271
|
+
* end-of-fade going out). Keep the element mounted; drive the state
|
|
2272
|
+
* with the data-juno-in attribute — zero JS here, the app owns when.
|
|
2273
|
+
*
|
|
2274
|
+
* The size must be DEFINITE (--juno-fold-size) because a transition
|
|
2275
|
+
* cannot run to or from an intrinsic width. If the row uses a flex/grid
|
|
2276
|
+
* gap, the folded slot still occupies one gap — name it in
|
|
2277
|
+
* --juno-fold-gap and the fold swallows it with a negative margin.
|
|
2278
|
+
*
|
|
2279
|
+
* Canonical use: a transient action in a toolbar/pill row (a
|
|
2280
|
+
* scroll-to-top arrow, a contextual button) whose arrival should slide
|
|
2281
|
+
* the row open rather than jump it.
|
|
2282
|
+
* Usage:
|
|
2283
|
+
* <div style="display:flex; gap:4px"> (any row)
|
|
2284
|
+
* <button class="juno-fold" data-juno-in aria-label="Scroll to top">…</button>
|
|
2285
|
+
* …other members…
|
|
2286
|
+
* </div>
|
|
2287
|
+
* prefers-reduced-motion needs nothing component-local — the base
|
|
2288
|
+
* layer zeroes every transition duration. */
|
|
2289
|
+
.juno-fold {
|
|
2290
|
+
--juno-fold-size: var(--juno-size-tap-comfortable);
|
|
2291
|
+
--juno-fold-gap: 0px;
|
|
2292
|
+
|
|
2293
|
+
flex-shrink: 0;
|
|
2294
|
+
inline-size: var(--juno-fold-size);
|
|
2295
|
+
overflow: hidden;
|
|
2296
|
+
transition:
|
|
2297
|
+
inline-size var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
|
|
2298
|
+
margin var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
|
|
2299
|
+
opacity var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
|
|
2300
|
+
visibility var(--juno-motion-duration-base) var(--juno-motion-ease-standard);
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
.juno-fold:not([data-juno-in]) {
|
|
2304
|
+
inline-size: 0;
|
|
2305
|
+
margin-inline-end: calc(-1 * var(--juno-fold-gap));
|
|
2306
|
+
opacity: 0;
|
|
2307
|
+
visibility: hidden;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
1693
2310
|
/* ════════════════════════════════════════════════════════════════════
|
|
1694
2311
|
* Component — Gauge (determinate metric ring)
|
|
1695
2312
|
* A fixed-value ring for readouts (CPU / RAM / DISK …) — the metric
|
|
@@ -1770,11 +2387,26 @@ button.juno-chip {
|
|
|
1770
2387
|
.juno-gauge--lg .juno-gauge__value { font-size: var(--juno-font-size-20); }
|
|
1771
2388
|
|
|
1772
2389
|
/* ════════════════════════════════════════════════════════════════════
|
|
1773
|
-
* Component — Icon loader (
|
|
1774
|
-
* A
|
|
1775
|
-
*
|
|
1776
|
-
*
|
|
1777
|
-
*
|
|
2390
|
+
* Component — Icon loader (the ring-a-control primitive)
|
|
2391
|
+
* A control ringed by the spinning arc while its section loads — the
|
|
2392
|
+
* "this is loading" affordance for a rail / dock / pillbar item, but also
|
|
2393
|
+
* any other control an app needs to ring: an icon button, a badge, an
|
|
2394
|
+
* avatar. What it wraps stays static on top; the arc rings it and eats no
|
|
2395
|
+
* pointer events, so the wrapped control still clicks through.
|
|
2396
|
+
*
|
|
2397
|
+
* This is the ONLY concentric-ring mechanism in junoui: anything that
|
|
2398
|
+
* wants an arc around it — a nav glyph, a dock bubble, an arbitrary
|
|
2399
|
+
* button or badge — composes this class instead of re-rolling absolute
|
|
2400
|
+
* positioning. Both ring dimensions are custom props, so one wrapper
|
|
2401
|
+
* serves a 14px glyph and a 44px bubble:
|
|
2402
|
+
* --juno-icon-loader-ring diameter (default 1.9em — em, so it
|
|
2403
|
+
* tracks the glyph it wraps)
|
|
2404
|
+
* --juno-icon-loader-ring-width stroke (default 0.14em)
|
|
2405
|
+
* A host with a definite box (a dock bubble, an icon button) sets the
|
|
2406
|
+
* diameter to that box's size, so the ring hugs the edge and the box does
|
|
2407
|
+
* not resize when the arc appears or leaves — ring it without changing
|
|
2408
|
+
* its box. Below ~24px, add .juno-arc--smooth (loader.css) alongside
|
|
2409
|
+
* --indeterminate: the default 12-step sweep reads as jitter that small.
|
|
1778
2410
|
*
|
|
1779
2411
|
* The app owns the state: add .juno-arc--indeterminate to spin, drop it
|
|
1780
2412
|
* when loaded (gate the blink to first-load, not every refetch). For a
|
|
@@ -1789,37 +2421,36 @@ button.juno-chip {
|
|
|
1789
2421
|
* </a>
|
|
1790
2422
|
*
|
|
1791
2423
|
* Gotcha encoded here: the arc's rotate animation overrides `transform`, so
|
|
1792
|
-
* the ring is centred with
|
|
2424
|
+
* the ring is centred with a single-cell grid — never translate(-50%,-50%),
|
|
1793
2425
|
* which the spin would clobber.
|
|
1794
2426
|
* ════════════════════════════════════════════════════════════════════ */
|
|
1795
2427
|
|
|
1796
2428
|
.juno-icon-loader {
|
|
1797
2429
|
--juno-role: var(--juno-active);
|
|
1798
2430
|
|
|
1799
|
-
/* Single-cell grid:
|
|
1800
|
-
|
|
1801
|
-
arc's rotation would clobber a translate) and no fragile
|
|
1802
|
-
|
|
2431
|
+
/* Single-cell grid: every child occupies the SAME cell and is centred in
|
|
2432
|
+
it, so the ring and what it rings are guaranteed concentric — no
|
|
2433
|
+
transform (the arc's rotation would clobber a translate) and no fragile
|
|
2434
|
+
margin math. With no explicit size the cell sizes to its largest child
|
|
2435
|
+
(normally the ring, --juno-icon-loader-ring); give the host a definite
|
|
2436
|
+
inline/block size to pin the cell and keep the box jitter-free. */
|
|
1803
2437
|
display: inline-grid;
|
|
1804
2438
|
place-items: center;
|
|
1805
2439
|
line-height: 0; /* drop the inline strut so the box hugs the ring */
|
|
1806
2440
|
}
|
|
1807
2441
|
|
|
1808
|
-
|
|
1809
|
-
.juno-icon-loader >
|
|
2442
|
+
/* every child shares the one cell; the ringed content paints above the ring */
|
|
2443
|
+
.juno-icon-loader > * {
|
|
1810
2444
|
grid-area: 1 / 1;
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
/* icon sits above the ring */
|
|
1814
|
-
.juno-icon-loader > .juno-icon {
|
|
1815
2445
|
z-index: 1;
|
|
1816
2446
|
}
|
|
1817
2447
|
|
|
1818
|
-
/* the ring: sized
|
|
2448
|
+
/* the ring: sized from the two custom props, transparent to clicks */
|
|
1819
2449
|
.juno-icon-loader > .juno-arc {
|
|
1820
2450
|
--juno-arc-size: var(--juno-icon-loader-ring, 1.9em);
|
|
1821
|
-
--juno-arc-width: 0.14em;
|
|
2451
|
+
--juno-arc-width: var(--juno-icon-loader-ring-width, 0.14em);
|
|
1822
2452
|
|
|
2453
|
+
z-index: 0;
|
|
1823
2454
|
pointer-events: none;
|
|
1824
2455
|
}
|
|
1825
2456
|
|
|
@@ -1841,11 +2472,13 @@ button.juno-chip {
|
|
|
1841
2472
|
* ════════════════════════════════════════════════════════════════════ */
|
|
1842
2473
|
|
|
1843
2474
|
.juno-icon {
|
|
1844
|
-
|
|
1845
|
-
|
|
2475
|
+
/* Default lives in the var() fallback, NOT as an element-level declaration:
|
|
2476
|
+
declaring --juno-icon-size on the element would shadow an ancestor that
|
|
2477
|
+
sets it for contextual sizing (e.g. .juno-list__chevron). Explicit
|
|
2478
|
+
modifiers below still set it on the element and win. See 20260727-011. */
|
|
1846
2479
|
display: inline-block;
|
|
1847
|
-
inline-size: var(--juno-icon-size);
|
|
1848
|
-
block-size: var(--juno-icon-size);
|
|
2480
|
+
inline-size: var(--juno-icon-size, 1.25em);
|
|
2481
|
+
block-size: var(--juno-icon-size, 1.25em);
|
|
1849
2482
|
flex-shrink: 0;
|
|
1850
2483
|
vertical-align: -0.2em; /* optical baseline alignment next to text */
|
|
1851
2484
|
fill: currentcolor;
|
|
@@ -2056,6 +2689,143 @@ button.juno-list__row:disabled {
|
|
|
2056
2689
|
color: var(--juno-muted);
|
|
2057
2690
|
}
|
|
2058
2691
|
|
|
2692
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
2693
|
+
* Component — Load state · a vocabulary so nothing spins forever
|
|
2694
|
+
* Names the treatment for every way a load can go, so no consumer reaches
|
|
2695
|
+
* for a spinner on a state that will never resolve:
|
|
2696
|
+
* resolving, ETA known → .juno-arc / .juno-beacon / .juno-bar (loader.css)
|
|
2697
|
+
* layout known, no data → .juno-skeleton (skeleton.css)
|
|
2698
|
+
* working, NO ETA → .juno-shimmer — motion without a completion promise
|
|
2699
|
+
* failed (terminal) → .juno-fault — static, never animates
|
|
2700
|
+
* legitimately none → .juno-empty — static, never animates
|
|
2701
|
+
* Full decision table + a11y contract: docs/components/load-state.md.
|
|
2702
|
+
*
|
|
2703
|
+
* Also ships an optional CSS-only switch: mark each treatment's wrapper
|
|
2704
|
+
* with data-juno-when, set data-juno-state on the .juno-state parent, and
|
|
2705
|
+
* the matching wrapper alone gets display:flex. Zero JS in junoui — the
|
|
2706
|
+
* app still decides WHEN a fetch becomes an error or an empty result.
|
|
2707
|
+
* Usage:
|
|
2708
|
+
* <div class="juno-state" data-juno-state="processing">
|
|
2709
|
+
* <div data-juno-when="loading">
|
|
2710
|
+
* <div class="juno-arc juno-arc--indeterminate" role="status" aria-label="Loading"></div>
|
|
2711
|
+
* </div>
|
|
2712
|
+
* <div data-juno-when="processing" aria-busy="true" aria-live="polite">
|
|
2713
|
+
* <div class="juno-shimmer" style="block-size: 120px;"></div>
|
|
2714
|
+
* </div>
|
|
2715
|
+
* <div data-juno-when="error">
|
|
2716
|
+
* <div class="juno-fault" role="status">
|
|
2717
|
+
* <span class="juno-fault__icon" aria-hidden="true">!</span>
|
|
2718
|
+
* <p>Couldn't load this.</p>
|
|
2719
|
+
* </div>
|
|
2720
|
+
* </div>
|
|
2721
|
+
* <div data-juno-when="empty">
|
|
2722
|
+
* <div class="juno-empty">
|
|
2723
|
+
* <span class="juno-empty__icon" aria-hidden="true">∅</span>
|
|
2724
|
+
* <p>Nothing here yet.</p>
|
|
2725
|
+
* </div>
|
|
2726
|
+
* </div>
|
|
2727
|
+
* </div>
|
|
2728
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
2729
|
+
|
|
2730
|
+
/* ── STATE SWITCH — mechanical only ─────────────────────────────────────
|
|
2731
|
+
Hide every marked child; show only the one(s) whose data-juno-when token
|
|
2732
|
+
list contains the parent's current data-juno-state. */
|
|
2733
|
+
.juno-state > [data-juno-when] { display: none; }
|
|
2734
|
+
|
|
2735
|
+
.juno-state[data-juno-state='loading'] > [data-juno-when~='loading'],
|
|
2736
|
+
.juno-state[data-juno-state='processing'] > [data-juno-when~='processing'],
|
|
2737
|
+
.juno-state[data-juno-state='error'] > [data-juno-when~='error'],
|
|
2738
|
+
.juno-state[data-juno-state='empty'] > [data-juno-when~='empty'] { display: flex; }
|
|
2739
|
+
|
|
2740
|
+
/* ── SHIMMER — work in progress, no completion promise ──────────────────
|
|
2741
|
+
For server-side work with no ETA (transcoding, indexing…): a spinner
|
|
2742
|
+
would spin forever, and a .juno-skeleton implies a known layout that
|
|
2743
|
+
doesn't exist here either — just "something is happening". Shares
|
|
2744
|
+
.juno-skeleton's compositor-only band verbatim (same @keyframes,
|
|
2745
|
+
defined once in skeleton.css — not redefined here) so there is one
|
|
2746
|
+
shimmer implementation, not two. Mark the region aria-busy="true". */
|
|
2747
|
+
.juno-shimmer {
|
|
2748
|
+
position: relative;
|
|
2749
|
+
overflow: hidden;
|
|
2750
|
+
background: var(--juno-s2);
|
|
2751
|
+
border-radius: var(--juno-radius-3);
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
.juno-shimmer::before {
|
|
2755
|
+
content: '';
|
|
2756
|
+
position: absolute;
|
|
2757
|
+
inset: 0;
|
|
2758
|
+
background: linear-gradient(
|
|
2759
|
+
100deg,
|
|
2760
|
+
transparent 20%,
|
|
2761
|
+
color-mix(in srgb, var(--juno-s3) 70%, transparent) 50%,
|
|
2762
|
+
transparent 80%
|
|
2763
|
+
);
|
|
2764
|
+
animation: juno-skeleton-shimmer var(--juno-shimmer-dur, 1.4s) ease-in-out infinite;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
/* ── FAULT — terminal, never animates ────────────────────────────────────
|
|
2768
|
+
The load will not resolve on its own (404, decode failure, permanent
|
|
2769
|
+
backend error). Static by design — motion here would lie about the
|
|
2770
|
+
state. Role defaults to caution; recolor with a .juno--<role> class for
|
|
2771
|
+
a different severity, never hardcode a color. This is a per-region
|
|
2772
|
+
fault, not a page-level alert — .juno-alert (role="alert") is for that;
|
|
2773
|
+
here the app wires role="status" so AT announces the failure once,
|
|
2774
|
+
politely, without interrupting. */
|
|
2775
|
+
.juno-fault {
|
|
2776
|
+
--juno-role: var(--juno-caution);
|
|
2777
|
+
|
|
2778
|
+
display: flex;
|
|
2779
|
+
flex-direction: column;
|
|
2780
|
+
align-items: center;
|
|
2781
|
+
justify-content: center;
|
|
2782
|
+
gap: var(--juno-space-8);
|
|
2783
|
+
padding: var(--juno-space-24);
|
|
2784
|
+
text-align: center;
|
|
2785
|
+
color: var(--juno-role);
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
.juno-fault__icon {
|
|
2789
|
+
display: flex;
|
|
2790
|
+
align-items: center;
|
|
2791
|
+
justify-content: center;
|
|
2792
|
+
inline-size: var(--juno-space-40);
|
|
2793
|
+
block-size: var(--juno-space-40);
|
|
2794
|
+
border-radius: var(--juno-radius-8);
|
|
2795
|
+
border: var(--juno-border-width-1) solid currentcolor;
|
|
2796
|
+
font-family: var(--juno-font-family-mono);
|
|
2797
|
+
font-size: var(--juno-font-size-18);
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
/* ── EMPTY — legitimately nothing, terminal ──────────────────────────────
|
|
2801
|
+
The load resolved; there is no data, and none will appear without new
|
|
2802
|
+
input from the user. Generalized, table-agnostic form of the existing
|
|
2803
|
+
.juno-table__empty (table.css) — same anatomy, any region. Neutral
|
|
2804
|
+
(muted) — an empty result isn't a failure, don't tint it like one. */
|
|
2805
|
+
.juno-empty {
|
|
2806
|
+
display: flex;
|
|
2807
|
+
flex-direction: column;
|
|
2808
|
+
align-items: center;
|
|
2809
|
+
justify-content: center;
|
|
2810
|
+
gap: var(--juno-space-12);
|
|
2811
|
+
padding: var(--juno-space-56) var(--juno-space-24);
|
|
2812
|
+
text-align: center;
|
|
2813
|
+
color: var(--juno-label);
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
.juno-empty__icon {
|
|
2817
|
+
display: flex;
|
|
2818
|
+
align-items: center;
|
|
2819
|
+
justify-content: center;
|
|
2820
|
+
inline-size: var(--juno-space-40);
|
|
2821
|
+
block-size: var(--juno-space-40);
|
|
2822
|
+
border-radius: var(--juno-radius-8);
|
|
2823
|
+
border: var(--juno-border-width-1) solid var(--juno-control-edge);
|
|
2824
|
+
color: var(--juno-muted);
|
|
2825
|
+
font-family: var(--juno-font-family-mono);
|
|
2826
|
+
font-size: var(--juno-font-size-18);
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2059
2829
|
/* ════════════════════════════════════════════════════════════════════
|
|
2060
2830
|
* Component — Loaders (Arc · Beacon · Linear bar)
|
|
2061
2831
|
*
|
|
@@ -2099,6 +2869,12 @@ button.juno-list__row:disabled {
|
|
|
2099
2869
|
animation: juno-arc-rot 1.1s steps(12) infinite; /* mechanical, 12-step sweep */
|
|
2100
2870
|
}
|
|
2101
2871
|
|
|
2872
|
+
/* the 12-step sweep reads as jitter under ~24px (e.g. rung around a small
|
|
2873
|
+
badge via .juno-icon-loader) — swap to a continuous rotation there */
|
|
2874
|
+
.juno-arc--smooth.juno-arc--indeterminate {
|
|
2875
|
+
animation-timing-function: linear;
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2102
2878
|
/* centred readout for determinate arcs — place a span inside */
|
|
2103
2879
|
.juno-arc__label {
|
|
2104
2880
|
position: absolute;
|
|
@@ -2283,6 +3059,10 @@ button.juno-list__row:disabled {
|
|
|
2283
3059
|
align-items: center;
|
|
2284
3060
|
gap: var(--juno-gap-control);
|
|
2285
3061
|
inline-size: 100%;
|
|
3062
|
+
|
|
3063
|
+
/* Dock overflow routes here on phones, making this a touch surface; hold the
|
|
3064
|
+
tap minimum (auto-promotes to 44px on coarse pointers). See 20260802-020. */
|
|
3065
|
+
min-block-size: var(--juno-size-tap-min);
|
|
2286
3066
|
padding: var(--juno-space-8) var(--juno-space-10);
|
|
2287
3067
|
background: transparent;
|
|
2288
3068
|
border: none;
|
|
@@ -2378,6 +3158,11 @@ button.juno-list__row:disabled {
|
|
|
2378
3158
|
}
|
|
2379
3159
|
|
|
2380
3160
|
.juno-modal[open] {
|
|
3161
|
+
/* Column so __body can be the bounded scroll port: the surface caps its own
|
|
3162
|
+
height (85dvh as a sheet) and clips, so without this a tall body is simply
|
|
3163
|
+
cut off and unreachable. See 20260803-029. */
|
|
3164
|
+
display: flex;
|
|
3165
|
+
flex-direction: column;
|
|
2381
3166
|
opacity: 1;
|
|
2382
3167
|
transform: translateY(0) scale(1);
|
|
2383
3168
|
}
|
|
@@ -2393,6 +3178,7 @@ button.juno-list__row:disabled {
|
|
|
2393
3178
|
.juno-modal::backdrop {
|
|
2394
3179
|
background: rgb(0 0 0);
|
|
2395
3180
|
opacity: var(--juno-opacity-scrim);
|
|
3181
|
+
-webkit-backdrop-filter: blur(1.5px);
|
|
2396
3182
|
backdrop-filter: blur(1.5px);
|
|
2397
3183
|
}
|
|
2398
3184
|
|
|
@@ -2433,7 +3219,19 @@ button.juno-list__row:disabled {
|
|
|
2433
3219
|
|
|
2434
3220
|
.juno-modal__close:hover { background: var(--juno-s2); color: var(--juno-data); }
|
|
2435
3221
|
|
|
2436
|
-
.juno-modal__body {
|
|
3222
|
+
.juno-modal__body {
|
|
3223
|
+
padding: var(--juno-space-12) var(--juno-pad-surface-inline) var(--juno-pad-surface-inline);
|
|
3224
|
+
|
|
3225
|
+
/* The scroll port (the docs already promised it "scrolls on its own").
|
|
3226
|
+
min-block-size:0 lets a flex item shrink below its content so overflow-y
|
|
3227
|
+
actually engages. overscroll-behavior stops the scroll chaining to the page
|
|
3228
|
+
behind once the body hits its end — the rubber-band-under-the-sheet effect
|
|
3229
|
+
on iOS. NOTE: overscroll-behavior is community convention here, not Apple
|
|
3230
|
+
guidance; no primary Apple source covers it. See 20260803-029. */
|
|
3231
|
+
min-block-size: 0;
|
|
3232
|
+
overflow-y: auto;
|
|
3233
|
+
overscroll-behavior: contain;
|
|
3234
|
+
}
|
|
2437
3235
|
|
|
2438
3236
|
.juno-modal__title {
|
|
2439
3237
|
font-family: var(--juno-font-family-sans);
|
|
@@ -2484,9 +3282,12 @@ button.juno-list__row:disabled {
|
|
|
2484
3282
|
.juno-modal:not(.juno-drawer)[open] { transform: translateY(100%); }
|
|
2485
3283
|
}
|
|
2486
3284
|
|
|
2487
|
-
/* keep the foot's actions above the home indicator
|
|
3285
|
+
/* keep the foot's actions above the home indicator.
|
|
3286
|
+
The env() fallback MUST carry a unit: inside calc(), a unitless `0` is a
|
|
3287
|
+
<number>, the addition is invalid, and the whole declaration is dropped —
|
|
3288
|
+
silently losing the padding on every device without a safe area. */
|
|
2488
3289
|
.juno-modal:not(.juno-drawer) .juno-modal__body {
|
|
2489
|
-
padding-block-end: calc(var(--juno-pad-surface-inline) + env(safe-area-inset-bottom,
|
|
3290
|
+
padding-block-end: calc(var(--juno-pad-surface-inline) + env(safe-area-inset-bottom, 0px));
|
|
2490
3291
|
}
|
|
2491
3292
|
|
|
2492
3293
|
/* full-width actions read better on a sheet */
|
|
@@ -2511,7 +3312,7 @@ button.juno-list__row:disabled {
|
|
|
2511
3312
|
* </a>
|
|
2512
3313
|
* <h1 class="juno-navbar__title">Album</h1>
|
|
2513
3314
|
* <div class="juno-navbar__actions">
|
|
2514
|
-
* <button class="juno-btn juno-btn--
|
|
3315
|
+
* <button class="juno-btn juno-btn--ghost">EDIT</button>
|
|
2515
3316
|
* </div>
|
|
2516
3317
|
* </header>
|
|
2517
3318
|
* ════════════════════════════════════════════════════════════════════ */
|
|
@@ -2592,6 +3393,10 @@ button.juno-list__row:disabled {
|
|
|
2592
3393
|
gap: var(--juno-space-4);
|
|
2593
3394
|
}
|
|
2594
3395
|
|
|
3396
|
+
/* Top-bar action controls are a touch surface: hold the tap minimum
|
|
3397
|
+
(auto-promotes to 44px on coarse pointers). See 20260802-020. */
|
|
3398
|
+
.juno-navbar__actions > * { min-block-size: var(--juno-size-tap-min); }
|
|
3399
|
+
|
|
2595
3400
|
.juno-navbar__back:focus-visible {
|
|
2596
3401
|
outline: var(--juno-border-width-2) solid var(--juno-active);
|
|
2597
3402
|
outline-offset: calc(-1 * var(--juno-space-2));
|
|
@@ -2691,23 +3496,38 @@ button.juno-list__row:disabled {
|
|
|
2691
3496
|
* <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-arrows-clockwise" /></svg>
|
|
2692
3497
|
* </button>
|
|
2693
3498
|
* </nav>
|
|
3499
|
+
* Overflow: for variable-membership pills (an app-owned capacity planner
|
|
3500
|
+
* decides how many __item fit, reading --juno-pillbar-item/-gap/-pad below),
|
|
3501
|
+
* swap the last __item for .juno-pillbar__overflow anchoring a .juno-menu —
|
|
3502
|
+
* see docs/components/pillbar.md#overflow-slot and menu.css.
|
|
2694
3503
|
* ════════════════════════════════════════════════════════════════════ */
|
|
2695
3504
|
|
|
2696
3505
|
.juno-pillbar {
|
|
2697
3506
|
--juno-role: var(--juno-active);
|
|
2698
3507
|
|
|
3508
|
+
/* Geometry contract — published so a consumer's overflow/capacity planner
|
|
3509
|
+
(deciding how many __item fit before routing the rest to __overflow)
|
|
3510
|
+
reads real values instead of re-declaring junoui's px constants in JS.
|
|
3511
|
+
Base-rule values only; the corner variants below keep their own literal
|
|
3512
|
+
offsets (distinct block vs. inline edges) and are not wired to -edge. */
|
|
3513
|
+
--juno-pillbar-item: var(--juno-size-tap-comfortable);
|
|
3514
|
+
--juno-pillbar-gap: var(--juno-space-2);
|
|
3515
|
+
--juno-pillbar-pad: var(--juno-space-4);
|
|
3516
|
+
--juno-pillbar-edge: var(--juno-space-16);
|
|
3517
|
+
|
|
2699
3518
|
position: sticky;
|
|
2700
|
-
inset-block-end: calc(var(--juno-
|
|
3519
|
+
inset-block-end: calc(var(--juno-pillbar-edge) + env(safe-area-inset-bottom, 0px));
|
|
2701
3520
|
z-index: var(--juno-z-raised);
|
|
2702
3521
|
margin-inline: auto;
|
|
2703
|
-
margin-block-end: var(--juno-
|
|
3522
|
+
margin-block-end: var(--juno-pillbar-edge);
|
|
2704
3523
|
inline-size: fit-content;
|
|
2705
|
-
max-inline-size: calc(100% - 2 * var(--juno-
|
|
3524
|
+
max-inline-size: calc(100% - 2 * var(--juno-pillbar-edge));
|
|
2706
3525
|
display: flex;
|
|
2707
3526
|
align-items: center;
|
|
2708
|
-
gap: var(--juno-
|
|
2709
|
-
padding: var(--juno-
|
|
3527
|
+
gap: var(--juno-pillbar-gap);
|
|
3528
|
+
padding: var(--juno-pillbar-pad);
|
|
2710
3529
|
background: color-mix(in srgb, var(--juno-s1) 88%, transparent);
|
|
3530
|
+
-webkit-backdrop-filter: blur(12px);
|
|
2711
3531
|
backdrop-filter: blur(12px);
|
|
2712
3532
|
border: var(--juno-border-width-1) solid var(--juno-border);
|
|
2713
3533
|
border-radius: 999px;
|
|
@@ -2718,8 +3538,8 @@ button.juno-list__row:disabled {
|
|
|
2718
3538
|
appearance: none;
|
|
2719
3539
|
margin: 0;
|
|
2720
3540
|
border: none;
|
|
2721
|
-
min-inline-size: var(--juno-
|
|
2722
|
-
min-block-size: var(--juno-
|
|
3541
|
+
min-inline-size: var(--juno-pillbar-item);
|
|
3542
|
+
min-block-size: var(--juno-pillbar-item);
|
|
2723
3543
|
display: inline-flex;
|
|
2724
3544
|
align-items: center;
|
|
2725
3545
|
justify-content: center;
|
|
@@ -2766,6 +3586,54 @@ button.juno-list__row:disabled {
|
|
|
2766
3586
|
cursor: not-allowed;
|
|
2767
3587
|
}
|
|
2768
3588
|
|
|
3589
|
+
/* Overflow slot — the "more" trigger for items that don't fit the pill.
|
|
3590
|
+
Styled like __item (same tap target, radius, hover/focus) but a dedicated
|
|
3591
|
+
element because it opens a menu instead of acting as a nav/toggle item.
|
|
3592
|
+
Pair with .juno-menu (menu.css) via popovertarget — the invoker is the
|
|
3593
|
+
menu's implicit anchor, so no extra anchor-positioning is needed here.
|
|
3594
|
+
junoui ships the docking point only: DECIDING which items overflow (and
|
|
3595
|
+
when) is app policy — mount/unmount this button and move the spilled
|
|
3596
|
+
__item markup into the menu, sizing the decision off the
|
|
3597
|
+
--juno-pillbar-item/-gap/-pad custom props above instead of hardcoded px.
|
|
3598
|
+
Zero JS in this file; aria-expanded is toggled by the app or a stateless
|
|
3599
|
+
enhancer, same convention as __item's [aria-pressed].
|
|
3600
|
+
.juno-pillbar__toggle (the --collapsible variant's expand/collapse control,
|
|
3601
|
+
below) shares this exact chrome: same circle, same states. */
|
|
3602
|
+
.juno-pillbar__overflow,
|
|
3603
|
+
.juno-pillbar__toggle {
|
|
3604
|
+
appearance: none;
|
|
3605
|
+
margin: 0;
|
|
3606
|
+
border: none;
|
|
3607
|
+
flex-shrink: 0;
|
|
3608
|
+
min-inline-size: var(--juno-pillbar-item);
|
|
3609
|
+
min-block-size: var(--juno-pillbar-item);
|
|
3610
|
+
display: inline-flex;
|
|
3611
|
+
align-items: center;
|
|
3612
|
+
justify-content: center;
|
|
3613
|
+
background: transparent;
|
|
3614
|
+
border-radius: 999px;
|
|
3615
|
+
color: var(--juno-label);
|
|
3616
|
+
cursor: pointer;
|
|
3617
|
+
transition:
|
|
3618
|
+
color var(--juno-motion-duration-quick) var(--juno-motion-ease-standard),
|
|
3619
|
+
background-color var(--juno-motion-duration-quick) var(--juno-motion-ease-standard);
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
.juno-pillbar__overflow:hover,
|
|
3623
|
+
.juno-pillbar__toggle:hover { color: var(--juno-data); }
|
|
3624
|
+
|
|
3625
|
+
.juno-pillbar__overflow[aria-expanded='true'],
|
|
3626
|
+
.juno-pillbar__toggle[aria-expanded='true'] {
|
|
3627
|
+
background: var(--juno-s3);
|
|
3628
|
+
color: var(--juno-role);
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
.juno-pillbar__overflow:focus-visible,
|
|
3632
|
+
.juno-pillbar__toggle:focus-visible {
|
|
3633
|
+
outline: var(--juno-border-width-2) solid var(--juno-active);
|
|
3634
|
+
outline-offset: calc(-1 * var(--juno-space-2));
|
|
3635
|
+
}
|
|
3636
|
+
|
|
2769
3637
|
.juno-pillbar__sep {
|
|
2770
3638
|
align-self: center;
|
|
2771
3639
|
inline-size: var(--juno-border-width-1);
|
|
@@ -2789,10 +3657,147 @@ button.juno-list__row:disabled {
|
|
|
2789
3657
|
z-index: var(--juno-z-anchored);
|
|
2790
3658
|
}
|
|
2791
3659
|
|
|
3660
|
+
/* Corner placement — a floating cluster pinned to one viewport corner rather
|
|
3661
|
+
than the centered bottom bar (e.g. a top-right search/filter cluster, or a
|
|
3662
|
+
bottom-right action pill). Each variant is self-contained: it flips to
|
|
3663
|
+
fixed, clears the base's bottom-center geometry (auto margins / bottom
|
|
3664
|
+
inset), and clamps the corner with safe-area insets so it never lands under
|
|
3665
|
+
a notch or the home indicator. Keep the base blur/border/shadow. Pair with a
|
|
3666
|
+
scroll-clearance reservation on the scroller if the corner overlaps content
|
|
3667
|
+
(var(--juno-pillbar-clearance) for the bottom corners). */
|
|
3668
|
+
.juno-pillbar--top-right,
|
|
3669
|
+
.juno-pillbar--top-left,
|
|
3670
|
+
.juno-pillbar--bottom-right,
|
|
3671
|
+
.juno-pillbar--bottom-left {
|
|
3672
|
+
position: fixed;
|
|
3673
|
+
inset-block: auto;
|
|
3674
|
+
inset-inline: auto;
|
|
3675
|
+
z-index: var(--juno-z-anchored);
|
|
3676
|
+
margin: 0;
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
.juno-pillbar--top-right {
|
|
3680
|
+
inset-block-start: calc(var(--juno-space-10) + env(safe-area-inset-top, 0px));
|
|
3681
|
+
inset-inline-end: calc(var(--juno-space-12) + env(safe-area-inset-right, 0px));
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
.juno-pillbar--top-left {
|
|
3685
|
+
inset-block-start: calc(var(--juno-space-10) + env(safe-area-inset-top, 0px));
|
|
3686
|
+
inset-inline-start: calc(var(--juno-space-12) + env(safe-area-inset-left, 0px));
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
.juno-pillbar--bottom-right {
|
|
3690
|
+
inset-block-end: calc(var(--juno-space-16) + env(safe-area-inset-bottom, 0px));
|
|
3691
|
+
inset-inline-end: calc(var(--juno-space-12) + env(safe-area-inset-right, 0px));
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
.juno-pillbar--bottom-left {
|
|
3695
|
+
inset-block-end: calc(var(--juno-space-16) + env(safe-area-inset-bottom, 0px));
|
|
3696
|
+
inset-inline-start: calc(var(--juno-space-12) + env(safe-area-inset-left, 0px));
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
/* Input slot — an expandable search field living inside the pill. Borderless
|
|
3700
|
+
and transparent so it reads as part of the pill, not a boxed control. The
|
|
3701
|
+
font-size floor matters: iOS Safari zooms the whole page onto any focused
|
|
3702
|
+
text field under 16px, so hold this at max(16px, …) exactly like the base
|
|
3703
|
+
.juno-input touch rule. Give it an accessible label (aria-label or a
|
|
3704
|
+
visually-hidden <label>) — the placeholder is not a label. */
|
|
3705
|
+
.juno-pillbar__input {
|
|
3706
|
+
flex: 0 1 auto;
|
|
3707
|
+
min-inline-size: 0;
|
|
3708
|
+
inline-size: min(52vw, 240px);
|
|
3709
|
+
margin: 0;
|
|
3710
|
+
padding-inline: var(--juno-space-8);
|
|
3711
|
+
border: none;
|
|
3712
|
+
background: transparent;
|
|
3713
|
+
outline: none;
|
|
3714
|
+
font-family: var(--juno-font-family-sans);
|
|
3715
|
+
font-size: max(16px, var(--juno-font-size-16));
|
|
3716
|
+
color: var(--juno-data);
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
.juno-pillbar__input::placeholder { color: var(--juno-muted); }
|
|
3720
|
+
|
|
3721
|
+
.juno-pillbar__input:focus-visible {
|
|
3722
|
+
outline: none;
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
/* Collapsible — the whole pill folds into a single circular toggle, and a
|
|
3726
|
+
tap expands it back to full width. State lives on the toggle's
|
|
3727
|
+
aria-expanded (app-owned, like __item's aria-pressed — zero JS here);
|
|
3728
|
+
the bar reads it via :has(), so DOM order of toggle vs. tray is free
|
|
3729
|
+
(toggle-first for a left-anchored pill, toggle-last for right-anchored).
|
|
3730
|
+
|
|
3731
|
+
Usage:
|
|
3732
|
+
<nav class="juno-pillbar juno-pillbar--collapsible" aria-label="Tools">
|
|
3733
|
+
<button class="juno-pillbar__toggle" aria-expanded="false" aria-label="Show toolbar">
|
|
3734
|
+
<svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-dots-three" /></svg>
|
|
3735
|
+
</button>
|
|
3736
|
+
<div class="juno-pillbar__tray">
|
|
3737
|
+
<div><!-- single wrapper, any element -->
|
|
3738
|
+
…the usual __item / __sep / __input children…
|
|
3739
|
+
</div>
|
|
3740
|
+
</div>
|
|
3741
|
+
</nav>
|
|
3742
|
+
|
|
3743
|
+
Mechanism: the tray is a one-column grid whose track animates 0fr ↔ 1fr —
|
|
3744
|
+
the only widely-supported way to TRANSITION to an intrinsic width (Safari 16+;
|
|
3745
|
+
width: fit-content and interpolate-size aren't animatable there). The track
|
|
3746
|
+
needs one shrinkable child, hence the mandatory single wrapper (any element:
|
|
3747
|
+
it's selected as > *, so there is no class to forget). visibility rides the
|
|
3748
|
+
same transition — discrete animation keeps the tray visible while it slides
|
|
3749
|
+
shut, then hides it, which also drops the hidden items from the tab order
|
|
3750
|
+
(collapsing while focus is inside the tray is the app's edge to handle:
|
|
3751
|
+
move focus to the toggle first). prefers-reduced-motion needs nothing
|
|
3752
|
+
component-local — the base layer zeroes every transition duration. */
|
|
3753
|
+
.juno-pillbar--collapsible {
|
|
3754
|
+
transition: gap var(--juno-motion-duration-deliberate) var(--juno-motion-ease-standard);
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
.juno-pillbar__tray {
|
|
3758
|
+
display: grid;
|
|
3759
|
+
grid-template-columns: 1fr;
|
|
3760
|
+
transition:
|
|
3761
|
+
grid-template-columns var(--juno-motion-duration-deliberate) var(--juno-motion-ease-standard),
|
|
3762
|
+
opacity var(--juno-motion-duration-deliberate) var(--juno-motion-ease-standard),
|
|
3763
|
+
visibility var(--juno-motion-duration-deliberate) var(--juno-motion-ease-standard);
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
.juno-pillbar__tray > * {
|
|
3767
|
+
display: flex;
|
|
3768
|
+
align-items: center;
|
|
3769
|
+
gap: var(--juno-pillbar-gap);
|
|
3770
|
+
min-inline-size: 0;
|
|
3771
|
+
overflow: hidden;
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
.juno-pillbar--collapsible:has(> .juno-pillbar__toggle[aria-expanded='false']) {
|
|
3775
|
+
/* gap → 0 so the collapsed pill is exactly the toggle's circle, whichever
|
|
3776
|
+
side the tray sits on (a negative margin would have to know the side) */
|
|
3777
|
+
gap: 0;
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
.juno-pillbar--collapsible:has(> .juno-pillbar__toggle[aria-expanded='false']) .juno-pillbar__tray {
|
|
3781
|
+
grid-template-columns: 0fr;
|
|
3782
|
+
opacity: 0;
|
|
3783
|
+
visibility: hidden;
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
/* the toggle's glyph turns a quarter as the tray opens — motion reads as
|
|
3787
|
+
"this button did it" even when the expansion pushes the pill off-center */
|
|
3788
|
+
.juno-pillbar__toggle .juno-icon {
|
|
3789
|
+
transition: transform var(--juno-motion-duration-deliberate) var(--juno-motion-ease-standard);
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
.juno-pillbar__toggle[aria-expanded='true'] .juno-icon {
|
|
3793
|
+
transform: rotate(90deg);
|
|
3794
|
+
}
|
|
3795
|
+
|
|
2792
3796
|
/* reduced-transparency preference → solid surface, no blur */
|
|
2793
3797
|
@media (prefers-reduced-transparency: reduce) {
|
|
2794
3798
|
.juno-pillbar {
|
|
2795
3799
|
background: var(--juno-s1);
|
|
3800
|
+
-webkit-backdrop-filter: none;
|
|
2796
3801
|
backdrop-filter: none;
|
|
2797
3802
|
}
|
|
2798
3803
|
}
|
|
@@ -3022,6 +4027,55 @@ button.juno-list__row:disabled {
|
|
|
3022
4027
|
margin-top: var(--juno-space-4);
|
|
3023
4028
|
}
|
|
3024
4029
|
|
|
4030
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
4031
|
+
* Component — Reload indicator (refetch-in-place)
|
|
4032
|
+
* The non-blocking counterpart to the skeleton. Skeletons stand in for
|
|
4033
|
+
* content that isn't there yet (first paint); the reload dot signals a
|
|
4034
|
+
* refresh happening OVER content that's already on screen — the stale data
|
|
4035
|
+
* stays readable and interactive while fresh data lands.
|
|
4036
|
+
*
|
|
4037
|
+
* A small pulsing dot with a soft halo, centered as a fixed overlay that
|
|
4038
|
+
* eats no pointer events (the page underneath stays fully usable). Color
|
|
4039
|
+
* comes from --juno-role (default = active). The app owns the state: render
|
|
4040
|
+
* it while a background refetch is in flight, drop it when it settles.
|
|
4041
|
+
*
|
|
4042
|
+
* a11y: role="status" + aria-label so assistive tech announces the refresh
|
|
4043
|
+
* politely without stealing focus. Keep it a live status, not an alert.
|
|
4044
|
+
* Usage:
|
|
4045
|
+
* <div class="juno-reload" role="status" aria-label="Reloading">
|
|
4046
|
+
* <span class="juno-reload__dot"></span>
|
|
4047
|
+
* </div>
|
|
4048
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
4049
|
+
|
|
4050
|
+
.juno-reload {
|
|
4051
|
+
--juno-role: var(--juno-active);
|
|
4052
|
+
|
|
4053
|
+
position: fixed;
|
|
4054
|
+
inset-block-start: 50%;
|
|
4055
|
+
inset-inline-start: 50%;
|
|
4056
|
+
transform: translate(-50%, -50%);
|
|
4057
|
+
z-index: var(--juno-z-anchored);
|
|
4058
|
+
display: flex;
|
|
4059
|
+
align-items: center;
|
|
4060
|
+
justify-content: center;
|
|
4061
|
+
pointer-events: none; /* refresh is non-blocking — never trap clicks */
|
|
4062
|
+
}
|
|
4063
|
+
|
|
4064
|
+
.juno-reload__dot {
|
|
4065
|
+
inline-size: var(--juno-space-16);
|
|
4066
|
+
block-size: var(--juno-space-16);
|
|
4067
|
+
border-radius: 50%;
|
|
4068
|
+
background: var(--juno-role);
|
|
4069
|
+
|
|
4070
|
+
/* soft halo so the dot reads over any surface without a hard edge */
|
|
4071
|
+
box-shadow: 0 0 0 var(--juno-space-8) color-mix(in srgb, var(--juno-role) 20%, transparent);
|
|
4072
|
+
animation: juno-pulse 1.4s ease-in-out infinite;
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
/* prefers-reduced-motion: the base layer already slows every animation to a
|
|
4076
|
+
stop; the dot + halo stay visible (steady), so the "refreshing" state is
|
|
4077
|
+
still conveyed without the pulse. */
|
|
4078
|
+
|
|
3025
4079
|
/* ════════════════════════════════════════════════════════════════════
|
|
3026
4080
|
* Component — Segmented control
|
|
3027
4081
|
* Exclusive-choice pill row (filters, mode switchers, sort pickers).
|
|
@@ -3165,14 +4219,20 @@ button.juno-seg__opt:disabled {
|
|
|
3165
4219
|
* Component — Skeleton · loading placeholder
|
|
3166
4220
|
* A shimmering block that stands in for content not yet loaded. Same
|
|
3167
4221
|
* shimmer language as the table's row skeleton, generalized: text lines,
|
|
3168
|
-
* blocks,
|
|
3169
|
-
* --juno-skeleton-h
|
|
3170
|
-
*
|
|
4222
|
+
* blocks, circles (avatars), and aspect-ratio tiles (media grids). Zero
|
|
4223
|
+
* JS — size it with width / height, --juno-skeleton-h, or (for --tile)
|
|
4224
|
+
* --juno-skeleton-ratio. The shimmer is a `::before` overlay animated on
|
|
4225
|
+
* transform/opacity only — never background-position — so it stays on
|
|
4226
|
+
* the compositor instead of repainting the gradient every frame; the
|
|
4227
|
+
* base fill is a plain solid color. Reduced motion is handled globally
|
|
4228
|
+
* (base.css collapses all animation-duration to ~0), no local override
|
|
4229
|
+
* needed here. Mark the loading region aria-busy="true" so AT announces it.
|
|
3171
4230
|
* Usage:
|
|
3172
4231
|
* <div aria-busy="true" aria-live="polite">
|
|
3173
4232
|
* <span class="juno-skeleton juno-skeleton--circle" style="--juno-skeleton-h: 48px;"></span>
|
|
3174
4233
|
* <span class="juno-skeleton juno-skeleton--text" style="inline-size: 60%;"></span>
|
|
3175
4234
|
* <span class="juno-skeleton juno-skeleton--text"></span>
|
|
4235
|
+
* <span class="juno-skeleton juno-skeleton--tile"></span>
|
|
3176
4236
|
* </div>
|
|
3177
4237
|
* ════════════════════════════════════════════════════════════════════ */
|
|
3178
4238
|
|
|
@@ -3180,11 +4240,27 @@ button.juno-seg__opt:disabled {
|
|
|
3180
4240
|
--juno-skeleton-h: var(--juno-space-16);
|
|
3181
4241
|
|
|
3182
4242
|
display: block;
|
|
4243
|
+
position: relative;
|
|
4244
|
+
overflow: hidden;
|
|
3183
4245
|
inline-size: 100%;
|
|
3184
4246
|
block-size: var(--juno-skeleton-h);
|
|
3185
4247
|
border-radius: var(--juno-radius-3);
|
|
3186
|
-
background:
|
|
3187
|
-
|
|
4248
|
+
background: var(--juno-s2);
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4251
|
+
/* the shimmer itself: a diagonal highlight band that slides across via
|
|
4252
|
+
transform only (compositor-only — no layout/paint per frame). Clipped to
|
|
4253
|
+
the parent's border-radius by the overflow: hidden above. */
|
|
4254
|
+
.juno-skeleton::before {
|
|
4255
|
+
content: '';
|
|
4256
|
+
position: absolute;
|
|
4257
|
+
inset: 0;
|
|
4258
|
+
background: linear-gradient(
|
|
4259
|
+
100deg,
|
|
4260
|
+
transparent 20%,
|
|
4261
|
+
color-mix(in srgb, var(--juno-s3) 70%, transparent) 50%,
|
|
4262
|
+
transparent 80%
|
|
4263
|
+
);
|
|
3188
4264
|
animation: juno-skeleton-shimmer 1.2s ease-in-out infinite;
|
|
3189
4265
|
}
|
|
3190
4266
|
|
|
@@ -3208,19 +4284,25 @@ button.juno-seg__opt:disabled {
|
|
|
3208
4284
|
border-radius: var(--juno-radius-4);
|
|
3209
4285
|
}
|
|
3210
4286
|
|
|
4287
|
+
/* content-box mode: size from an aspect ratio instead of a fixed height —
|
|
4288
|
+
the dominant case for media-grid tiles standing in for a real image/card
|
|
4289
|
+
of known proportions. Default 1 (square); override per instance with
|
|
4290
|
+
--juno-skeleton-ratio (e.g. "16 / 9"). */
|
|
4291
|
+
.juno-skeleton--tile {
|
|
4292
|
+
--juno-skeleton-ratio: 1;
|
|
4293
|
+
|
|
4294
|
+
aspect-ratio: var(--juno-skeleton-ratio);
|
|
4295
|
+
block-size: auto;
|
|
4296
|
+
border-radius: var(--juno-radius-4);
|
|
4297
|
+
}
|
|
4298
|
+
|
|
3211
4299
|
@keyframes juno-skeleton-shimmer {
|
|
3212
4300
|
from {
|
|
3213
|
-
|
|
4301
|
+
transform: translateX(-100%);
|
|
3214
4302
|
}
|
|
3215
4303
|
|
|
3216
4304
|
to {
|
|
3217
|
-
|
|
3218
|
-
}
|
|
3219
|
-
}
|
|
3220
|
-
|
|
3221
|
-
@media (prefers-reduced-motion: reduce) {
|
|
3222
|
-
.juno-skeleton {
|
|
3223
|
-
animation: none;
|
|
4305
|
+
transform: translateX(100%);
|
|
3224
4306
|
}
|
|
3225
4307
|
}
|
|
3226
4308
|
|
|
@@ -4098,6 +5180,11 @@ button.juno-seg__opt:disabled {
|
|
|
4098
5180
|
scrollbar-width: none;
|
|
4099
5181
|
}
|
|
4100
5182
|
|
|
5183
|
+
/* scrollbar-width only reached Safari 18.2, so the scrollable strip still shows
|
|
5184
|
+
a bar on older iOS — keep the WebKit pseudo-element alongside it (same pair
|
|
5185
|
+
.juno-scroller--bare uses). */
|
|
5186
|
+
.juno-tabs__list::-webkit-scrollbar { display: none; }
|
|
5187
|
+
|
|
4101
5188
|
.juno-tabs__tab {
|
|
4102
5189
|
appearance: none;
|
|
4103
5190
|
margin: 0;
|
|
@@ -4145,10 +5232,22 @@ button.juno-seg__opt:disabled {
|
|
|
4145
5232
|
* <figure class="juno-thumb" style="aspect-ratio: 1">
|
|
4146
5233
|
* <img src="…" alt="Sunset clip" onerror="this.remove()" />
|
|
4147
5234
|
* </figure>
|
|
4148
|
-
*
|
|
5235
|
+
* \3c !-- known-missing: ship no <img>; label the figure instead -->
|
|
4149
5236
|
* <figure class="juno-thumb juno-thumb--video" role="img" aria-label="Preview unavailable"></figure>
|
|
4150
5237
|
* The onerror one-liner is the whole JS contract (optional, stateless):
|
|
4151
5238
|
* a broken image removes itself and the placeholder underneath shows.
|
|
5239
|
+
* Aspect is locked by default (--juno-thumb-ratio, square) so a media wall's
|
|
5240
|
+
* scroll height is stable before anything loads; override per instance with
|
|
5241
|
+
* style="--juno-thumb-ratio: 16/9" (an inline aspect-ratio still wins too).
|
|
5242
|
+
* Selection is an outline (never a border — a border would shift the frame)
|
|
5243
|
+
* toggled by a class/data-attr; junoui never decides what's selected.
|
|
5244
|
+
* Corner slots anchor badges/duration chips over the media with logical
|
|
5245
|
+
* inset so they swap sides correctly under dir="rtl".
|
|
5246
|
+
* Usage:
|
|
5247
|
+
* <figure class="juno-thumb juno-thumb--selected">
|
|
5248
|
+
* <img src="…" alt="Sunset clip" onerror="this.remove()" />
|
|
5249
|
+
* <span class="juno-thumb__corner juno-thumb__corner--top-end badge">0:42</span>
|
|
5250
|
+
* </figure>
|
|
4152
5251
|
* ════════════════════════════════════════════════════════════════════ */
|
|
4153
5252
|
|
|
4154
5253
|
.juno-thumb {
|
|
@@ -4159,12 +5258,30 @@ button.juno-seg__opt:disabled {
|
|
|
4159
5258
|
justify-content: center;
|
|
4160
5259
|
gap: var(--juno-space-4);
|
|
4161
5260
|
margin: 0;
|
|
5261
|
+
aspect-ratio: var(--juno-thumb-ratio, 1);
|
|
4162
5262
|
overflow: hidden;
|
|
4163
5263
|
background: var(--juno-s2);
|
|
4164
5264
|
border: var(--juno-border-width-1) solid var(--juno-border);
|
|
4165
5265
|
border-radius: var(--juno-radius-3);
|
|
4166
5266
|
}
|
|
4167
5267
|
|
|
5268
|
+
/* Flush — drop the border/radius for tight walls where the frame's own edge
|
|
5269
|
+
would fight the grid (full-bleed tiles); the media stays edge-to-edge via
|
|
5270
|
+
object-fit: cover regardless. */
|
|
5271
|
+
.juno-thumb--flush {
|
|
5272
|
+
border: none;
|
|
5273
|
+
border-radius: 0;
|
|
5274
|
+
}
|
|
5275
|
+
|
|
5276
|
+
/* Selected — an inset outline, not a border: it overlays the existing frame
|
|
5277
|
+
edge instead of adding to the box, so nothing reflows when selection
|
|
5278
|
+
toggles. Role-neutral (--juno-active), matching the checkbox/switch focus
|
|
5279
|
+
ring elsewhere in the system. */
|
|
5280
|
+
.juno-thumb--selected {
|
|
5281
|
+
outline: var(--juno-border-width-2) solid var(--juno-active);
|
|
5282
|
+
outline-offset: calc(-1 * var(--juno-border-width-2));
|
|
5283
|
+
}
|
|
5284
|
+
|
|
4168
5285
|
/* placeholder glyph (Phosphor "images", masked so it takes any color) */
|
|
4169
5286
|
.juno-thumb::before {
|
|
4170
5287
|
--juno-thumb-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath d='M160,88a16,16,0,1,1,16,16A16,16,0,0,1,160,88Zm76-32V160a20,20,0,0,1-20,20H204v20a20,20,0,0,1-20,20H40a20,20,0,0,1-20-20V88A20,20,0,0,1,40,68H60V56A20,20,0,0,1,80,36H216A20,20,0,0,1,236,56ZM180,180H80a20,20,0,0,1-20-20V92H44V196H180Zm-21.66-24L124,121.66,89.66,156ZM212,60H84v67.72l25.86-25.86a20,20,0,0,1,28.28,0L192.28,156H212Z'/%3E%3C/svg%3E");
|
|
@@ -4204,6 +5321,38 @@ button.juno-seg__opt:disabled {
|
|
|
4204
5321
|
object-fit: cover;
|
|
4205
5322
|
}
|
|
4206
5323
|
|
|
5324
|
+
/* Corner overlay — anchor a badge, check, or duration chip over the media
|
|
5325
|
+
without joining flow or touching the frame's aspect-ratio. Pick one
|
|
5326
|
+
position modifier; the base only sets stacking + inset from the frame
|
|
5327
|
+
edge. Logical inset (not top/left) so start/end swap under dir="rtl" —
|
|
5328
|
+
and the modifiers are named start/end for the same reason: a `--top-right`
|
|
5329
|
+
that renders top-LEFT in RTL would be a trap. */
|
|
5330
|
+
.juno-thumb__corner {
|
|
5331
|
+
position: absolute;
|
|
5332
|
+
z-index: 1;
|
|
5333
|
+
margin: var(--juno-space-4);
|
|
5334
|
+
}
|
|
5335
|
+
|
|
5336
|
+
.juno-thumb__corner--top-start {
|
|
5337
|
+
inset-block-start: 0;
|
|
5338
|
+
inset-inline-start: 0;
|
|
5339
|
+
}
|
|
5340
|
+
|
|
5341
|
+
.juno-thumb__corner--top-end {
|
|
5342
|
+
inset-block-start: 0;
|
|
5343
|
+
inset-inline-end: 0;
|
|
5344
|
+
}
|
|
5345
|
+
|
|
5346
|
+
.juno-thumb__corner--bottom-start {
|
|
5347
|
+
inset-block-end: 0;
|
|
5348
|
+
inset-inline-start: 0;
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5351
|
+
.juno-thumb__corner--bottom-end {
|
|
5352
|
+
inset-block-end: 0;
|
|
5353
|
+
inset-inline-end: 0;
|
|
5354
|
+
}
|
|
5355
|
+
|
|
4207
5356
|
/* ════════════════════════════════════════════════════════════════════
|
|
4208
5357
|
* Component — Toast / snackbar
|
|
4209
5358
|
* A transient, floating notification. Same role-rail language as the
|
|
@@ -4297,7 +5446,11 @@ button.juno-seg__opt:disabled {
|
|
|
4297
5446
|
@media (width <= 639.98px) {
|
|
4298
5447
|
.juno-toast-stack {
|
|
4299
5448
|
inset-inline: var(--juno-space-12);
|
|
4300
|
-
|
|
5449
|
+
|
|
5450
|
+
/* unit-bearing env() fallback: inside calc() a unitless `0` is a <number>,
|
|
5451
|
+
which invalidates the sum and drops the whole declaration (the stack
|
|
5452
|
+
would then sit flush at 0 on any device without a safe area). */
|
|
5453
|
+
inset-block-end: calc(var(--juno-space-12) + env(safe-area-inset-bottom, 0px));
|
|
4301
5454
|
inline-size: auto;
|
|
4302
5455
|
}
|
|
4303
5456
|
|
|
@@ -4604,8 +5757,11 @@ button.juno-seg__opt:disabled {
|
|
|
4604
5757
|
font-variant-numeric: tabular-nums;
|
|
4605
5758
|
}
|
|
4606
5759
|
|
|
5760
|
+
/* --juno-label-size: optional size override, set by an ancestor (e.g. a
|
|
5761
|
+
* density wrapper or a "compact" section) — never declared here, so it
|
|
5762
|
+
* isn't shadowed for descendants that also want to read it. */
|
|
4607
5763
|
.juno-label {
|
|
4608
|
-
font-size: var(--juno-font-size-13);
|
|
5764
|
+
font-size: var(--juno-label-size, var(--juno-font-size-13));
|
|
4609
5765
|
color: var(--juno-label);
|
|
4610
5766
|
}
|
|
4611
5767
|
|