@junoput01/junoui 0.6.0 → 0.8.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 +209 -0
- package/README.md +17 -16
- package/dist/android/dimens.xml +2 -0
- package/dist/classes.json +1774 -0
- package/dist/css/juno-tokens.css +10 -0
- package/dist/css/juno.css +1057 -56
- package/dist/flutter/juno_tokens.dart +10 -0
- package/dist/ios/JunoTokens.swift +10 -0
- package/dist/js/tokens.js +16 -0
- package/dist/json/tokens.json +51 -0
- package/dist/rust/juno_tokens.rs +383 -0
- package/dist/scss/_juno-tokens.scss +10 -0
- package/docs/accessibility.md +6 -0
- package/docs/browser-support.md +3 -0
- package/docs/components/button.md +11 -2
- package/docs/components/canvas-ink.md +71 -0
- package/docs/components/dock.md +34 -0
- package/docs/components/fold-slot.md +26 -1
- package/docs/components/gizmo.md +114 -0
- package/docs/components/swatch.md +95 -0
- package/docs/components/tree.md +112 -0
- package/docs/conformance-kit.md +255 -0
- package/docs/getting-started.md +14 -0
- package/docs/integration.md +52 -6
- package/docs/ios-conformance.md +200 -3
- package/docs/ios-pwa.md +273 -0
- package/docs/native.md +38 -1
- package/docs/tokens-reference.md +15 -0
- package/package.json +8 -2
- package/src/css/base.css +35 -43
- package/src/css/components/button.css +42 -2
- package/src/css/components/canvas-ink.css +97 -0
- package/src/css/components/dock.css +65 -6
- package/src/css/components/fold-slot.css +49 -3
- package/src/css/components/gizmo.css +238 -0
- package/src/css/components/segmented.css +15 -2
- package/src/css/components/swatch.css +187 -0
- package/src/css/components/tree.css +259 -0
- package/src/css/touch-surfaces.mjs +95 -0
- package/tools/gizmo.mjs +144 -0
- package/tools/testing.mjs +177 -0
- package/tools/tree.mjs +178 -0
package/dist/css/juno.css
CHANGED
|
@@ -21,6 +21,16 @@
|
|
|
21
21
|
--juno-shadow-1: 0 1px 2px rgb(0 0 0 / 0.30);
|
|
22
22
|
--juno-shadow-2: 0 4px 14px rgb(0 0 0 / 0.35);
|
|
23
23
|
--juno-shadow-3: 0 12px 32px rgb(0 0 0 / 0.50);
|
|
24
|
+
--juno-ink-canvas-ink: #FFFFFF;
|
|
25
|
+
--juno-ink-canvas-halo: #000000;
|
|
26
|
+
--juno-ink-canvas-halo-width: 2px;
|
|
27
|
+
--juno-ink-canvas-halo-width-lg: 3px;
|
|
28
|
+
--juno-ink-canvas-scrim: 0.28;
|
|
29
|
+
--juno-ink-vivid-nominal: oklch(76% 0.28 148);
|
|
30
|
+
--juno-ink-vivid-active: oklch(76% 0.24 205);
|
|
31
|
+
--juno-ink-vivid-target: oklch(70% 0.30 328);
|
|
32
|
+
--juno-ink-vivid-caution: oklch(82% 0.20 82);
|
|
33
|
+
--juno-ink-vivid-warning: oklch(68% 0.28 25);
|
|
24
34
|
--juno-motion-duration-instant: 80ms;
|
|
25
35
|
--juno-motion-duration-quick: 140ms;
|
|
26
36
|
--juno-motion-duration-base: 200ms;
|
|
@@ -398,9 +408,30 @@
|
|
|
398
408
|
var(--juno-size-tap-comfortable) + 4 * var(--juno-space-4) + 2 * var(--juno-border-width-1)
|
|
399
409
|
);
|
|
400
410
|
--juno-dock-clearance-scale: 1;
|
|
411
|
+
|
|
412
|
+
/* The bar's own offset from the bottom edge — declared ONCE and consumed by
|
|
413
|
+
both the bar's margin (dock.css) and the clearance below, so the two cannot
|
|
414
|
+
disagree about where the bar sits.
|
|
415
|
+
|
|
416
|
+
That is the whole reason this is a token rather than a literal in each
|
|
417
|
+
place. junoui's default is additive, per docs/ios-conformance.md: a
|
|
418
|
+
FLOATING element sits off the edge, so its gap and the inset stack. A
|
|
419
|
+
consumer whose design wants the bar flush above the home indicator instead
|
|
420
|
+
writes `max(8px, env(safe-area-inset-bottom, 0px))` here and BOTH the
|
|
421
|
+
margin and the reservation follow it. Before this token the form was baked
|
|
422
|
+
into each site separately, so a consumer that changed one silently
|
|
423
|
+
disagreed with the other — measured at 16px of dead band at inset 0 and
|
|
424
|
+
24px at inset 34 (20260815-055), and no single --juno-dock-h could
|
|
425
|
+
reconcile them because one side added the inset and the other maxed it. */
|
|
426
|
+
--juno-dock-edge-offset: calc(var(--juno-space-16) + env(safe-area-inset-bottom, 0px));
|
|
427
|
+
|
|
428
|
+
/* Gap between the bar's top edge and the last row of content. Zero is a
|
|
429
|
+
legitimate setting: a translucent bar that content is MEANT to scroll under
|
|
430
|
+
wants overlap, not breathing room. */
|
|
431
|
+
--juno-dock-clearance-breathing: var(--juno-space-8);
|
|
401
432
|
--juno-dock-clearance: calc(
|
|
402
|
-
var(--juno-dock-h) * var(--juno-dock-clearance-scale) + var(--juno-
|
|
403
|
-
var(--juno-
|
|
433
|
+
var(--juno-dock-h) * var(--juno-dock-clearance-scale) + var(--juno-dock-edge-offset) +
|
|
434
|
+
var(--juno-dock-clearance-breathing)
|
|
404
435
|
);
|
|
405
436
|
|
|
406
437
|
/* The pillbar is the same shape one size down: no bubble, so its height is
|
|
@@ -409,8 +440,8 @@
|
|
|
409
440
|
var(--juno-size-tap-comfortable) + 2 * var(--juno-space-4) + 2 * var(--juno-border-width-1)
|
|
410
441
|
);
|
|
411
442
|
--juno-pillbar-clearance: calc(
|
|
412
|
-
var(--juno-pillbar-h) * var(--juno-dock-clearance-scale) + var(--juno-
|
|
413
|
-
var(--juno-
|
|
443
|
+
var(--juno-pillbar-h) * var(--juno-dock-clearance-scale) + var(--juno-dock-edge-offset) +
|
|
444
|
+
var(--juno-dock-clearance-breathing)
|
|
414
445
|
);
|
|
415
446
|
|
|
416
447
|
/* JS-readable motion contract. CSS-only `prefers-reduced-motion` never reaches
|
|
@@ -519,47 +550,18 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
519
550
|
`.juno-input` font-size later in the bundle. It did, silently, until
|
|
520
551
|
20260815-006's coarse-pointer project measured it. */
|
|
521
552
|
|
|
522
|
-
/*
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
.juno-btn,
|
|
527
|
-
.juno-dock__item,
|
|
528
|
-
.juno-pillbar__item,
|
|
529
|
-
.juno-tabs__tab,
|
|
530
|
-
.juno-list__item,
|
|
531
|
-
.juno-menu__item
|
|
532
|
-
) {
|
|
533
|
-
-webkit-tap-highlight-color: transparent;
|
|
534
|
-
}
|
|
553
|
+
/* The tap-highlight default that used to live here is GENERATED now, from
|
|
554
|
+
src/css/touch-surfaces.mjs, together with the touch-action default that
|
|
555
|
+
shares its member list. Two hand-maintained lists had drifted from the
|
|
556
|
+
classes and from each other; see that file. */
|
|
535
557
|
}
|
|
536
558
|
|
|
537
|
-
/*
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
NOT inside the pointer:coarse block above: a hybrid device (touch laptop,
|
|
544
|
-
iPad with a trackpad) reports a fine primary pointer while still taking
|
|
545
|
-
touch input, and the property is inert on a mouse anyway.
|
|
546
|
-
Community convention — no primary Apple/WebKit source names it; see
|
|
547
|
-
docs/ios-conformance.md. Named components only, so a consumer's own elements
|
|
548
|
-
are untouched. See 20260803-038. */
|
|
549
|
-
:where(
|
|
550
|
-
.juno-btn,
|
|
551
|
-
.juno-dock__item,
|
|
552
|
-
.juno-pillbar__item,
|
|
553
|
-
.juno-pillbar__overflow,
|
|
554
|
-
.juno-tabs__tab,
|
|
555
|
-
.juno-list__item,
|
|
556
|
-
.juno-menu__item,
|
|
557
|
-
.juno-seg__option,
|
|
558
|
-
.juno-chip,
|
|
559
|
-
.juno-toggle-btn
|
|
560
|
-
) {
|
|
561
|
-
touch-action: manipulation;
|
|
562
|
-
}
|
|
559
|
+
/* The touch defaults that used to be two hand-maintained `:where()` lists here
|
|
560
|
+
are GENERATED from src/css/touch-surfaces.mjs and emitted straight after this
|
|
561
|
+
file. Both lists had drifted — from the classes (`.juno-seg__option`,
|
|
562
|
+
`.juno-list__item`: neither exists, so `:where()` matched nothing and the
|
|
563
|
+
rule parsed anyway) and from each other. One declared set now feeds both.
|
|
564
|
+
See 20260826-024 and docs/conformance-kit.md. */
|
|
563
565
|
|
|
564
566
|
/* Gesture-owned surfaces — for an element whose pointer events are fully
|
|
565
567
|
driven by app JS (drag-pan, pinch-zoom, swipe classification: a state
|
|
@@ -699,6 +701,62 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
699
701
|
.juno-tooltip__bubble[popover] { display: none; }
|
|
700
702
|
}
|
|
701
703
|
|
|
704
|
+
/* GENERATED from src/css/touch-surfaces.mjs — do not edit here.
|
|
705
|
+
*
|
|
706
|
+
* Tappable primitives opt out of double-tap-to-zoom. A browser that still
|
|
707
|
+
* recognises that gesture has to WAIT after the first tap to see whether a
|
|
708
|
+
* second one is coming, which reads as a late, mushy tap on exactly the
|
|
709
|
+
* surfaces a phone UI is built from. `manipulation` keeps panning and
|
|
710
|
+
* pinch-zoom (so the page stays zoomable — never `none` here, that would be an
|
|
711
|
+
* a11y regression) and drops only the double-tap.
|
|
712
|
+
* Community convention — no primary Apple/WebKit source names it; see
|
|
713
|
+
* docs/ios-conformance.md. Named components only, so a consumer's own elements
|
|
714
|
+
* are untouched. See 20260803-038. */
|
|
715
|
+
:where(
|
|
716
|
+
.juno-btn,
|
|
717
|
+
.juno-chip,
|
|
718
|
+
.juno-dock__item,
|
|
719
|
+
.juno-list__row,
|
|
720
|
+
.juno-menu__item,
|
|
721
|
+
.juno-pillbar__item,
|
|
722
|
+
.juno-pillbar__overflow,
|
|
723
|
+
.juno-seg__opt,
|
|
724
|
+
.juno-tabs__tab,
|
|
725
|
+
.juno-toggle-btn,
|
|
726
|
+
.juno-tree__row,
|
|
727
|
+
.juno-gizmo__mark,
|
|
728
|
+
.juno-gizmo__center,
|
|
729
|
+
.juno-swatch--button,
|
|
730
|
+
.juno-palette__option
|
|
731
|
+
) {
|
|
732
|
+
touch-action: manipulation;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/* And on touch, kill the UA tap-highlight square so it never flashes past a
|
|
736
|
+
* rounded control. Consumers were adding this by hand per component; it is a
|
|
737
|
+
* first-class touch default. See 20260802-020. */
|
|
738
|
+
@media (pointer: coarse) {
|
|
739
|
+
:where(
|
|
740
|
+
.juno-btn,
|
|
741
|
+
.juno-chip,
|
|
742
|
+
.juno-dock__item,
|
|
743
|
+
.juno-list__row,
|
|
744
|
+
.juno-menu__item,
|
|
745
|
+
.juno-pillbar__item,
|
|
746
|
+
.juno-pillbar__overflow,
|
|
747
|
+
.juno-seg__opt,
|
|
748
|
+
.juno-tabs__tab,
|
|
749
|
+
.juno-toggle-btn,
|
|
750
|
+
.juno-tree__row,
|
|
751
|
+
.juno-gizmo__mark,
|
|
752
|
+
.juno-gizmo__center,
|
|
753
|
+
.juno-swatch--button,
|
|
754
|
+
.juno-palette__option
|
|
755
|
+
) {
|
|
756
|
+
-webkit-tap-highlight-color: transparent;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
702
760
|
/* ════════════════════════════════════════════════════════════════════
|
|
703
761
|
* junoui — layout layer (intrinsic & responsive)
|
|
704
762
|
*
|
|
@@ -1401,8 +1459,10 @@ details.juno-accordion__item:only-child {
|
|
|
1401
1459
|
.juno-btn--ghost:hover { background: var(--juno-s2); filter: none; }
|
|
1402
1460
|
|
|
1403
1461
|
/* Small — for dense desktop toolbars (40–46px bars) where the default
|
|
1404
|
-
control would not fit.
|
|
1405
|
-
|
|
1462
|
+
control would not fit. Combines with --ghost + roles.
|
|
1463
|
+
On a FINE pointer this is 24px: the WCAG 2.2 AA floor (2.5.8) exactly, and
|
|
1464
|
+
no more. On a coarse pointer it promotes to the comfortable target — see
|
|
1465
|
+
below. */
|
|
1406
1466
|
.juno-btn--sm {
|
|
1407
1467
|
min-height: var(--juno-space-24);
|
|
1408
1468
|
font-size: var(--juno-font-size-11);
|
|
@@ -1411,6 +1471,44 @@ details.juno-accordion__item:only-child {
|
|
|
1411
1471
|
border-radius: var(--juno-radius-3);
|
|
1412
1472
|
}
|
|
1413
1473
|
|
|
1474
|
+
/* --sm names a DENSITY, and consumers reach for it as a SEMANTIC. Audited
|
|
1475
|
+
across one app: 40 call sites, nearly all of them `--sm --ghost` meaning
|
|
1476
|
+
"secondary", shipping a 24px target on a phone — and junoui's own showcase
|
|
1477
|
+
does it twice in a navbar action slot. A size modifier should not quietly
|
|
1478
|
+
become a tap-target decision, so on touch it holds the same floor every
|
|
1479
|
+
other control does. Type and padding still shrink: --sm stays a density
|
|
1480
|
+
modifier, it just stops being a touch-target one (20260826-026).
|
|
1481
|
+
|
|
1482
|
+
THE RULE LIVES HERE, not in base.css's (pointer: coarse) block: a media
|
|
1483
|
+
query adds no specificity, so a `.juno-btn--sm` there would lose to this
|
|
1484
|
+
file's own `.juno-btn--sm` later in the bundle. That is not a guess — it is
|
|
1485
|
+
what silently happened to the .juno-input font floor until 20260815-006
|
|
1486
|
+
measured it.
|
|
1487
|
+
|
|
1488
|
+
--dense is the opt-out, and it is deliberately only meaningful ON --sm:
|
|
1489
|
+
a dense touch toolbar is a real thing (a media scrubber, an editor rail),
|
|
1490
|
+
but it is a decision someone has to make by name rather than inherit from a
|
|
1491
|
+
size.
|
|
1492
|
+
|
|
1493
|
+
The two rules below are REDUNDANT ON PURPOSE, and mutation testing is how
|
|
1494
|
+
that became a decision rather than an accident: deleting either one leaves
|
|
1495
|
+
--dense at 24px. The `:not()` excludes a dense button from the promotion, so
|
|
1496
|
+
the pairing below it is dead weight — until the `:not()` names the wrong
|
|
1497
|
+
class, at which point the pairing (also (0,2,0), and later in the file) is
|
|
1498
|
+
the only thing holding the opt-out. Each covers the other's failure, so a
|
|
1499
|
+
single edit to either cannot silently promote a --dense button. Both
|
|
1500
|
+
selectors are (0,2,0) and mutually exclusive in the correct state, so they
|
|
1501
|
+
never race. */
|
|
1502
|
+
@media (pointer: coarse) {
|
|
1503
|
+
.juno-btn--sm:not(.juno-btn--dense) {
|
|
1504
|
+
min-height: var(--juno-size-tap-min);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
.juno-btn--sm.juno-btn--dense {
|
|
1508
|
+
min-height: var(--juno-space-24);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1414
1512
|
.juno-btn:disabled {
|
|
1415
1513
|
background: var(--juno-muted);
|
|
1416
1514
|
color: var(--juno-label);
|
|
@@ -1419,6 +1517,104 @@ details.juno-accordion__item:only-child {
|
|
|
1419
1517
|
filter: none;
|
|
1420
1518
|
}
|
|
1421
1519
|
|
|
1520
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
1521
|
+
* Component — Canvas ink (marks drawn over arbitrary imagery)
|
|
1522
|
+
* For text and vector ink sitting on a photo, a map, a video frame or a
|
|
1523
|
+
* camera feed: anywhere the background is CONTENT rather than one of
|
|
1524
|
+
* junoui's surfaces.
|
|
1525
|
+
*
|
|
1526
|
+
* WHY IT IS NOT JUST A COLOUR. junoui's whole contrast story assumes a
|
|
1527
|
+
* controlled surface, s0 through s3 — every role colour's ratio is
|
|
1528
|
+
* computed against a known background. Over imagery there is no known
|
|
1529
|
+
* background. In one orthophoto a black shadow and a snowfield are
|
|
1530
|
+
* adjacent pixels, so no single ink colour is legible and no contrast
|
|
1531
|
+
* ratio can be asserted about one. The answer is a PAIR that spans the
|
|
1532
|
+
* luminance range: over a light backing the halo carries the contrast,
|
|
1533
|
+
* over a dark one the ink does. Neither half works alone, which is why
|
|
1534
|
+
* they are applied together by one class.
|
|
1535
|
+
*
|
|
1536
|
+
* NOT THEMED, on purpose. A satellite image does not get lighter because
|
|
1537
|
+
* the user chose light mode. Theming this pair would make it track the
|
|
1538
|
+
* app's surface, which is precisely the background it is NOT over.
|
|
1539
|
+
* Usage:
|
|
1540
|
+
* <figcaption class="juno-canvas-ink">Sector 7 · 1.2 km</figcaption>
|
|
1541
|
+
* <svg class="juno-canvas-ink"><path class="juno-canvas-ink__stroke" …/></svg>
|
|
1542
|
+
* <div class="juno-canvas-scrim">…chrome floating over the content…</div>
|
|
1543
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
1544
|
+
|
|
1545
|
+
/* Text. Four offset shadows rather than one blur: a blurred shadow fades at
|
|
1546
|
+
the glyph's corners, which is exactly where a thin stroke needs the most
|
|
1547
|
+
help, and it costs the same. `paint-order` is for SVG text, where a real
|
|
1548
|
+
stroke is available and better. */
|
|
1549
|
+
.juno-canvas-ink {
|
|
1550
|
+
color: var(--juno-ink-canvas-ink);
|
|
1551
|
+
paint-order: stroke fill;
|
|
1552
|
+
stroke: var(--juno-ink-canvas-halo);
|
|
1553
|
+
stroke-width: var(--juno-ink-canvas-halo-width);
|
|
1554
|
+
text-shadow:
|
|
1555
|
+
var(--juno-ink-canvas-halo-width) 0 0 var(--juno-ink-canvas-halo),
|
|
1556
|
+
calc(-1 * var(--juno-ink-canvas-halo-width)) 0 0 var(--juno-ink-canvas-halo),
|
|
1557
|
+
0 var(--juno-ink-canvas-halo-width) 0 var(--juno-ink-canvas-halo),
|
|
1558
|
+
0 calc(-1 * var(--juno-ink-canvas-halo-width)) 0 var(--juno-ink-canvas-halo);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/* Display sizes: a 2px halo reads as a hairline against a heavier stroke. */
|
|
1562
|
+
.juno-canvas-ink--lg {
|
|
1563
|
+
stroke-width: var(--juno-ink-canvas-halo-width-lg);
|
|
1564
|
+
text-shadow:
|
|
1565
|
+
var(--juno-ink-canvas-halo-width-lg) 0 0 var(--juno-ink-canvas-halo),
|
|
1566
|
+
calc(-1 * var(--juno-ink-canvas-halo-width-lg)) 0 0 var(--juno-ink-canvas-halo),
|
|
1567
|
+
0 var(--juno-ink-canvas-halo-width-lg) 0 var(--juno-ink-canvas-halo),
|
|
1568
|
+
0 calc(-1 * var(--juno-ink-canvas-halo-width-lg)) 0 var(--juno-ink-canvas-halo);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/* Vector ink — measurement lines, selection outlines. Two passes: the halo
|
|
1572
|
+
is the same path drawn wider underneath. In SVG give the halo element
|
|
1573
|
+
this class and the mark element __stroke; in canvas, stroke twice. */
|
|
1574
|
+
.juno-canvas-ink__halo {
|
|
1575
|
+
fill: none;
|
|
1576
|
+
stroke: var(--juno-ink-canvas-halo);
|
|
1577
|
+
stroke-width: calc(var(--juno-ink-canvas-halo-width) * 2);
|
|
1578
|
+
stroke-linecap: round;
|
|
1579
|
+
stroke-linejoin: round;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
.juno-canvas-ink__stroke {
|
|
1583
|
+
fill: none;
|
|
1584
|
+
stroke: var(--juno-ink-canvas-ink);
|
|
1585
|
+
stroke-linecap: round;
|
|
1586
|
+
stroke-linejoin: round;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/* Status over canvas. The themed role hues are chosen against s0–s3 and wash
|
|
1590
|
+
out over a saturated backdrop, so these are the same hues at raised chroma.
|
|
1591
|
+
Louder than the panel roles by design — they will look wrong on a panel. */
|
|
1592
|
+
.juno-canvas-ink--nominal { color: var(--juno-ink-vivid-nominal); }
|
|
1593
|
+
.juno-canvas-ink--active { color: var(--juno-ink-vivid-active); }
|
|
1594
|
+
.juno-canvas-ink--target { color: var(--juno-ink-vivid-target); }
|
|
1595
|
+
.juno-canvas-ink--caution { color: var(--juno-ink-vivid-caution); }
|
|
1596
|
+
.juno-canvas-ink--warning { color: var(--juno-ink-vivid-warning); }
|
|
1597
|
+
|
|
1598
|
+
/* Chrome floating over live content. Deliberately NOT --juno-opacity-scrim
|
|
1599
|
+
(0.62): that value suppresses a modal's background, and here the background
|
|
1600
|
+
is the thing the chrome is annotating — greying it out defeats the purpose. */
|
|
1601
|
+
.juno-canvas-scrim {
|
|
1602
|
+
background: color-mix(in srgb, var(--juno-s0) calc(var(--juno-ink-canvas-scrim) * 100%), transparent);
|
|
1603
|
+
-webkit-backdrop-filter: blur(8px);
|
|
1604
|
+
backdrop-filter: blur(8px);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/* A translucent bar over moving content is the case where reduced
|
|
1608
|
+
transparency matters most — the motion behind the text is the problem, not
|
|
1609
|
+
the blur. Fall back to the opaque surface. */
|
|
1610
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
1611
|
+
.juno-canvas-scrim {
|
|
1612
|
+
background: var(--juno-s0);
|
|
1613
|
+
-webkit-backdrop-filter: none;
|
|
1614
|
+
backdrop-filter: none;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1422
1618
|
/* ════════════════════════════════════════════════════════════════════
|
|
1423
1619
|
* Component — Card / panel
|
|
1424
1620
|
* Usage:
|
|
@@ -1750,6 +1946,55 @@ button.juno-chip {
|
|
|
1750
1946
|
media query. */
|
|
1751
1947
|
--juno-dock-scale: 1;
|
|
1752
1948
|
|
|
1949
|
+
/* ── Horizontal budget contract ──────────────────────────────────────
|
|
1950
|
+
Published so a consumer's capacity planner ("how many items fit, and
|
|
1951
|
+
do they still hold a tap target?") reads junoui's real geometry
|
|
1952
|
+
instead of re-deriving it from the numbers in this file. Two
|
|
1953
|
+
consumers had derived it by hand, twice, in prose — and both had
|
|
1954
|
+
drifted: they subtracted 12px of inline padding where the pill
|
|
1955
|
+
actually spends 8, so their per-item width came out ~0.8px low at
|
|
1956
|
+
every viewport (20260826-027).
|
|
1957
|
+
|
|
1958
|
+
--juno-dock-items the item budget. junoui does not enforce it —
|
|
1959
|
+
__item is `flex: 1 1 0`, so the layout divides
|
|
1960
|
+
by however many items are present — it is the
|
|
1961
|
+
number the DERIVATIONS below are about, and the
|
|
1962
|
+
consumer sets it to what it renders.
|
|
1963
|
+
--juno-dock-item-inline the width one item gets. A prediction of what
|
|
1964
|
+
the flex layout produces, not an input to it;
|
|
1965
|
+
asserted against the measured box in the test
|
|
1966
|
+
suite so it cannot drift from the layout.
|
|
1967
|
+
--juno-dock-fit-inline the narrowest viewport at which every item
|
|
1968
|
+
still holds --juno-size-tap-comfortable. Use it
|
|
1969
|
+
in a media query and DROP an item; that is the
|
|
1970
|
+
answer junoui can give exactly.
|
|
1971
|
+
|
|
1972
|
+
A SCALE floor (44px / the item's width) is deliberately not published:
|
|
1973
|
+
it is a ratio of two lengths, and CSS cannot divide by a length. A
|
|
1974
|
+
consumer that must scale rather than drop compares
|
|
1975
|
+
--juno-dock-item-inline against --juno-size-tap-comfortable itself.
|
|
1976
|
+
|
|
1977
|
+
The three inline terms are declared ONCE here and consumed by both the
|
|
1978
|
+
variants' own margin/padding/border and the sums below, so the budget
|
|
1979
|
+
cannot disagree with the bar it describes — the same reason
|
|
1980
|
+
--juno-dock-edge-offset exists (20260815-055). The base bar is
|
|
1981
|
+
full-bleed with a top-edge border only, so all three are zero; --pill
|
|
1982
|
+
and --float set them. */
|
|
1983
|
+
--juno-dock-items: 5;
|
|
1984
|
+
--juno-dock-margin-inline: 0px;
|
|
1985
|
+
--juno-dock-pad-inline: 0px;
|
|
1986
|
+
--juno-dock-border-inline: 0px;
|
|
1987
|
+
--juno-dock-chrome-inline: calc(
|
|
1988
|
+
2 * (var(--juno-dock-margin-inline) + var(--juno-dock-pad-inline) + var(--juno-dock-border-inline))
|
|
1989
|
+
);
|
|
1990
|
+
--juno-dock-avail: 100vw;
|
|
1991
|
+
--juno-dock-item-inline: calc(
|
|
1992
|
+
(var(--juno-dock-avail) - var(--juno-dock-chrome-inline)) / var(--juno-dock-items)
|
|
1993
|
+
);
|
|
1994
|
+
--juno-dock-fit-inline: calc(
|
|
1995
|
+
var(--juno-dock-items) * var(--juno-size-tap-comfortable) + var(--juno-dock-chrome-inline)
|
|
1996
|
+
);
|
|
1997
|
+
|
|
1753
1998
|
position: sticky;
|
|
1754
1999
|
inset-block-end: 0;
|
|
1755
2000
|
z-index: var(--juno-z-raised);
|
|
@@ -1844,16 +2089,21 @@ button.juno-chip {
|
|
|
1844
2089
|
.juno-dock--pill {
|
|
1845
2090
|
--juno-role: var(--juno-active);
|
|
1846
2091
|
|
|
2092
|
+
/* the three inline terms the budget contract sums — see .juno-dock */
|
|
2093
|
+
--juno-dock-margin-inline: var(--juno-space-12);
|
|
2094
|
+
--juno-dock-pad-inline: var(--juno-space-4);
|
|
2095
|
+
--juno-dock-border-inline: var(--juno-border-width-1);
|
|
2096
|
+
|
|
1847
2097
|
position: fixed;
|
|
1848
2098
|
inset-inline: 0;
|
|
1849
2099
|
inset-block-end: 0;
|
|
1850
2100
|
z-index: var(--juno-z-raised);
|
|
1851
|
-
margin: 0 var(--juno-
|
|
1852
|
-
padding: var(--juno-
|
|
2101
|
+
margin: 0 var(--juno-dock-margin-inline) var(--juno-dock-edge-offset);
|
|
2102
|
+
padding: var(--juno-dock-pad-inline);
|
|
1853
2103
|
background: color-mix(in srgb, var(--juno-s1) 88%, transparent);
|
|
1854
2104
|
-webkit-backdrop-filter: blur(12px);
|
|
1855
2105
|
backdrop-filter: blur(12px);
|
|
1856
|
-
border: var(--juno-border-
|
|
2106
|
+
border: var(--juno-dock-border-inline) solid var(--juno-border);
|
|
1857
2107
|
border-radius: 999px;
|
|
1858
2108
|
box-shadow: var(--juno-shadow-2);
|
|
1859
2109
|
}
|
|
@@ -1947,17 +2197,22 @@ button.juno-chip {
|
|
|
1947
2197
|
Out of flow (fixed) so the page scrolls UNDER it — reserve room on the
|
|
1948
2198
|
scroller with padding-block-end: var(--juno-dock-clearance). */
|
|
1949
2199
|
.juno-dock--float {
|
|
2200
|
+
/* the three inline terms the budget contract sums — see .juno-dock */
|
|
2201
|
+
--juno-dock-margin-inline: var(--juno-space-12);
|
|
2202
|
+
--juno-dock-pad-inline: var(--juno-space-4);
|
|
2203
|
+
--juno-dock-border-inline: var(--juno-border-width-1);
|
|
2204
|
+
|
|
1950
2205
|
position: fixed;
|
|
1951
2206
|
inset-inline: 0;
|
|
1952
2207
|
inset-block-end: 0;
|
|
1953
2208
|
z-index: var(--juno-z-raised);
|
|
1954
|
-
margin: 0 var(--juno-
|
|
1955
|
-
padding: var(--juno-
|
|
2209
|
+
margin: 0 var(--juno-dock-margin-inline) var(--juno-dock-edge-offset);
|
|
2210
|
+
padding: var(--juno-dock-pad-inline);
|
|
1956
2211
|
overflow: hidden;
|
|
1957
2212
|
background: color-mix(in srgb, var(--juno-s1) 88%, transparent);
|
|
1958
2213
|
-webkit-backdrop-filter: blur(12px);
|
|
1959
2214
|
backdrop-filter: blur(12px);
|
|
1960
|
-
border: var(--juno-border-
|
|
2215
|
+
border: var(--juno-dock-border-inline) solid var(--juno-border);
|
|
1961
2216
|
border-radius: 999px;
|
|
1962
2217
|
box-shadow: var(--juno-shadow-2);
|
|
1963
2218
|
}
|
|
@@ -2335,17 +2590,44 @@ button.juno-chip {
|
|
|
2335
2590
|
* gap, the folded slot still occupies one gap — name it in
|
|
2336
2591
|
* --juno-fold-gap and the fold swallows it with a negative margin.
|
|
2337
2592
|
*
|
|
2593
|
+
* COMPOSITION. The canonical use puts .juno-fold on an element that is
|
|
2594
|
+
* already a component — a .juno-pillbar__item, a .juno-btn, a .juno-chip
|
|
2595
|
+
* — because that is where the capsule chrome (tap floor, padding, hover,
|
|
2596
|
+
* focus ring, aria-pressed) lives. Two things follow, and both are
|
|
2597
|
+
* load-bearing rather than defensive:
|
|
2598
|
+
*
|
|
2599
|
+
* 1. The fold's own declarations are stated at ATTRIBUTE specificity
|
|
2600
|
+
* (0,2,0), on both branches of data-juno-in, i.e. always. A component
|
|
2601
|
+
* class is (0,1,0), and `transition` is a SHORTHAND: a later rule does
|
|
2602
|
+
* not add to the list, it replaces it. Measured in Chromium against the
|
|
2603
|
+
* built bundle, before this was raised: on a
|
|
2604
|
+
* .juno-fold.juno-pillbar__item, transition-property resolved to
|
|
2605
|
+
* `color, background-color` — pillbar.css sorts after this file, so the
|
|
2606
|
+
* fold had no transition at all and jumped shut instead of folding.
|
|
2607
|
+
* inline-size and overflow would lose the same way to a component that
|
|
2608
|
+
* sets either.
|
|
2609
|
+
*
|
|
2610
|
+
* 2. Owning `transition` means owning ALL of it, so the capsule chrome
|
|
2611
|
+
* properties are in the fold's list too, at the quick duration the
|
|
2612
|
+
* components themselves use. Dropping them would trade a broken fold
|
|
2613
|
+
* for a broken hover.
|
|
2614
|
+
*
|
|
2615
|
+
* Which floors the fold RELEASES when folded — see the folded rule below:
|
|
2616
|
+
* min-inline-size, padding-inline, border-inline-width. Those are exactly
|
|
2617
|
+
* the three inputs that can hold a border-box inline size above zero.
|
|
2618
|
+
*
|
|
2338
2619
|
* Canonical use: a transient action in a toolbar/pill row (a
|
|
2339
2620
|
* scroll-to-top arrow, a contextual button) whose arrival should slide
|
|
2340
2621
|
* the row open rather than jump it.
|
|
2341
2622
|
* Usage:
|
|
2342
2623
|
* <div style="display:flex; gap:4px"> (any row)
|
|
2343
|
-
* <button class="juno-fold" data-juno-in aria-label="Scroll to top">…</button>
|
|
2624
|
+
* <button class="juno-fold juno-pillbar__item" data-juno-in aria-label="Scroll to top">…</button>
|
|
2344
2625
|
* …other members…
|
|
2345
2626
|
* </div>
|
|
2346
2627
|
* prefers-reduced-motion needs nothing component-local — the base
|
|
2347
2628
|
* layer zeroes every transition duration. */
|
|
2348
|
-
.juno-fold
|
|
2629
|
+
.juno-fold[data-juno-in],
|
|
2630
|
+
.juno-fold:not([data-juno-in]) {
|
|
2349
2631
|
--juno-fold-size: var(--juno-size-tap-comfortable);
|
|
2350
2632
|
--juno-fold-gap: 0px;
|
|
2351
2633
|
|
|
@@ -2354,13 +2636,32 @@ button.juno-chip {
|
|
|
2354
2636
|
overflow: hidden;
|
|
2355
2637
|
transition:
|
|
2356
2638
|
inline-size var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
|
|
2639
|
+
min-inline-size var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
|
|
2640
|
+
padding var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
|
|
2641
|
+
border-width var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
|
|
2357
2642
|
margin var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
|
|
2358
2643
|
opacity var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
|
|
2359
|
-
visibility var(--juno-motion-duration-base) var(--juno-motion-ease-standard)
|
|
2644
|
+
visibility var(--juno-motion-duration-base) var(--juno-motion-ease-standard),
|
|
2645
|
+
color var(--juno-motion-duration-quick) var(--juno-motion-ease-standard),
|
|
2646
|
+
background-color var(--juno-motion-duration-quick) var(--juno-motion-ease-standard);
|
|
2360
2647
|
}
|
|
2361
2648
|
|
|
2649
|
+
/* Folded away. A min-* floor clamps the USED value whichever rule won the
|
|
2650
|
+
cascade, and a border-box inline size can never resolve below its own
|
|
2651
|
+
padding plus border — so a composed capsule holds the slot open however
|
|
2652
|
+
hard the fold pushes. Measured against the built bundle, composed with
|
|
2653
|
+
.juno-pillbar__item at a 390px viewport: 44px with both floors in place
|
|
2654
|
+
(the tap target), 20px with min-inline-size released (the padding alone),
|
|
2655
|
+
0px with all of them released.
|
|
2656
|
+
min-inline-size is in the transition list above for the OPENING half:
|
|
2657
|
+
released to 0 it would otherwise snap back to the capsule's tap floor the
|
|
2658
|
+
instant data-juno-in returns, and the used width — max(inline-size,
|
|
2659
|
+
min-inline-size) — would jump to full width with nothing to animate. */
|
|
2362
2660
|
.juno-fold:not([data-juno-in]) {
|
|
2363
2661
|
inline-size: 0;
|
|
2662
|
+
min-inline-size: 0;
|
|
2663
|
+
padding-inline: 0;
|
|
2664
|
+
border-inline-width: 0;
|
|
2364
2665
|
margin-inline-end: calc(-1 * var(--juno-fold-gap));
|
|
2365
2666
|
opacity: 0;
|
|
2366
2667
|
visibility: hidden;
|
|
@@ -2445,6 +2746,245 @@ button.juno-chip {
|
|
|
2445
2746
|
}
|
|
2446
2747
|
.juno-gauge--lg .juno-gauge__value { font-size: var(--juno-font-size-20); }
|
|
2447
2748
|
|
|
2749
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
2750
|
+
* Component — Viewport orientation gizmo (compass ring + pitch arc)
|
|
2751
|
+
* The orientation widget any 3D or map viewport ships: a ring showing a
|
|
2752
|
+
* heading with clickable snap targets, a secondary arc for a second
|
|
2753
|
+
* angle (pitch / tilt / elevation) inside a clamped range, and a centre
|
|
2754
|
+
* target that resets the view.
|
|
2755
|
+
*
|
|
2756
|
+
* A RING, NOT A CUBE, and that is a design decision rather than a style
|
|
2757
|
+
* one. An Autodesk-style ViewCube is the wrong shape for anything with a
|
|
2758
|
+
* privileged up-vector — a map, a terrain, a site plan — because there is
|
|
2759
|
+
* no meaningful front, right or bottom face to click. A ring degrades to
|
|
2760
|
+
* the up-vector case and generalises to the free-orbit one; a cube does
|
|
2761
|
+
* not go the other way.
|
|
2762
|
+
*
|
|
2763
|
+
* THE APP OWNS THE CAMERA. It writes two angles in as custom properties
|
|
2764
|
+
* and junoui rotates the marks; nothing here stores or changes state.
|
|
2765
|
+
* element.style.setProperty('--juno-gizmo-heading', `${yaw}deg`);
|
|
2766
|
+
* element.style.setProperty('--juno-gizmo-pitch', `${pitch}deg`);
|
|
2767
|
+
*
|
|
2768
|
+
* Usage — see docs/components/gizmo.md for the full a11y contract, which
|
|
2769
|
+
* is the half apps get wrong and the reason this is upstream:
|
|
2770
|
+
* <div class="juno-gizmo" role="group" aria-label="View orientation">
|
|
2771
|
+
* <p class="juno-gizmo__readout" aria-live="polite">Facing north, 0 degrees.</p>
|
|
2772
|
+
* <div class="juno-gizmo__ring">
|
|
2773
|
+
* <span class="juno-gizmo__needle" aria-hidden="true"></span>
|
|
2774
|
+
* <button class="juno-gizmo__mark" style="--juno-gizmo-at:0deg"
|
|
2775
|
+
* aria-label="Face north" aria-current="true">N</button>
|
|
2776
|
+
* …seven more…
|
|
2777
|
+
* <button class="juno-gizmo__center" aria-label="Reset view">⌖</button>
|
|
2778
|
+
* </div>
|
|
2779
|
+
* <div class="juno-gizmo__arc">
|
|
2780
|
+
* <span class="juno-gizmo__arc-hand" aria-hidden="true"></span>
|
|
2781
|
+
* </div>
|
|
2782
|
+
* </div>
|
|
2783
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
2784
|
+
|
|
2785
|
+
.juno-gizmo {
|
|
2786
|
+
--juno-role: var(--juno-active);
|
|
2787
|
+
|
|
2788
|
+
/* The angles the app writes. Defaults are a level view facing north, so a
|
|
2789
|
+
gizmo that is mounted before the camera reports is not a blank circle. */
|
|
2790
|
+
--juno-gizmo-heading: 0deg;
|
|
2791
|
+
--juno-gizmo-pitch: 0deg;
|
|
2792
|
+
|
|
2793
|
+
/* MINIMUM DIAMETER, DERIVED — and derived from the CHORD, which is the
|
|
2794
|
+
correction that matters. N marks sit evenly around the rim, so the
|
|
2795
|
+
straight-line distance between two adjacent centres is d * sin(pi / N).
|
|
2796
|
+
For their targets not to overlap THAT must be at least one tap target:
|
|
2797
|
+
|
|
2798
|
+
d >= tap / sin(pi / N)
|
|
2799
|
+
|
|
2800
|
+
...and the marks sit INSIDE the rim, so their centres are on a circle of
|
|
2801
|
+
radius d/2 - tap/2, not d/2. Substituting that and solving:
|
|
2802
|
+
|
|
2803
|
+
d >= tap * (1 / sin(pi / N) + 1)
|
|
2804
|
+
|
|
2805
|
+
THIS DERIVATION WAS WRONG TWICE, and the browser guard caught both. First
|
|
2806
|
+
it sized off the ARC between centres (N * tap / pi) — an arc is longer
|
|
2807
|
+
than its chord, so at N=8 and a 44px target that gave 112.05px whose chord
|
|
2808
|
+
is 42.9px, a 1.1px overlap on every neighbouring pair. Corrected to the
|
|
2809
|
+
chord, it still measured 27.16px between centres, because the inset had
|
|
2810
|
+
been left out of the radius. Neither error is visible in the source; both
|
|
2811
|
+
are one measurement away.
|
|
2812
|
+
|
|
2813
|
+
The tap floor MOVES, 24px to 44px on a coarse pointer, which is precisely
|
|
2814
|
+
when the ring has to grow; a hard-coded diameter ships eight overlapping
|
|
2815
|
+
targets to a phone. The 96px is an aesthetic floor, not a derived one —
|
|
2816
|
+
a 63px ring is legible-but-cramped on desktop — and max() keeps whichever
|
|
2817
|
+
is larger. */
|
|
2818
|
+
--juno-gizmo-marks: 8;
|
|
2819
|
+
--juno-gizmo-size: max(
|
|
2820
|
+
96px,
|
|
2821
|
+
calc(var(--juno-size-tap-min) * (1 / sin(180deg / var(--juno-gizmo-marks)) + 1))
|
|
2822
|
+
);
|
|
2823
|
+
|
|
2824
|
+
display: inline-flex;
|
|
2825
|
+
flex-direction: column;
|
|
2826
|
+
align-items: center;
|
|
2827
|
+
gap: var(--juno-space-8);
|
|
2828
|
+
font-family: var(--juno-font-family-sans);
|
|
2829
|
+
color: var(--juno-data);
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
/* The spoken state. Visible by default — a bearing is useful to everyone —
|
|
2833
|
+
but it is the live region either way, because a rotating needle announces
|
|
2834
|
+
nothing. Consumers that want it invisible use .juno-sr-only rather than
|
|
2835
|
+
display:none, which would take it out of the accessibility tree too. */
|
|
2836
|
+
.juno-gizmo__readout {
|
|
2837
|
+
margin: 0;
|
|
2838
|
+
font-size: var(--juno-font-size-11);
|
|
2839
|
+
font-variant-numeric: tabular-nums;
|
|
2840
|
+
letter-spacing: var(--juno-font-tracking-label);
|
|
2841
|
+
color: var(--juno-label);
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
.juno-gizmo__ring {
|
|
2845
|
+
position: relative;
|
|
2846
|
+
inline-size: var(--juno-gizmo-size);
|
|
2847
|
+
block-size: var(--juno-gizmo-size);
|
|
2848
|
+
border-radius: 50%;
|
|
2849
|
+
border: var(--juno-border-width-1) solid var(--juno-border);
|
|
2850
|
+
background: color-mix(in srgb, var(--juno-s1) 88%, transparent);
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
/* The needle points where the camera looks. Rotated by the app's angle, and
|
|
2854
|
+
the transition runs on the motion scale so prefers-reduced-motion collapses
|
|
2855
|
+
it without this file needing its own media query (base.css owns that). */
|
|
2856
|
+
.juno-gizmo__needle {
|
|
2857
|
+
position: absolute;
|
|
2858
|
+
inset: 0;
|
|
2859
|
+
display: grid;
|
|
2860
|
+
place-items: start center;
|
|
2861
|
+
pointer-events: none;
|
|
2862
|
+
rotate: var(--juno-gizmo-heading);
|
|
2863
|
+
transition: rotate calc(var(--juno-motion-duration-base) * var(--juno-motion-scale))
|
|
2864
|
+
var(--juno-motion-ease-standard);
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
.juno-gizmo__needle::before {
|
|
2868
|
+
content: '';
|
|
2869
|
+
inline-size: var(--juno-border-width-2);
|
|
2870
|
+
block-size: 38%;
|
|
2871
|
+
margin-block-start: var(--juno-space-4);
|
|
2872
|
+
background: linear-gradient(var(--juno-role), transparent);
|
|
2873
|
+
border-radius: var(--juno-border-width-2);
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
/* ── snap targets ──────────────────────────────────────────────────────
|
|
2877
|
+
REAL BUTTONS. Not a canvas hit test, not a div with a click handler: a
|
|
2878
|
+
button is focusable, activates on Enter and Space, is announced as a
|
|
2879
|
+
control, and works with a screen reader's forms mode. That is most of
|
|
2880
|
+
this component's value and the reason it is upstream.
|
|
2881
|
+
|
|
2882
|
+
Positioned by rotating the mark out to the rim and counter-rotating the
|
|
2883
|
+
glyph, so the letter stays upright at every angle. --juno-gizmo-at is the
|
|
2884
|
+
mark's own bearing, set per element. */
|
|
2885
|
+
.juno-gizmo__mark {
|
|
2886
|
+
position: absolute;
|
|
2887
|
+
inset-block-start: 50%;
|
|
2888
|
+
inset-inline-start: 50%;
|
|
2889
|
+
inline-size: var(--juno-size-tap-min);
|
|
2890
|
+
block-size: var(--juno-size-tap-min);
|
|
2891
|
+
margin: calc(var(--juno-size-tap-min) / -2);
|
|
2892
|
+
display: grid;
|
|
2893
|
+
place-items: center;
|
|
2894
|
+
padding: 0;
|
|
2895
|
+
border: none;
|
|
2896
|
+
border-radius: 50%;
|
|
2897
|
+
background: transparent;
|
|
2898
|
+
color: var(--juno-label);
|
|
2899
|
+
font: inherit;
|
|
2900
|
+
font-size: var(--juno-font-size-11);
|
|
2901
|
+
font-weight: var(--juno-font-weight-bold);
|
|
2902
|
+
cursor: pointer;
|
|
2903
|
+
transform: rotate(var(--juno-gizmo-at, 0deg))
|
|
2904
|
+
translate(0, calc(var(--juno-gizmo-size) / -2 + var(--juno-size-tap-min) / 2))
|
|
2905
|
+
rotate(calc(-1 * var(--juno-gizmo-at, 0deg)));
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
.juno-gizmo__mark:hover {
|
|
2909
|
+
color: var(--juno-data);
|
|
2910
|
+
background: var(--juno-s2);
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
/* The mark the camera is currently nearest. aria-current, not a class: the
|
|
2914
|
+
app already has to say it for the screen reader, and a class would be a
|
|
2915
|
+
second copy of the same fact. */
|
|
2916
|
+
.juno-gizmo__mark[aria-current='true'] {
|
|
2917
|
+
color: var(--juno-role);
|
|
2918
|
+
background: var(--juno-s3);
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
.juno-gizmo__mark:focus-visible,
|
|
2922
|
+
.juno-gizmo__center:focus-visible {
|
|
2923
|
+
outline: var(--juno-border-width-2) solid var(--juno-active);
|
|
2924
|
+
outline-offset: var(--juno-space-2);
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
/* Centre target — reset to the default view. */
|
|
2928
|
+
.juno-gizmo__center {
|
|
2929
|
+
position: absolute;
|
|
2930
|
+
inset-block-start: 50%;
|
|
2931
|
+
inset-inline-start: 50%;
|
|
2932
|
+
inline-size: var(--juno-size-tap-min);
|
|
2933
|
+
block-size: var(--juno-size-tap-min);
|
|
2934
|
+
margin: calc(var(--juno-size-tap-min) / -2);
|
|
2935
|
+
display: grid;
|
|
2936
|
+
place-items: center;
|
|
2937
|
+
padding: 0;
|
|
2938
|
+
border: var(--juno-border-width-1) solid var(--juno-border);
|
|
2939
|
+
border-radius: 50%;
|
|
2940
|
+
background: var(--juno-s1);
|
|
2941
|
+
color: var(--juno-label);
|
|
2942
|
+
font: inherit;
|
|
2943
|
+
cursor: pointer;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
.juno-gizmo__center:hover {
|
|
2947
|
+
color: var(--juno-role);
|
|
2948
|
+
border-color: var(--juno-role);
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
/* ── pitch arc ─────────────────────────────────────────────────────────
|
|
2952
|
+
The second angle, inside a CLAMPED range — a camera that can tilt 0..85°
|
|
2953
|
+
should not show a hand that can point anywhere, or the widget lies about
|
|
2954
|
+
what the app will accept. The clamp lives here so every consumer gets it
|
|
2955
|
+
without re-deriving it from its own camera limits. */
|
|
2956
|
+
.juno-gizmo__arc {
|
|
2957
|
+
--juno-gizmo-pitch-min: 0deg;
|
|
2958
|
+
--juno-gizmo-pitch-max: 85deg;
|
|
2959
|
+
|
|
2960
|
+
position: relative;
|
|
2961
|
+
inline-size: var(--juno-gizmo-size);
|
|
2962
|
+
block-size: calc(var(--juno-gizmo-size) / 2);
|
|
2963
|
+
overflow: hidden;
|
|
2964
|
+
border-block-end: var(--juno-border-width-1) solid var(--juno-border);
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
.juno-gizmo__arc-hand {
|
|
2968
|
+
position: absolute;
|
|
2969
|
+
inset-block-end: 0;
|
|
2970
|
+
inset-inline-start: 50%;
|
|
2971
|
+
inline-size: var(--juno-border-width-2);
|
|
2972
|
+
block-size: 80%;
|
|
2973
|
+
background: var(--juno-role);
|
|
2974
|
+
transform-origin: bottom center;
|
|
2975
|
+
rotate: clamp(var(--juno-gizmo-pitch-min), var(--juno-gizmo-pitch), var(--juno-gizmo-pitch-max));
|
|
2976
|
+
transition: rotate calc(var(--juno-motion-duration-base) * var(--juno-motion-scale))
|
|
2977
|
+
var(--juno-motion-ease-standard);
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
/* ── touch ─────────────────────────────────────────────────────────────
|
|
2981
|
+
Nothing to grow here, and that is the point: the marks are sized in
|
|
2982
|
+
--juno-size-tap-min, which base.css promotes to the comfortable target on a
|
|
2983
|
+
coarse pointer, and --juno-gizmo-size is derived from it — so the whole ring
|
|
2984
|
+
grows and the marks stay non-overlapping by construction. A gizmo that keeps
|
|
2985
|
+
its desktop diameter on a phone has eight overlapping targets, which is the
|
|
2986
|
+
defect this derivation exists to make impossible. */
|
|
2987
|
+
|
|
2448
2988
|
/* ════════════════════════════════════════════════════════════════════
|
|
2449
2989
|
* Component — Icon loader (the ring-a-control primitive)
|
|
2450
2990
|
* A control ringed by the spinning arc while its section loads — the
|
|
@@ -4187,11 +4727,20 @@ button.juno-list__row:disabled {
|
|
|
4187
4727
|
opacity: 0;
|
|
4188
4728
|
}
|
|
4189
4729
|
|
|
4190
|
-
/* the pill — a span after the radio, or the aria-pressed button itself
|
|
4730
|
+
/* the pill — a span after the radio, or the aria-pressed button itself.
|
|
4731
|
+
The tap floor is on the PAINTED box, not on the label that wraps it: the
|
|
4732
|
+
label is a bare inline-flex and takes its height from this. Segmented is a
|
|
4733
|
+
phone-first control and is routinely the only control on a whole settings
|
|
4734
|
+
section, so it holds --juno-size-tap-min like .juno-btn does — 24px (WCAG
|
|
4735
|
+
2.2 AA, 2.5.8) on a fine pointer, promoted to the 44px comfortable target
|
|
4736
|
+
under (pointer: coarse) by base.css. Without it the pill computed ~25px
|
|
4737
|
+
from its padding alone, which is the AA floor by accident and nothing on
|
|
4738
|
+
touch. See 20260826-025. */
|
|
4191
4739
|
.juno-seg__opt input + span,
|
|
4192
4740
|
button.juno-seg__opt {
|
|
4193
4741
|
display: inline-flex;
|
|
4194
4742
|
align-items: center;
|
|
4743
|
+
min-block-size: var(--juno-size-tap-min);
|
|
4195
4744
|
gap: var(--juno-gap-control);
|
|
4196
4745
|
font-family: var(--juno-font-family-sans);
|
|
4197
4746
|
font-size: var(--juno-font-size-11);
|
|
@@ -4239,7 +4788,11 @@ button.juno-seg__opt:disabled {
|
|
|
4239
4788
|
cursor: not-allowed;
|
|
4240
4789
|
}
|
|
4241
4790
|
|
|
4242
|
-
/* Small —
|
|
4791
|
+
/* Small — dense toolbars: less type, tighter padding. UNLIKE .juno-btn--sm it
|
|
4792
|
+
does NOT drop below the tap floor, which the base rule above holds: --sm is a
|
|
4793
|
+
density modifier here, not an opt-out of the touch target. A segmented row is
|
|
4794
|
+
often the only control on a settings section, so a sub-tap variant of it has
|
|
4795
|
+
no safe use on a phone. */
|
|
4243
4796
|
.juno-seg--sm .juno-seg__opt input + span,
|
|
4244
4797
|
.juno-seg--sm > button.juno-seg__opt {
|
|
4245
4798
|
font-size: var(--juno-font-size-10);
|
|
@@ -4644,6 +5197,194 @@ button.juno-seg__opt:disabled {
|
|
|
4644
5197
|
block-size: 100%;
|
|
4645
5198
|
}
|
|
4646
5199
|
|
|
5200
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
5201
|
+
* Component — Colour swatch + palette
|
|
5202
|
+
* Showing a user-chosen colour, and letting someone pick one. Diagrams,
|
|
5203
|
+
* calendars, tag and label systems, chart series colours, annotation
|
|
5204
|
+
* tools, theming UIs, kanban boards.
|
|
5205
|
+
*
|
|
5206
|
+
* THE HARD PART IS NOT THE SQUARE. A swatch shows an ARBITRARY colour, so
|
|
5207
|
+
* every piece of chrome on it — its border, its focus ring, its checked
|
|
5208
|
+
* indicator — has to stay visible against a colour junoui has never seen.
|
|
5209
|
+
* A single hairline fails at one end of the range: a dark border vanishes
|
|
5210
|
+
* on near-black, a light one vanishes on near-white, and the swatch that
|
|
5211
|
+
* loses its border is the one that has merged with the panel behind it.
|
|
5212
|
+
*
|
|
5213
|
+
* The answer is a PAIR of hairlines drawn as an inset and an outset ring.
|
|
5214
|
+
* They do NOT do the same job, which is the part worth reading twice: the
|
|
5215
|
+
* inset one composites over the SWATCH and edges it against its own fill,
|
|
5216
|
+
* the outset one composites over the PANEL and separates it from the
|
|
5217
|
+
* surface. With the swatch's own contrast against that panel, the boundary
|
|
5218
|
+
* has three ways to be visible and needs only one of them.
|
|
5219
|
+
*
|
|
5220
|
+
* Guarded by sweeping the swatch colour against both panels rather than by
|
|
5221
|
+
* asserting a border value (test/swatch.test.mjs).
|
|
5222
|
+
*
|
|
5223
|
+
* COLOUR IS NEVER THE ONLY SIGNAL. junoui's own standing rule, and a bare
|
|
5224
|
+
* swatch is exactly what violates it: every swatch carries an accessible
|
|
5225
|
+
* NAME, and the checked state carries a GLYPH, not just a hue.
|
|
5226
|
+
*
|
|
5227
|
+
* The app owns the colour list and which one is chosen.
|
|
5228
|
+
* Usage:
|
|
5229
|
+
* <button class="juno-swatch juno-swatch--button" style="--juno-swatch-color:#C41E3A"
|
|
5230
|
+
* aria-label="Crimson"></button>
|
|
5231
|
+
*
|
|
5232
|
+
* <div class="juno-popover" popover id="palette">
|
|
5233
|
+
* <div class="juno-palette" role="listbox" aria-label="Annotation colour">
|
|
5234
|
+
* <button class="juno-palette__option" role="option" aria-selected="true"
|
|
5235
|
+
* style="--juno-swatch-color:#C41E3A" aria-label="Crimson">
|
|
5236
|
+
* <svg class="juno-icon juno-palette__check" aria-hidden="true">
|
|
5237
|
+
* <use href="…#juno-i-check" /></svg>
|
|
5238
|
+
* </button>
|
|
5239
|
+
* </div>
|
|
5240
|
+
* </div>
|
|
5241
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
5242
|
+
|
|
5243
|
+
.juno-swatch {
|
|
5244
|
+
/* The colour the app is showing. Named rather than an inline `background`
|
|
5245
|
+
so the rings below can sit on top of it without the caller reassembling
|
|
5246
|
+
the whole box-shadow stack. */
|
|
5247
|
+
--juno-swatch-color: var(--juno-muted);
|
|
5248
|
+
|
|
5249
|
+
/* Sized off the control scale, so a swatch in a form row matches the
|
|
5250
|
+
controls beside it and grows with them on a coarse pointer. */
|
|
5251
|
+
--juno-swatch-size: var(--juno-size-tap-min);
|
|
5252
|
+
|
|
5253
|
+
display: inline-block;
|
|
5254
|
+
inline-size: var(--juno-swatch-size);
|
|
5255
|
+
block-size: var(--juno-swatch-size);
|
|
5256
|
+
flex-shrink: 0;
|
|
5257
|
+
border: none;
|
|
5258
|
+
border-radius: var(--juno-radius-3);
|
|
5259
|
+
background: var(--juno-swatch-color);
|
|
5260
|
+
|
|
5261
|
+
/* THE TWO-TONE RING, and the alphas are measured rather than chosen.
|
|
5262
|
+
The inset ring composites over the SWATCH and gives it an edge against
|
|
5263
|
+
its own fill; the outset ring composites over the PANEL — an outset
|
|
5264
|
+
box-shadow is outside the element — and separates the swatch from the
|
|
5265
|
+
surface behind it. Between them, plus the swatch's own contrast with the
|
|
5266
|
+
panel, the boundary is discernible whatever colour the app supplies.
|
|
5267
|
+
|
|
5268
|
+
At 0.45 / 0.35 that property FAILS: on a light panel a mid-grey swatch
|
|
5269
|
+
(around #919191) leaves the best of the three at 2.57:1, under the 3:1
|
|
5270
|
+
non-text floor. 0.65 on both takes the worst case to 3.39:1. The sweep in
|
|
5271
|
+
test/swatch.test.mjs is what measured it — the first version of that test
|
|
5272
|
+
asked the wrong question (both rings against the swatch) and reported 145
|
|
5273
|
+
failures; the right question is whether the BOUNDARY is visible, and it
|
|
5274
|
+
has three ways to be.
|
|
5275
|
+
|
|
5276
|
+
Deliberately not `border`, which would eat into the colour area and change
|
|
5277
|
+
the swatch's size with its style. */
|
|
5278
|
+
box-shadow:
|
|
5279
|
+
inset 0 0 0 var(--juno-border-width-1) rgb(0 0 0 / 0.65),
|
|
5280
|
+
0 0 0 var(--juno-border-width-1) rgb(255 255 255 / 0.65);
|
|
5281
|
+
}
|
|
5282
|
+
|
|
5283
|
+
.juno-swatch--circle {
|
|
5284
|
+
border-radius: 50%;
|
|
5285
|
+
}
|
|
5286
|
+
|
|
5287
|
+
.juno-swatch--sm {
|
|
5288
|
+
--juno-swatch-size: var(--juno-space-16);
|
|
5289
|
+
}
|
|
5290
|
+
|
|
5291
|
+
.juno-swatch--lg {
|
|
5292
|
+
--juno-swatch-size: var(--juno-size-tap-comfortable);
|
|
5293
|
+
}
|
|
5294
|
+
|
|
5295
|
+
/* A swatch used as a trigger — the thing you click to open the palette. */
|
|
5296
|
+
.juno-swatch--button {
|
|
5297
|
+
padding: 0;
|
|
5298
|
+
cursor: pointer;
|
|
5299
|
+
}
|
|
5300
|
+
|
|
5301
|
+
/* FOCUS RINGS ON AN ARBITRARY HUE. The ring is drawn OUTSIDE the swatch,
|
|
5302
|
+
with an offset, so its contrast is against the panel — a known surface —
|
|
5303
|
+
rather than against a colour junoui cannot predict. A ring drawn on the
|
|
5304
|
+
swatch itself has the same unsolvable problem as the border, and "use a
|
|
5305
|
+
thicker ring" does not fix a hue collision. */
|
|
5306
|
+
.juno-swatch--button:focus-visible,
|
|
5307
|
+
.juno-palette__option:focus-visible {
|
|
5308
|
+
outline: var(--juno-border-width-2) solid var(--juno-active);
|
|
5309
|
+
outline-offset: var(--juno-space-4);
|
|
5310
|
+
}
|
|
5311
|
+
|
|
5312
|
+
/* ── palette ───────────────────────────────────────────────────────────
|
|
5313
|
+
A grid of swatches inside a .juno-popover. junoui ships the grid and the
|
|
5314
|
+
states; the app owns the list and which one is chosen. */
|
|
5315
|
+
.juno-palette {
|
|
5316
|
+
display: grid;
|
|
5317
|
+
grid-template-columns: repeat(var(--juno-palette-columns, 6), auto);
|
|
5318
|
+
gap: var(--juno-space-8);
|
|
5319
|
+
padding: var(--juno-space-4);
|
|
5320
|
+
}
|
|
5321
|
+
|
|
5322
|
+
.juno-palette__option {
|
|
5323
|
+
--juno-swatch-color: var(--juno-muted);
|
|
5324
|
+
--juno-swatch-size: var(--juno-size-tap-min);
|
|
5325
|
+
|
|
5326
|
+
position: relative;
|
|
5327
|
+
inline-size: var(--juno-swatch-size);
|
|
5328
|
+
block-size: var(--juno-swatch-size);
|
|
5329
|
+
display: grid;
|
|
5330
|
+
place-items: center;
|
|
5331
|
+
padding: 0;
|
|
5332
|
+
border: none;
|
|
5333
|
+
border-radius: var(--juno-radius-3);
|
|
5334
|
+
background: var(--juno-swatch-color);
|
|
5335
|
+
cursor: pointer;
|
|
5336
|
+
box-shadow:
|
|
5337
|
+
inset 0 0 0 var(--juno-border-width-1) rgb(0 0 0 / 0.65),
|
|
5338
|
+
0 0 0 var(--juno-border-width-1) rgb(255 255 255 / 0.65);
|
|
5339
|
+
}
|
|
5340
|
+
|
|
5341
|
+
/* THE CHECKED STATE IS A GLYPH, NOT A HUE. Colour is never the only signal,
|
|
5342
|
+
and "the chosen one is the one that looks slightly different" is exactly
|
|
5343
|
+
the failure that rule exists to prevent — it is invisible to anyone who
|
|
5344
|
+
cannot separate the two hues, and to anyone looking at a screenshot.
|
|
5345
|
+
|
|
5346
|
+
The check itself sits on the arbitrary colour, so it gets the same
|
|
5347
|
+
treatment as the swatch's own ring: a light glyph with a dark halo, which
|
|
5348
|
+
is the canvas-ink pair at glyph scale. */
|
|
5349
|
+
.juno-palette__check {
|
|
5350
|
+
display: none;
|
|
5351
|
+
color: #fff;
|
|
5352
|
+
filter: drop-shadow(0 0 var(--juno-border-width-1) rgb(0 0 0 / 0.9))
|
|
5353
|
+
drop-shadow(0 0 var(--juno-border-width-2) rgb(0 0 0 / 0.7));
|
|
5354
|
+
}
|
|
5355
|
+
|
|
5356
|
+
.juno-palette__option[aria-selected='true'] .juno-palette__check,
|
|
5357
|
+
.juno-palette__option[aria-checked='true'] .juno-palette__check {
|
|
5358
|
+
display: block;
|
|
5359
|
+
}
|
|
5360
|
+
|
|
5361
|
+
/* ...and a second, non-glyph cue for the same state, because a check inside a
|
|
5362
|
+
28px square is small: the selected option also grows a ring in the active
|
|
5363
|
+
role, drawn OUTSIDE the swatch where its contrast is against the panel. */
|
|
5364
|
+
.juno-palette__option[aria-selected='true'],
|
|
5365
|
+
.juno-palette__option[aria-checked='true'] {
|
|
5366
|
+
box-shadow:
|
|
5367
|
+
inset 0 0 0 var(--juno-border-width-1) rgb(0 0 0 / 0.65),
|
|
5368
|
+
0 0 0 var(--juno-border-width-1) rgb(255 255 255 / 0.65),
|
|
5369
|
+
0 0 0 calc(var(--juno-border-width-1) + var(--juno-border-width-2)) var(--juno-active);
|
|
5370
|
+
}
|
|
5371
|
+
|
|
5372
|
+
/* A swatch showing "no colour" — transparent, unset, inherit. Without this a
|
|
5373
|
+
consumer paints it in a mid grey and the user cannot tell "grey" from
|
|
5374
|
+
"none", which is a different thing to know. */
|
|
5375
|
+
.juno-swatch--none,
|
|
5376
|
+
.juno-palette__option--none {
|
|
5377
|
+
background:
|
|
5378
|
+
linear-gradient(
|
|
5379
|
+
to bottom right,
|
|
5380
|
+
transparent calc(50% - var(--juno-border-width-1)),
|
|
5381
|
+
var(--juno-warning) calc(50% - var(--juno-border-width-1)),
|
|
5382
|
+
var(--juno-warning) calc(50% + var(--juno-border-width-1)),
|
|
5383
|
+
transparent calc(50% + var(--juno-border-width-1))
|
|
5384
|
+
)
|
|
5385
|
+
var(--juno-s1);
|
|
5386
|
+
}
|
|
5387
|
+
|
|
4647
5388
|
/* ════════════════════════════════════════════════════════════════════
|
|
4648
5389
|
* Component — Switch (toggle) · labeled rocker
|
|
4649
5390
|
* A wide squared slide-switch with the state printed inside the track
|
|
@@ -5786,6 +6527,266 @@ button.juno-seg__opt:disabled {
|
|
|
5786
6527
|
inset-inline-start: calc(100% - var(--juno-space-4));
|
|
5787
6528
|
}
|
|
5788
6529
|
|
|
6530
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
6531
|
+
* Component — Tree / outliner (nested rows, disclosure, selection)
|
|
6532
|
+
* Layer stacks, file browsers, settings trees, org charts, comment
|
|
6533
|
+
* threads, nested navigation. Where .juno-list is flat and
|
|
6534
|
+
* .juno-accordion is single-level, this nests to arbitrary depth and
|
|
6535
|
+
* carries a selection.
|
|
6536
|
+
*
|
|
6537
|
+
* ZERO JS FOR THE VISUALS. Indentation comes from the nested `role=group`
|
|
6538
|
+
* lists the ARIA pattern already requires, so depth costs no custom
|
|
6539
|
+
* property and no inline style; collapse is `[aria-expanded='false']`
|
|
6540
|
+
* hiding the child group. The app owns the attribute, exactly like
|
|
6541
|
+
* __item's aria-pressed elsewhere in junoui.
|
|
6542
|
+
*
|
|
6543
|
+
* KEYBOARD IS NOT OPTIONAL and is not CSS. A tree without arrow-key
|
|
6544
|
+
* traversal and a roving tabindex is a list of buttons wearing tree
|
|
6545
|
+
* roles. junoui ships the contract in docs/components/tree.md and a
|
|
6546
|
+
* stateless enhancer at `junoui/tree` — use one or the other, but a tree
|
|
6547
|
+
* that has neither is not accessible and no stylesheet can fix it.
|
|
6548
|
+
*
|
|
6549
|
+
* Usage (the markup the roles require — see the doc for the full ARIA
|
|
6550
|
+
* contract, which is the half apps get wrong):
|
|
6551
|
+
* <ul class="juno-tree" role="tree" aria-label="Scene">
|
|
6552
|
+
* <li class="juno-tree__item" role="treeitem" aria-level="1"
|
|
6553
|
+
* aria-expanded="true" aria-selected="false">
|
|
6554
|
+
* <div class="juno-tree__row">
|
|
6555
|
+
* <button class="juno-tree__caret" tabindex="-1" aria-hidden="true"></button>
|
|
6556
|
+
* <svg class="juno-icon juno-tree__icon" aria-hidden="true">…</svg>
|
|
6557
|
+
* <span class="juno-tree__label">Terrain</span>
|
|
6558
|
+
* <span class="juno-tree__count">12</span>
|
|
6559
|
+
* <span class="juno-tree__trail">…</span>
|
|
6560
|
+
* <button class="juno-tree__handle" aria-label="Reorder Terrain"></button>
|
|
6561
|
+
* </div>
|
|
6562
|
+
* <ul class="juno-tree__group" role="group">…</ul>
|
|
6563
|
+
* </li>
|
|
6564
|
+
* </ul>
|
|
6565
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
6566
|
+
|
|
6567
|
+
.juno-tree {
|
|
6568
|
+
--juno-role: var(--juno-active);
|
|
6569
|
+
|
|
6570
|
+
/* One indent step. Applied per nested group, so depth is structural and
|
|
6571
|
+
arbitrary rather than a level number someone has to keep in sync. */
|
|
6572
|
+
--juno-tree-indent: var(--juno-space-16);
|
|
6573
|
+
|
|
6574
|
+
margin: 0;
|
|
6575
|
+
padding: 0;
|
|
6576
|
+
list-style: none;
|
|
6577
|
+
font-family: var(--juno-font-family-sans);
|
|
6578
|
+
font-size: var(--juno-font-size-13);
|
|
6579
|
+
color: var(--juno-data);
|
|
6580
|
+
}
|
|
6581
|
+
|
|
6582
|
+
.juno-tree__group {
|
|
6583
|
+
margin: 0;
|
|
6584
|
+
padding: 0;
|
|
6585
|
+
padding-inline-start: var(--juno-tree-indent);
|
|
6586
|
+
list-style: none;
|
|
6587
|
+
}
|
|
6588
|
+
|
|
6589
|
+
/* Collapsed: the child group goes. `display: none` and not a height
|
|
6590
|
+
animation — a tree collapses an unknown number of rows at an unknown
|
|
6591
|
+
depth, so there is no end value to transition to, and interpolate-size
|
|
6592
|
+
is not available everywhere junoui targets. */
|
|
6593
|
+
.juno-tree__item[aria-expanded='false'] > .juno-tree__group {
|
|
6594
|
+
display: none;
|
|
6595
|
+
}
|
|
6596
|
+
|
|
6597
|
+
.juno-tree__row {
|
|
6598
|
+
display: flex;
|
|
6599
|
+
align-items: center;
|
|
6600
|
+
gap: var(--juno-space-8);
|
|
6601
|
+
inline-size: 100%;
|
|
6602
|
+
min-block-size: var(--juno-size-tap-min);
|
|
6603
|
+
padding-inline: var(--juno-space-8);
|
|
6604
|
+
padding-block: var(--juno-space-4);
|
|
6605
|
+
border: none;
|
|
6606
|
+
border-radius: var(--juno-radius-3);
|
|
6607
|
+
background: transparent;
|
|
6608
|
+
color: inherit;
|
|
6609
|
+
font: inherit;
|
|
6610
|
+
text-align: start;
|
|
6611
|
+
text-decoration: none;
|
|
6612
|
+
cursor: pointer;
|
|
6613
|
+
}
|
|
6614
|
+
|
|
6615
|
+
.juno-tree__row:hover {
|
|
6616
|
+
background: var(--juno-s2);
|
|
6617
|
+
}
|
|
6618
|
+
|
|
6619
|
+
/* SELECTION is a different fact from hover and from aria-current, and the
|
|
6620
|
+
ticket is right that conflating them is the common bug: hover is where the
|
|
6621
|
+
pointer is, aria-current is which page you are on, aria-selected is what
|
|
6622
|
+
the next action will apply to. A layer stack has all three at once. */
|
|
6623
|
+
.juno-tree__item[aria-selected='true'] > .juno-tree__row {
|
|
6624
|
+
background: var(--juno-s3);
|
|
6625
|
+
color: var(--juno-role);
|
|
6626
|
+
}
|
|
6627
|
+
|
|
6628
|
+
.juno-tree__item[aria-current] > .juno-tree__row {
|
|
6629
|
+
box-shadow: inset var(--juno-border-width-2) 0 0 var(--juno-role);
|
|
6630
|
+
}
|
|
6631
|
+
|
|
6632
|
+
.juno-tree__row:focus-visible {
|
|
6633
|
+
outline: var(--juno-border-width-2) solid var(--juno-active);
|
|
6634
|
+
outline-offset: calc(-1 * var(--juno-border-width-2));
|
|
6635
|
+
}
|
|
6636
|
+
|
|
6637
|
+
/* ── disclosure caret ──────────────────────────────────────────────────
|
|
6638
|
+
aria-hidden and tabindex=-1 on purpose: the row itself is the treeitem
|
|
6639
|
+
and Left/Right already collapse and expand, so a separately focusable
|
|
6640
|
+
caret puts a second stop in the tab order for an action the row has. It
|
|
6641
|
+
stays a real button for the pointer. */
|
|
6642
|
+
.juno-tree__caret {
|
|
6643
|
+
flex-shrink: 0;
|
|
6644
|
+
inline-size: var(--juno-space-16);
|
|
6645
|
+
block-size: var(--juno-space-16);
|
|
6646
|
+
display: grid;
|
|
6647
|
+
place-items: center;
|
|
6648
|
+
padding: 0;
|
|
6649
|
+
border: none;
|
|
6650
|
+
background: transparent;
|
|
6651
|
+
color: var(--juno-label);
|
|
6652
|
+
cursor: pointer;
|
|
6653
|
+
transition: transform var(--juno-motion-duration-quick) var(--juno-motion-ease-standard);
|
|
6654
|
+
}
|
|
6655
|
+
|
|
6656
|
+
.juno-tree__caret::before {
|
|
6657
|
+
content: '';
|
|
6658
|
+
inline-size: 0;
|
|
6659
|
+
block-size: 0;
|
|
6660
|
+
border-block: calc(var(--juno-space-4) + 1px) solid transparent;
|
|
6661
|
+
border-inline-start: calc(var(--juno-space-4) + 2px) solid currentcolor;
|
|
6662
|
+
}
|
|
6663
|
+
|
|
6664
|
+
.juno-tree__item[aria-expanded='true'] > .juno-tree__row > .juno-tree__caret {
|
|
6665
|
+
transform: rotate(90deg);
|
|
6666
|
+
}
|
|
6667
|
+
|
|
6668
|
+
/* A leaf keeps the caret's box so labels line up down the column, but paints
|
|
6669
|
+
nothing — an empty gap would let sibling labels wander. */
|
|
6670
|
+
.juno-tree__item:not([aria-expanded]) > .juno-tree__row > .juno-tree__caret {
|
|
6671
|
+
visibility: hidden;
|
|
6672
|
+
cursor: default;
|
|
6673
|
+
}
|
|
6674
|
+
|
|
6675
|
+
.juno-tree__icon {
|
|
6676
|
+
flex-shrink: 0;
|
|
6677
|
+
color: var(--juno-label);
|
|
6678
|
+
}
|
|
6679
|
+
|
|
6680
|
+
.juno-tree__label {
|
|
6681
|
+
min-inline-size: 0;
|
|
6682
|
+
overflow: hidden;
|
|
6683
|
+
text-overflow: ellipsis;
|
|
6684
|
+
white-space: nowrap;
|
|
6685
|
+
}
|
|
6686
|
+
|
|
6687
|
+
/* Count badge on a group row. Pushed to the trailing edge with the rest of
|
|
6688
|
+
the trailing furniture. */
|
|
6689
|
+
.juno-tree__count {
|
|
6690
|
+
margin-inline-start: auto;
|
|
6691
|
+
flex-shrink: 0;
|
|
6692
|
+
font-size: var(--juno-font-size-11);
|
|
6693
|
+
font-variant-numeric: tabular-nums;
|
|
6694
|
+
color: var(--juno-muted);
|
|
6695
|
+
}
|
|
6696
|
+
|
|
6697
|
+
/* The same trailing-control slot .juno-list__row has: a switch, a menu
|
|
6698
|
+
trigger, a value. Follows the count when both are present. */
|
|
6699
|
+
.juno-tree__trail {
|
|
6700
|
+
margin-inline-start: auto;
|
|
6701
|
+
flex-shrink: 0;
|
|
6702
|
+
display: inline-flex;
|
|
6703
|
+
align-items: center;
|
|
6704
|
+
gap: var(--juno-space-8);
|
|
6705
|
+
}
|
|
6706
|
+
|
|
6707
|
+
.juno-tree__count + .juno-tree__trail {
|
|
6708
|
+
margin-inline-start: 0;
|
|
6709
|
+
}
|
|
6710
|
+
|
|
6711
|
+
/* ── reorder handle ────────────────────────────────────────────────────
|
|
6712
|
+
The AFFORDANCE and its hit area; the reorder logic is the app's, which is
|
|
6713
|
+
junoui's usual line. It is a real button and always visible on touch: a
|
|
6714
|
+
long-press-drag is the obvious gesture and the wrong one, because a tree
|
|
6715
|
+
sitting on or beside a pan/zoom surface has to let the pan win. An
|
|
6716
|
+
explicit handle is the only unambiguous target — see the doc.
|
|
6717
|
+
`touch-action: none` on the handle ALONE, so dragging it never scrolls
|
|
6718
|
+
while the rest of the row still pans normally. */
|
|
6719
|
+
.juno-tree__handle {
|
|
6720
|
+
flex-shrink: 0;
|
|
6721
|
+
inline-size: var(--juno-space-16);
|
|
6722
|
+
block-size: var(--juno-space-16);
|
|
6723
|
+
display: grid;
|
|
6724
|
+
place-items: center;
|
|
6725
|
+
padding: 0;
|
|
6726
|
+
border: none;
|
|
6727
|
+
background: transparent;
|
|
6728
|
+
color: var(--juno-muted);
|
|
6729
|
+
cursor: grab;
|
|
6730
|
+
touch-action: none;
|
|
6731
|
+
}
|
|
6732
|
+
|
|
6733
|
+
.juno-tree__handle:active {
|
|
6734
|
+
cursor: grabbing;
|
|
6735
|
+
}
|
|
6736
|
+
|
|
6737
|
+
.juno-tree__handle::before {
|
|
6738
|
+
content: '';
|
|
6739
|
+
inline-size: var(--juno-space-10);
|
|
6740
|
+
block-size: var(--juno-border-width-1);
|
|
6741
|
+
box-shadow:
|
|
6742
|
+
0 calc(-1 * var(--juno-space-4)) 0 currentcolor,
|
|
6743
|
+
0 var(--juno-space-4) 0 currentcolor;
|
|
6744
|
+
background: currentcolor;
|
|
6745
|
+
}
|
|
6746
|
+
|
|
6747
|
+
/* The row being dragged, while the app moves it. */
|
|
6748
|
+
.juno-tree__item[data-juno-dragging] > .juno-tree__row {
|
|
6749
|
+
opacity: var(--juno-opacity-muted);
|
|
6750
|
+
}
|
|
6751
|
+
|
|
6752
|
+
/* Drop target edge. A line rather than a filled row: the drop lands BETWEEN
|
|
6753
|
+
rows, and a filled highlight says "into this one", which is a different
|
|
6754
|
+
operation in a tree and the one users complain about. */
|
|
6755
|
+
.juno-tree__item[data-juno-drop='before'] > .juno-tree__row {
|
|
6756
|
+
box-shadow: inset 0 var(--juno-border-width-2) 0 var(--juno-active);
|
|
6757
|
+
}
|
|
6758
|
+
|
|
6759
|
+
.juno-tree__item[data-juno-drop='after'] > .juno-tree__row {
|
|
6760
|
+
box-shadow: inset 0 calc(-1 * var(--juno-border-width-2)) 0 var(--juno-active);
|
|
6761
|
+
}
|
|
6762
|
+
|
|
6763
|
+
.juno-tree__item[data-juno-drop='into'] > .juno-tree__row {
|
|
6764
|
+
outline: var(--juno-border-width-2) solid var(--juno-active);
|
|
6765
|
+
outline-offset: calc(-1 * var(--juno-border-width-2));
|
|
6766
|
+
}
|
|
6767
|
+
|
|
6768
|
+
/* ── touch ─────────────────────────────────────────────────────────────
|
|
6769
|
+
The caret and the handle paint small so a dense tree stays dense, but a
|
|
6770
|
+
16px target is not tappable. Grow the HIT AREA only, with a transparent
|
|
6771
|
+
overlay, exactly as .nx-check does downstream: a 44px painted caret would
|
|
6772
|
+
swallow the row it sits in. The row's own floor comes from
|
|
6773
|
+
--juno-size-tap-min, which base.css promotes on a coarse pointer. */
|
|
6774
|
+
@media (pointer: coarse) {
|
|
6775
|
+
.juno-tree__caret,
|
|
6776
|
+
.juno-tree__handle {
|
|
6777
|
+
position: relative;
|
|
6778
|
+
}
|
|
6779
|
+
|
|
6780
|
+
.juno-tree__caret::after,
|
|
6781
|
+
.juno-tree__handle::after {
|
|
6782
|
+
content: '';
|
|
6783
|
+
position: absolute;
|
|
6784
|
+
|
|
6785
|
+
/* (44 - 16) / 2 = 14 */
|
|
6786
|
+
inset: calc(-1 * (var(--juno-size-tap-comfortable) - var(--juno-space-16)) / 2);
|
|
6787
|
+
}
|
|
6788
|
+
}
|
|
6789
|
+
|
|
5789
6790
|
/* ════════════════════════════════════════════════════════════════════
|
|
5790
6791
|
* junoui — utilities layer
|
|
5791
6792
|
*
|