@junoput01/junoui 0.2.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 +251 -0
- package/README.md +23 -19
- package/dist/css/juno-custom-media.css +32 -0
- package/dist/css/juno.css +1355 -40
- package/dist/icons/inline.js +22 -0
- package/docs/accessibility.md +188 -0
- package/docs/boot-shell.md +295 -0
- package/docs/components/README.md +71 -0
- package/docs/components/accordion.md +49 -0
- package/docs/components/alert.md +46 -0
- package/docs/components/avatar.md +45 -0
- package/docs/components/badge.md +35 -0
- package/docs/components/breadcrumb.md +31 -0
- package/docs/components/button.md +32 -0
- package/docs/components/card.md +41 -0
- package/docs/components/checkbox.md +41 -0
- package/docs/components/chip.md +44 -0
- package/docs/components/divider.md +32 -0
- package/docs/components/dock.md +215 -0
- package/docs/components/drawer.md +100 -0
- package/docs/components/field.md +44 -0
- package/docs/components/fold-slot.md +31 -0
- package/docs/components/gauge.md +48 -0
- package/docs/components/icon-loader.md +122 -0
- package/docs/components/icon.md +70 -0
- package/docs/components/input.md +45 -0
- package/docs/components/list.md +76 -0
- package/docs/components/load-state.md +131 -0
- package/docs/components/loader.md +88 -0
- package/docs/components/menu.md +54 -0
- package/docs/components/modal.md +52 -0
- package/docs/components/navbar.md +53 -0
- package/docs/components/pagination.md +42 -0
- package/docs/components/pillbar.md +211 -0
- package/docs/components/popover.md +43 -0
- package/docs/components/rail.md +54 -0
- package/docs/components/readout.md +39 -0
- package/docs/components/reload.md +41 -0
- package/docs/components/segmented.md +46 -0
- package/docs/components/select.md +33 -0
- package/docs/components/skeleton.md +45 -0
- package/docs/components/slider.md +48 -0
- package/docs/components/spark.md +51 -0
- package/docs/components/status.md +30 -0
- package/docs/components/stepper.md +46 -0
- package/docs/components/switch.md +38 -0
- package/docs/components/table.md +126 -0
- package/docs/components/tabs.md +61 -0
- package/docs/components/thumb.md +81 -0
- package/docs/components/toast.md +48 -0
- package/docs/components/toggle-button.md +40 -0
- package/docs/components/tooltip.md +63 -0
- package/docs/design-guidelines.md +173 -0
- package/docs/flutter.md +42 -0
- package/docs/getting-started.md +93 -0
- package/docs/integration.md +95 -0
- package/docs/ios-conformance.md +224 -0
- package/docs/layout.md +271 -0
- package/docs/native.md +52 -0
- package/docs/roadmap.md +106 -0
- package/docs/tokens-reference.md +223 -0
- package/docs/web.md +95 -0
- package/package.json +7 -3
- package/src/css/base.css +171 -1
- package/src/css/components/dock.css +335 -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 +67 -0
- 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 +222 -7
- package/src/css/components/rail.css +8 -0
- 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 +95 -5
- 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
|
|
@@ -440,11 +470,103 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
440
470
|
the contract; a phone just reads the bigger one. */
|
|
441
471
|
@media (pointer: coarse) {
|
|
442
472
|
:root { --juno-size-tap-min: var(--juno-size-tap-comfortable); }
|
|
473
|
+
|
|
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. */
|
|
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
|
+
}
|
|
443
497
|
}
|
|
444
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
|
+
|
|
445
554
|
@keyframes juno-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
|
446
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
|
+
|
|
447
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
|
+
|
|
448
570
|
*, *::before, *::after {
|
|
449
571
|
animation-duration: 0.01ms !important;
|
|
450
572
|
animation-iteration-count: 1 !important;
|
|
@@ -470,6 +592,54 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
470
592
|
.juno-badge { forced-color-adjust: none; } /* keep status fill meaningful */
|
|
471
593
|
}
|
|
472
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
|
+
|
|
473
643
|
/* ════════════════════════════════════════════════════════════════════
|
|
474
644
|
* junoui — layout layer (intrinsic & responsive)
|
|
475
645
|
*
|
|
@@ -554,21 +724,111 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
554
724
|
flex-basis: calc((var(--juno-switcher-threshold, var(--juno-bp-sm)) - 100%) * 999);
|
|
555
725
|
}
|
|
556
726
|
|
|
557
|
-
/*
|
|
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. */
|
|
558
756
|
.juno-reel {
|
|
559
757
|
display: flex;
|
|
560
758
|
gap: var(--juno-reel-space, var(--juno-space-12));
|
|
561
759
|
overflow-x: auto;
|
|
562
760
|
overscroll-behavior-inline: contain;
|
|
563
|
-
scroll-snap-type: inline mandatory;
|
|
761
|
+
scroll-snap-type: var(--juno-scroller-snap, inline mandatory);
|
|
564
762
|
}
|
|
565
763
|
.juno-reel > * { scroll-snap-align: start; flex: 0 0 auto; }
|
|
566
764
|
|
|
765
|
+
/* App shell — the standard product frame, as classes instead of the
|
|
766
|
+
copy-pasted \3c style> block every consumer used to hand-roll. A rail beside
|
|
767
|
+
a body column; the body holds an optional topbar, a scrolling main region,
|
|
768
|
+
and a dock/pillbar pinned at its foot. The MAIN region is the scroller
|
|
769
|
+
(not the page), so the dock never needs sticky/fixed to stay put and the
|
|
770
|
+
mobile address bar can't clip it. Height is 100dvh (dynamic viewport) so
|
|
771
|
+
it fills the visible area under shrinking browser chrome. Pairs with the
|
|
772
|
+
viewport helpers below to swap rail↔dock across md; see docs/layout.md#app-shell.
|
|
773
|
+
<div class="juno-app-shell">
|
|
774
|
+
<nav class="juno-rail juno-rail--responsive" aria-label="Primary">…</nav>
|
|
775
|
+
<div class="juno-app-shell__body">
|
|
776
|
+
<header class="juno-app-shell__topbar">…</header>
|
|
777
|
+
<main class="juno-app-shell__main">…</main>
|
|
778
|
+
<nav class="juno-dock juno-hide-from-md" aria-label="Primary">…</nav>
|
|
779
|
+
</div>
|
|
780
|
+
</div> */
|
|
781
|
+
.juno-app-shell {
|
|
782
|
+
display: flex;
|
|
783
|
+
block-size: 100dvh;
|
|
784
|
+
overflow: hidden; /* the main region scrolls, not the shell */
|
|
785
|
+
padding-inline: env(safe-area-inset-left, 0) env(safe-area-inset-right, 0);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.juno-app-shell__body {
|
|
789
|
+
flex: 1;
|
|
790
|
+
min-inline-size: 0;
|
|
791
|
+
display: flex;
|
|
792
|
+
flex-direction: column;
|
|
793
|
+
min-block-size: 0; /* let __main shrink and scroll instead of the shell */
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.juno-app-shell__topbar {
|
|
797
|
+
flex-shrink: 0;
|
|
798
|
+
display: flex;
|
|
799
|
+
align-items: center;
|
|
800
|
+
gap: var(--juno-space-12);
|
|
801
|
+
min-block-size: var(--juno-app-shell-topbar-size, 46px);
|
|
802
|
+
padding-inline: var(--juno-pad-surface-inline);
|
|
803
|
+
padding-block-start: env(safe-area-inset-top, 0);
|
|
804
|
+
background: var(--juno-s1);
|
|
805
|
+
border-block-end: var(--juno-border-width-1) solid var(--juno-border);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.juno-app-shell__main {
|
|
809
|
+
flex: 1;
|
|
810
|
+
min-block-size: 0;
|
|
811
|
+
overflow: auto;
|
|
812
|
+
overscroll-behavior: contain;
|
|
813
|
+
padding: var(--juno-pad-surface-block) var(--juno-pad-surface-inline);
|
|
814
|
+
}
|
|
815
|
+
|
|
567
816
|
/* Hide/show by viewport breakpoint — the rare case an intrinsic primitive
|
|
568
|
-
can't express (e.g. swap a nav for a menu button).
|
|
569
|
-
|
|
817
|
+
can't express (e.g. swap a nav for a menu button). `hide-below-X` hides
|
|
818
|
+
under breakpoint X; `hide-from-X` hides at X and up. `show-*` are the
|
|
819
|
+
readable inverse aliases (show-from-X ≡ hide-below-X, show-below-X ≡
|
|
820
|
+
hide-from-X) — same media rule, name whichever reads clearer at the site. */
|
|
821
|
+
@media (width <= 639.98px) { .juno-hide-below-sm, .juno-show-from-sm { display: none !important; } }
|
|
822
|
+
|
|
823
|
+
@media (width >= 640px) { .juno-hide-from-sm, .juno-show-below-sm { display: none !important; } }
|
|
824
|
+
|
|
825
|
+
@media (width <= 767.98px) { .juno-hide-below-md, .juno-show-from-md { display: none !important; } }
|
|
826
|
+
|
|
827
|
+
@media (width >= 768px) { .juno-hide-from-md, .juno-show-below-md { display: none !important; } }
|
|
828
|
+
|
|
829
|
+
@media (width <= 1023.98px) { .juno-hide-below-lg, .juno-show-from-lg { display: none !important; } }
|
|
570
830
|
|
|
571
|
-
@media (width >=
|
|
831
|
+
@media (width >= 1024px) { .juno-hide-from-lg, .juno-show-below-lg { display: none !important; } }
|
|
572
832
|
|
|
573
833
|
/* ════════════════════════════════════════════════════════════════════
|
|
574
834
|
* junoui — density layer
|
|
@@ -611,6 +871,28 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
611
871
|
--juno-gap-content: var(--juno-space-4);
|
|
612
872
|
}
|
|
613
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
|
+
|
|
614
896
|
/* ════════════════════════════════════════════════════════════════════
|
|
615
897
|
* junoui — type-scale layer
|
|
616
898
|
* One multiplier, --juno-font-scale, scales every --juno-font-size-*
|
|
@@ -1401,6 +1683,14 @@ button.juno-chip {
|
|
|
1401
1683
|
.juno-dock {
|
|
1402
1684
|
--juno-role: var(--juno-active);
|
|
1403
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
|
+
|
|
1404
1694
|
position: sticky;
|
|
1405
1695
|
inset-block-end: 0;
|
|
1406
1696
|
z-index: var(--juno-z-raised);
|
|
@@ -1409,6 +1699,9 @@ button.juno-chip {
|
|
|
1409
1699
|
background: var(--juno-s1);
|
|
1410
1700
|
border-block-start: var(--juno-border-width-1) solid var(--juno-border);
|
|
1411
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);
|
|
1412
1705
|
}
|
|
1413
1706
|
|
|
1414
1707
|
.juno-dock__item {
|
|
@@ -1421,6 +1714,13 @@ button.juno-chip {
|
|
|
1421
1714
|
justify-content: center;
|
|
1422
1715
|
gap: var(--juno-space-2);
|
|
1423
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;
|
|
1424
1724
|
border-block-start: var(--juno-border-width-2) solid transparent;
|
|
1425
1725
|
font-family: var(--juno-font-family-sans);
|
|
1426
1726
|
font-size: var(--juno-font-size-10);
|
|
@@ -1453,6 +1753,323 @@ button.juno-chip {
|
|
|
1453
1753
|
outline-offset: calc(-1 * var(--juno-space-2));
|
|
1454
1754
|
}
|
|
1455
1755
|
|
|
1756
|
+
/* Fixed — same story as the pillbar: sticky only pins while the column
|
|
1757
|
+
overflows, so on a short page the dock lands mid-content. Pin to the
|
|
1758
|
+
viewport foot in a page-scroll shell where content height varies. Since
|
|
1759
|
+
fixed leaves the flow, reserve its height at the page foot (e.g.
|
|
1760
|
+
padding-block-end) so it doesn't cover the last row. In-flow
|
|
1761
|
+
.juno-app-shell (main is the scroller, dock at the body foot) needs none
|
|
1762
|
+
of this — prefer it. */
|
|
1763
|
+
.juno-dock--fixed {
|
|
1764
|
+
position: fixed;
|
|
1765
|
+
inset-inline: 0;
|
|
1766
|
+
inset-block-end: 0;
|
|
1767
|
+
}
|
|
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
|
+
|
|
1456
2073
|
/* ════════════════════════════════════════════════════════════════════
|
|
1457
2074
|
* Component — Status dot + label
|
|
1458
2075
|
* Usage:
|
|
@@ -1501,6 +2118,12 @@ button.juno-chip {
|
|
|
1501
2118
|
* Reuses .juno-modal for the surface + scrim; this file re-pins + re-slides.
|
|
1502
2119
|
* Selectors are compound (.juno-modal.juno-drawer) so they override the
|
|
1503
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.
|
|
1504
2127
|
* ════════════════════════════════════════════════════════════════════ */
|
|
1505
2128
|
|
|
1506
2129
|
.juno-modal.juno-drawer {
|
|
@@ -1546,15 +2169,24 @@ button.juno-chip {
|
|
|
1546
2169
|
.juno-modal.juno-drawer--bottom {
|
|
1547
2170
|
inline-size: 100%;
|
|
1548
2171
|
max-inline-size: 100%;
|
|
1549
|
-
|
|
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);
|
|
1550
2179
|
margin-block: auto 0;
|
|
1551
2180
|
margin-inline: 0;
|
|
1552
2181
|
border: none;
|
|
1553
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);
|
|
1554
2185
|
transform: translateY(100%);
|
|
1555
2186
|
|
|
1556
|
-
/*
|
|
1557
|
-
|
|
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;
|
|
1558
2190
|
}
|
|
1559
2191
|
|
|
1560
2192
|
.juno-modal.juno-drawer--bottom[open] { transform: translateY(0); }
|
|
@@ -1563,6 +2195,28 @@ button.juno-chip {
|
|
|
1563
2195
|
.juno-modal.juno-drawer--bottom[open] { transform: translateY(100%); }
|
|
1564
2196
|
}
|
|
1565
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
|
+
|
|
1566
2220
|
/* ── narrow viewports ────────────────────────────────────────────────────
|
|
1567
2221
|
Side drawers cap below the full width so a sliver of scrim survives —
|
|
1568
2222
|
the visual cue (and tap target) that "outside" closes the panel. */
|
|
@@ -1608,6 +2262,51 @@ button.juno-chip {
|
|
|
1608
2262
|
|
|
1609
2263
|
.juno-field__error { font-size: var(--juno-font-size-11); color: var(--juno-warning); }
|
|
1610
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
|
+
|
|
1611
2310
|
/* ════════════════════════════════════════════════════════════════════
|
|
1612
2311
|
* Component — Gauge (determinate metric ring)
|
|
1613
2312
|
* A fixed-value ring for readouts (CPU / RAM / DISK …) — the metric
|
|
@@ -1687,6 +2386,74 @@ button.juno-chip {
|
|
|
1687
2386
|
}
|
|
1688
2387
|
.juno-gauge--lg .juno-gauge__value { font-size: var(--juno-font-size-20); }
|
|
1689
2388
|
|
|
2389
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
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.
|
|
2410
|
+
*
|
|
2411
|
+
* The app owns the state: add .juno-arc--indeterminate to spin, drop it
|
|
2412
|
+
* when loaded (gate the blink to first-load, not every refetch). For a
|
|
2413
|
+
* determinate ring, set --juno-progress on the .juno-arc instead.
|
|
2414
|
+
* Usage:
|
|
2415
|
+
* <a class="juno-rail__item" href="…" aria-current="page">
|
|
2416
|
+
* <span class="juno-icon-loader">
|
|
2417
|
+
* <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-squares-four" /></svg>
|
|
2418
|
+
* <span class="juno-arc juno-arc--indeterminate" role="status" aria-label="Loading"></span>
|
|
2419
|
+
* </span>
|
|
2420
|
+
* <span class="juno-rail__label">Library</span>
|
|
2421
|
+
* </a>
|
|
2422
|
+
*
|
|
2423
|
+
* Gotcha encoded here: the arc's rotate animation overrides `transform`, so
|
|
2424
|
+
* the ring is centred with a single-cell grid — never translate(-50%,-50%),
|
|
2425
|
+
* which the spin would clobber.
|
|
2426
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
2427
|
+
|
|
2428
|
+
.juno-icon-loader {
|
|
2429
|
+
--juno-role: var(--juno-active);
|
|
2430
|
+
|
|
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. */
|
|
2437
|
+
display: inline-grid;
|
|
2438
|
+
place-items: center;
|
|
2439
|
+
line-height: 0; /* drop the inline strut so the box hugs the ring */
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
/* every child shares the one cell; the ringed content paints above the ring */
|
|
2443
|
+
.juno-icon-loader > * {
|
|
2444
|
+
grid-area: 1 / 1;
|
|
2445
|
+
z-index: 1;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
/* the ring: sized from the two custom props, transparent to clicks */
|
|
2449
|
+
.juno-icon-loader > .juno-arc {
|
|
2450
|
+
--juno-arc-size: var(--juno-icon-loader-ring, 1.9em);
|
|
2451
|
+
--juno-arc-width: var(--juno-icon-loader-ring-width, 0.14em);
|
|
2452
|
+
|
|
2453
|
+
z-index: 0;
|
|
2454
|
+
pointer-events: none;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
1690
2457
|
/* ════════════════════════════════════════════════════════════════════
|
|
1691
2458
|
* Component — Icon · inline SVG glyph
|
|
1692
2459
|
* A zero-JS icon primitive. Reference a symbol from the sprite; the glyph
|
|
@@ -1705,11 +2472,13 @@ button.juno-chip {
|
|
|
1705
2472
|
* ════════════════════════════════════════════════════════════════════ */
|
|
1706
2473
|
|
|
1707
2474
|
.juno-icon {
|
|
1708
|
-
|
|
1709
|
-
|
|
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. */
|
|
1710
2479
|
display: inline-block;
|
|
1711
|
-
inline-size: var(--juno-icon-size);
|
|
1712
|
-
block-size: var(--juno-icon-size);
|
|
2480
|
+
inline-size: var(--juno-icon-size, 1.25em);
|
|
2481
|
+
block-size: var(--juno-icon-size, 1.25em);
|
|
1713
2482
|
flex-shrink: 0;
|
|
1714
2483
|
vertical-align: -0.2em; /* optical baseline alignment next to text */
|
|
1715
2484
|
fill: currentcolor;
|
|
@@ -1920,6 +2689,143 @@ button.juno-list__row:disabled {
|
|
|
1920
2689
|
color: var(--juno-muted);
|
|
1921
2690
|
}
|
|
1922
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
|
+
|
|
1923
2829
|
/* ════════════════════════════════════════════════════════════════════
|
|
1924
2830
|
* Component — Loaders (Arc · Beacon · Linear bar)
|
|
1925
2831
|
*
|
|
@@ -1963,6 +2869,12 @@ button.juno-list__row:disabled {
|
|
|
1963
2869
|
animation: juno-arc-rot 1.1s steps(12) infinite; /* mechanical, 12-step sweep */
|
|
1964
2870
|
}
|
|
1965
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
|
+
|
|
1966
2878
|
/* centred readout for determinate arcs — place a span inside */
|
|
1967
2879
|
.juno-arc__label {
|
|
1968
2880
|
position: absolute;
|
|
@@ -2147,6 +3059,10 @@ button.juno-list__row:disabled {
|
|
|
2147
3059
|
align-items: center;
|
|
2148
3060
|
gap: var(--juno-gap-control);
|
|
2149
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);
|
|
2150
3066
|
padding: var(--juno-space-8) var(--juno-space-10);
|
|
2151
3067
|
background: transparent;
|
|
2152
3068
|
border: none;
|
|
@@ -2242,6 +3158,11 @@ button.juno-list__row:disabled {
|
|
|
2242
3158
|
}
|
|
2243
3159
|
|
|
2244
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;
|
|
2245
3166
|
opacity: 1;
|
|
2246
3167
|
transform: translateY(0) scale(1);
|
|
2247
3168
|
}
|
|
@@ -2257,6 +3178,7 @@ button.juno-list__row:disabled {
|
|
|
2257
3178
|
.juno-modal::backdrop {
|
|
2258
3179
|
background: rgb(0 0 0);
|
|
2259
3180
|
opacity: var(--juno-opacity-scrim);
|
|
3181
|
+
-webkit-backdrop-filter: blur(1.5px);
|
|
2260
3182
|
backdrop-filter: blur(1.5px);
|
|
2261
3183
|
}
|
|
2262
3184
|
|
|
@@ -2297,7 +3219,19 @@ button.juno-list__row:disabled {
|
|
|
2297
3219
|
|
|
2298
3220
|
.juno-modal__close:hover { background: var(--juno-s2); color: var(--juno-data); }
|
|
2299
3221
|
|
|
2300
|
-
.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
|
+
}
|
|
2301
3235
|
|
|
2302
3236
|
.juno-modal__title {
|
|
2303
3237
|
font-family: var(--juno-font-family-sans);
|
|
@@ -2348,9 +3282,12 @@ button.juno-list__row:disabled {
|
|
|
2348
3282
|
.juno-modal:not(.juno-drawer)[open] { transform: translateY(100%); }
|
|
2349
3283
|
}
|
|
2350
3284
|
|
|
2351
|
-
/* 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. */
|
|
2352
3289
|
.juno-modal:not(.juno-drawer) .juno-modal__body {
|
|
2353
|
-
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));
|
|
2354
3291
|
}
|
|
2355
3292
|
|
|
2356
3293
|
/* full-width actions read better on a sheet */
|
|
@@ -2375,7 +3312,7 @@ button.juno-list__row:disabled {
|
|
|
2375
3312
|
* </a>
|
|
2376
3313
|
* <h1 class="juno-navbar__title">Album</h1>
|
|
2377
3314
|
* <div class="juno-navbar__actions">
|
|
2378
|
-
* <button class="juno-btn juno-btn--
|
|
3315
|
+
* <button class="juno-btn juno-btn--ghost">EDIT</button>
|
|
2379
3316
|
* </div>
|
|
2380
3317
|
* </header>
|
|
2381
3318
|
* ════════════════════════════════════════════════════════════════════ */
|
|
@@ -2456,6 +3393,10 @@ button.juno-list__row:disabled {
|
|
|
2456
3393
|
gap: var(--juno-space-4);
|
|
2457
3394
|
}
|
|
2458
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
|
+
|
|
2459
3400
|
.juno-navbar__back:focus-visible {
|
|
2460
3401
|
outline: var(--juno-border-width-2) solid var(--juno-active);
|
|
2461
3402
|
outline-offset: calc(-1 * var(--juno-space-2));
|
|
@@ -2555,23 +3496,38 @@ button.juno-list__row:disabled {
|
|
|
2555
3496
|
* <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-arrows-clockwise" /></svg>
|
|
2556
3497
|
* </button>
|
|
2557
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.
|
|
2558
3503
|
* ════════════════════════════════════════════════════════════════════ */
|
|
2559
3504
|
|
|
2560
3505
|
.juno-pillbar {
|
|
2561
3506
|
--juno-role: var(--juno-active);
|
|
2562
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
|
+
|
|
2563
3518
|
position: sticky;
|
|
2564
|
-
inset-block-end: calc(var(--juno-
|
|
3519
|
+
inset-block-end: calc(var(--juno-pillbar-edge) + env(safe-area-inset-bottom, 0px));
|
|
2565
3520
|
z-index: var(--juno-z-raised);
|
|
2566
3521
|
margin-inline: auto;
|
|
2567
|
-
margin-block-end: var(--juno-
|
|
3522
|
+
margin-block-end: var(--juno-pillbar-edge);
|
|
2568
3523
|
inline-size: fit-content;
|
|
2569
|
-
max-inline-size: calc(100% - 2 * var(--juno-
|
|
3524
|
+
max-inline-size: calc(100% - 2 * var(--juno-pillbar-edge));
|
|
2570
3525
|
display: flex;
|
|
2571
3526
|
align-items: center;
|
|
2572
|
-
gap: var(--juno-
|
|
2573
|
-
padding: var(--juno-
|
|
3527
|
+
gap: var(--juno-pillbar-gap);
|
|
3528
|
+
padding: var(--juno-pillbar-pad);
|
|
2574
3529
|
background: color-mix(in srgb, var(--juno-s1) 88%, transparent);
|
|
3530
|
+
-webkit-backdrop-filter: blur(12px);
|
|
2575
3531
|
backdrop-filter: blur(12px);
|
|
2576
3532
|
border: var(--juno-border-width-1) solid var(--juno-border);
|
|
2577
3533
|
border-radius: 999px;
|
|
@@ -2582,8 +3538,8 @@ button.juno-list__row:disabled {
|
|
|
2582
3538
|
appearance: none;
|
|
2583
3539
|
margin: 0;
|
|
2584
3540
|
border: none;
|
|
2585
|
-
min-inline-size: var(--juno-
|
|
2586
|
-
min-block-size: var(--juno-
|
|
3541
|
+
min-inline-size: var(--juno-pillbar-item);
|
|
3542
|
+
min-block-size: var(--juno-pillbar-item);
|
|
2587
3543
|
display: inline-flex;
|
|
2588
3544
|
align-items: center;
|
|
2589
3545
|
justify-content: center;
|
|
@@ -2630,6 +3586,54 @@ button.juno-list__row:disabled {
|
|
|
2630
3586
|
cursor: not-allowed;
|
|
2631
3587
|
}
|
|
2632
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
|
+
|
|
2633
3637
|
.juno-pillbar__sep {
|
|
2634
3638
|
align-self: center;
|
|
2635
3639
|
inline-size: var(--juno-border-width-1);
|
|
@@ -2638,10 +3642,162 @@ button.juno-list__row:disabled {
|
|
|
2638
3642
|
flex-shrink: 0;
|
|
2639
3643
|
}
|
|
2640
3644
|
|
|
3645
|
+
/* Fixed — pin to the viewport instead of flowing at the column's end.
|
|
3646
|
+
Sticky only pins while the column overflows; on a short (non-scrolling)
|
|
3647
|
+
page the sticky pillbar lands mid-content. Use --fixed in a page-scroll
|
|
3648
|
+
shell where content height varies. Centered via inset-inline:0 + the base
|
|
3649
|
+
margin-inline:auto (NOT translateX, which the app-shell model avoids
|
|
3650
|
+
anyway). Leaves the viewport foot free — reserve space so it doesn't cover
|
|
3651
|
+
the last row (e.g. padding-block-end on the scroll region). Prefer the
|
|
3652
|
+
in-flow .juno-app-shell (main scrolls, pillbar sits at the body foot) —
|
|
3653
|
+
that needs no fixed at all. */
|
|
3654
|
+
.juno-pillbar--fixed {
|
|
3655
|
+
position: fixed;
|
|
3656
|
+
inset-inline: 0;
|
|
3657
|
+
z-index: var(--juno-z-anchored);
|
|
3658
|
+
}
|
|
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
|
+
|
|
2641
3796
|
/* reduced-transparency preference → solid surface, no blur */
|
|
2642
3797
|
@media (prefers-reduced-transparency: reduce) {
|
|
2643
3798
|
.juno-pillbar {
|
|
2644
3799
|
background: var(--juno-s1);
|
|
3800
|
+
-webkit-backdrop-filter: none;
|
|
2645
3801
|
backdrop-filter: none;
|
|
2646
3802
|
}
|
|
2647
3803
|
}
|
|
@@ -2817,6 +3973,14 @@ button.juno-list__row:disabled {
|
|
|
2817
3973
|
padding-inline: 0;
|
|
2818
3974
|
}
|
|
2819
3975
|
|
|
3976
|
+
/* Responsive — the rail self-hides below md, so you pair it with a
|
|
3977
|
+
.juno-dock (or .juno-pillbar) carrying .juno-hide-from-md and get the
|
|
3978
|
+
rail↔dock swap without hanging a juno-hide-below-md on the rail yourself.
|
|
3979
|
+
Reciprocal of the dock's md pairing; see dock.css / docs/layout.md#app-shell. */
|
|
3980
|
+
@media (width <= 767.98px) {
|
|
3981
|
+
.juno-rail--responsive { display: none !important; }
|
|
3982
|
+
}
|
|
3983
|
+
|
|
2820
3984
|
/* ════════════════════════════════════════════════════════════════════
|
|
2821
3985
|
* Component — Data readout tile
|
|
2822
3986
|
* Usage:
|
|
@@ -2863,6 +4027,55 @@ button.juno-list__row:disabled {
|
|
|
2863
4027
|
margin-top: var(--juno-space-4);
|
|
2864
4028
|
}
|
|
2865
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
|
+
|
|
2866
4079
|
/* ════════════════════════════════════════════════════════════════════
|
|
2867
4080
|
* Component — Segmented control
|
|
2868
4081
|
* Exclusive-choice pill row (filters, mode switchers, sort pickers).
|
|
@@ -3006,14 +4219,20 @@ button.juno-seg__opt:disabled {
|
|
|
3006
4219
|
* Component — Skeleton · loading placeholder
|
|
3007
4220
|
* A shimmering block that stands in for content not yet loaded. Same
|
|
3008
4221
|
* shimmer language as the table's row skeleton, generalized: text lines,
|
|
3009
|
-
* blocks,
|
|
3010
|
-
* --juno-skeleton-h
|
|
3011
|
-
*
|
|
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.
|
|
3012
4230
|
* Usage:
|
|
3013
4231
|
* <div aria-busy="true" aria-live="polite">
|
|
3014
4232
|
* <span class="juno-skeleton juno-skeleton--circle" style="--juno-skeleton-h: 48px;"></span>
|
|
3015
4233
|
* <span class="juno-skeleton juno-skeleton--text" style="inline-size: 60%;"></span>
|
|
3016
4234
|
* <span class="juno-skeleton juno-skeleton--text"></span>
|
|
4235
|
+
* <span class="juno-skeleton juno-skeleton--tile"></span>
|
|
3017
4236
|
* </div>
|
|
3018
4237
|
* ════════════════════════════════════════════════════════════════════ */
|
|
3019
4238
|
|
|
@@ -3021,11 +4240,27 @@ button.juno-seg__opt:disabled {
|
|
|
3021
4240
|
--juno-skeleton-h: var(--juno-space-16);
|
|
3022
4241
|
|
|
3023
4242
|
display: block;
|
|
4243
|
+
position: relative;
|
|
4244
|
+
overflow: hidden;
|
|
3024
4245
|
inline-size: 100%;
|
|
3025
4246
|
block-size: var(--juno-skeleton-h);
|
|
3026
4247
|
border-radius: var(--juno-radius-3);
|
|
3027
|
-
background:
|
|
3028
|
-
|
|
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
|
+
);
|
|
3029
4264
|
animation: juno-skeleton-shimmer 1.2s ease-in-out infinite;
|
|
3030
4265
|
}
|
|
3031
4266
|
|
|
@@ -3049,19 +4284,25 @@ button.juno-seg__opt:disabled {
|
|
|
3049
4284
|
border-radius: var(--juno-radius-4);
|
|
3050
4285
|
}
|
|
3051
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
|
+
|
|
3052
4299
|
@keyframes juno-skeleton-shimmer {
|
|
3053
4300
|
from {
|
|
3054
|
-
|
|
4301
|
+
transform: translateX(-100%);
|
|
3055
4302
|
}
|
|
3056
4303
|
|
|
3057
4304
|
to {
|
|
3058
|
-
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
|
|
3062
|
-
@media (prefers-reduced-motion: reduce) {
|
|
3063
|
-
.juno-skeleton {
|
|
3064
|
-
animation: none;
|
|
4305
|
+
transform: translateX(100%);
|
|
3065
4306
|
}
|
|
3066
4307
|
}
|
|
3067
4308
|
|
|
@@ -3939,6 +5180,11 @@ button.juno-seg__opt:disabled {
|
|
|
3939
5180
|
scrollbar-width: none;
|
|
3940
5181
|
}
|
|
3941
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
|
+
|
|
3942
5188
|
.juno-tabs__tab {
|
|
3943
5189
|
appearance: none;
|
|
3944
5190
|
margin: 0;
|
|
@@ -3986,10 +5232,22 @@ button.juno-seg__opt:disabled {
|
|
|
3986
5232
|
* <figure class="juno-thumb" style="aspect-ratio: 1">
|
|
3987
5233
|
* <img src="…" alt="Sunset clip" onerror="this.remove()" />
|
|
3988
5234
|
* </figure>
|
|
3989
|
-
*
|
|
5235
|
+
* \3c !-- known-missing: ship no <img>; label the figure instead -->
|
|
3990
5236
|
* <figure class="juno-thumb juno-thumb--video" role="img" aria-label="Preview unavailable"></figure>
|
|
3991
5237
|
* The onerror one-liner is the whole JS contract (optional, stateless):
|
|
3992
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>
|
|
3993
5251
|
* ════════════════════════════════════════════════════════════════════ */
|
|
3994
5252
|
|
|
3995
5253
|
.juno-thumb {
|
|
@@ -4000,12 +5258,30 @@ button.juno-seg__opt:disabled {
|
|
|
4000
5258
|
justify-content: center;
|
|
4001
5259
|
gap: var(--juno-space-4);
|
|
4002
5260
|
margin: 0;
|
|
5261
|
+
aspect-ratio: var(--juno-thumb-ratio, 1);
|
|
4003
5262
|
overflow: hidden;
|
|
4004
5263
|
background: var(--juno-s2);
|
|
4005
5264
|
border: var(--juno-border-width-1) solid var(--juno-border);
|
|
4006
5265
|
border-radius: var(--juno-radius-3);
|
|
4007
5266
|
}
|
|
4008
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
|
+
|
|
4009
5285
|
/* placeholder glyph (Phosphor "images", masked so it takes any color) */
|
|
4010
5286
|
.juno-thumb::before {
|
|
4011
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");
|
|
@@ -4045,6 +5321,38 @@ button.juno-seg__opt:disabled {
|
|
|
4045
5321
|
object-fit: cover;
|
|
4046
5322
|
}
|
|
4047
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
|
+
|
|
4048
5356
|
/* ════════════════════════════════════════════════════════════════════
|
|
4049
5357
|
* Component — Toast / snackbar
|
|
4050
5358
|
* A transient, floating notification. Same role-rail language as the
|
|
@@ -4138,7 +5446,11 @@ button.juno-seg__opt:disabled {
|
|
|
4138
5446
|
@media (width <= 639.98px) {
|
|
4139
5447
|
.juno-toast-stack {
|
|
4140
5448
|
inset-inline: var(--juno-space-12);
|
|
4141
|
-
|
|
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));
|
|
4142
5454
|
inline-size: auto;
|
|
4143
5455
|
}
|
|
4144
5456
|
|
|
@@ -4445,8 +5757,11 @@ button.juno-seg__opt:disabled {
|
|
|
4445
5757
|
font-variant-numeric: tabular-nums;
|
|
4446
5758
|
}
|
|
4447
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. */
|
|
4448
5763
|
.juno-label {
|
|
4449
|
-
font-size: var(--juno-font-size-13);
|
|
5764
|
+
font-size: var(--juno-label-size, var(--juno-font-size-13));
|
|
4450
5765
|
color: var(--juno-label);
|
|
4451
5766
|
}
|
|
4452
5767
|
|