@junoput01/junoui 0.2.0 → 0.3.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 +32 -0
- package/dist/css/juno.css +162 -3
- package/docs/accessibility.md +167 -0
- package/docs/components/README.md +68 -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 +56 -0
- package/docs/components/drawer.md +60 -0
- package/docs/components/field.md +44 -0
- package/docs/components/gauge.md +48 -0
- package/docs/components/icon-loader.md +55 -0
- package/docs/components/icon.md +49 -0
- package/docs/components/input.md +45 -0
- package/docs/components/list.md +76 -0
- package/docs/components/loader.md +80 -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 +64 -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/segmented.md +46 -0
- package/docs/components/select.md +33 -0
- package/docs/components/skeleton.md +38 -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 +61 -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 +147 -0
- package/docs/flutter.md +42 -0
- package/docs/getting-started.md +70 -0
- package/docs/integration.md +95 -0
- package/docs/layout.md +201 -0
- package/docs/native.md +52 -0
- package/docs/roadmap.md +106 -0
- package/docs/tokens-reference.md +223 -0
- package/docs/web.md +89 -0
- package/package.json +3 -2
- package/src/css/base.css +7 -0
- package/src/css/components/dock.css +13 -0
- package/src/css/components/icon-loader.css +53 -0
- package/src/css/components/pillbar.css +15 -0
- package/src/css/components/rail.css +8 -0
- package/src/css/layout.css +65 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a7fb0b8: App-shell composition primitive + nav ergonomics — all CSS, all additive:
|
|
8
|
+
|
|
9
|
+
- `.juno-app-shell` — the standard product frame as classes instead of a
|
|
10
|
+
copy-pasted `<style>` block: `__body` column, `__topbar`, and a `__main`
|
|
11
|
+
scroller with a dock/pillbar pinned at its foot. Encodes `100dvh` (dock isn't
|
|
12
|
+
clipped by mobile browser chrome), main-as-scroller (dock stays put with no
|
|
13
|
+
`sticky`/`fixed`), and safe-area insets. Knob: `--juno-app-shell-topbar-size`.
|
|
14
|
+
- `.juno-rail--responsive` — the rail self-hides below `md`, so the rail↔dock
|
|
15
|
+
swap is one modifier instead of hanging `.juno-hide-below-md` yourself. rail.css
|
|
16
|
+
now cross-references the dock pairing.
|
|
17
|
+
- Viewport helpers extended to `sm` (640px) and `lg` (1024px), each with
|
|
18
|
+
`hide-below-*` / `hide-from-*`, plus readable `show-from-*` / `show-below-*`
|
|
19
|
+
inverse aliases.
|
|
20
|
+
- `.juno-dock--fixed` (pins flush to the foot) / `.juno-pillbar--fixed` (fixes
|
|
21
|
+
to the viewport, still floating its gap above the foot) — for
|
|
22
|
+
page-scroll shells, where `position: sticky` won't pin on a non-overflowing
|
|
23
|
+
column and the bar would land mid-content. Prefer `.juno-app-shell`, which
|
|
24
|
+
avoids the problem entirely.
|
|
25
|
+
- `.juno-icon-loader` — a nav destination's icon ringed by the spinning
|
|
26
|
+
[arc](../docs/components/loader.md) while its section loads. Icon static on
|
|
27
|
+
top; ring sized in `em`, pointer-transparent, kept concentric with a
|
|
28
|
+
single-cell grid (never `translate`, which the arc's rotation would clobber).
|
|
29
|
+
- Touch inputs hold a 16px font floor on coarse pointers, so iOS Safari no
|
|
30
|
+
longer zooms the page when a text field is focused.
|
|
31
|
+
- `docs/` now ships in the npm tarball, so the paths README and the CSS
|
|
32
|
+
comments point at (e.g. `docs/layout.md#app-shell`) resolve inside
|
|
33
|
+
`node_modules` for consumers.
|
|
34
|
+
|
|
3
35
|
## 0.2.0
|
|
4
36
|
|
|
5
37
|
### Minor Changes
|
package/dist/css/juno.css
CHANGED
|
@@ -440,6 +440,13 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
440
440
|
the contract; a phone just reads the bigger one. */
|
|
441
441
|
@media (pointer: coarse) {
|
|
442
442
|
:root { --juno-size-tap-min: var(--juno-size-tap-comfortable); }
|
|
443
|
+
|
|
444
|
+
/* iOS Safari zooms the page onto any focused text field whose font-size is
|
|
445
|
+
below 16px. Hold text-entry controls (.juno-input covers input, textarea
|
|
446
|
+
and the select's inner control) at a 16px floor on touch so focusing never
|
|
447
|
+
triggers that jump. max() keeps the floor even under a scaled-down
|
|
448
|
+
--juno-font-scale, and still grows when scaled up. */
|
|
449
|
+
.juno-input { font-size: max(16px, var(--juno-font-size-16)); }
|
|
443
450
|
}
|
|
444
451
|
|
|
445
452
|
@keyframes juno-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
|
@@ -564,11 +571,73 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
564
571
|
}
|
|
565
572
|
.juno-reel > * { scroll-snap-align: start; flex: 0 0 auto; }
|
|
566
573
|
|
|
574
|
+
/* App shell — the standard product frame, as classes instead of the
|
|
575
|
+
copy-pasted \3c style> block every consumer used to hand-roll. A rail beside
|
|
576
|
+
a body column; the body holds an optional topbar, a scrolling main region,
|
|
577
|
+
and a dock/pillbar pinned at its foot. The MAIN region is the scroller
|
|
578
|
+
(not the page), so the dock never needs sticky/fixed to stay put and the
|
|
579
|
+
mobile address bar can't clip it. Height is 100dvh (dynamic viewport) so
|
|
580
|
+
it fills the visible area under shrinking browser chrome. Pairs with the
|
|
581
|
+
viewport helpers below to swap rail↔dock across md; see docs/layout.md#app-shell.
|
|
582
|
+
<div class="juno-app-shell">
|
|
583
|
+
<nav class="juno-rail juno-rail--responsive" aria-label="Primary">…</nav>
|
|
584
|
+
<div class="juno-app-shell__body">
|
|
585
|
+
<header class="juno-app-shell__topbar">…</header>
|
|
586
|
+
<main class="juno-app-shell__main">…</main>
|
|
587
|
+
<nav class="juno-dock juno-hide-from-md" aria-label="Primary">…</nav>
|
|
588
|
+
</div>
|
|
589
|
+
</div> */
|
|
590
|
+
.juno-app-shell {
|
|
591
|
+
display: flex;
|
|
592
|
+
block-size: 100dvh;
|
|
593
|
+
overflow: hidden; /* the main region scrolls, not the shell */
|
|
594
|
+
padding-inline: env(safe-area-inset-left, 0) env(safe-area-inset-right, 0);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.juno-app-shell__body {
|
|
598
|
+
flex: 1;
|
|
599
|
+
min-inline-size: 0;
|
|
600
|
+
display: flex;
|
|
601
|
+
flex-direction: column;
|
|
602
|
+
min-block-size: 0; /* let __main shrink and scroll instead of the shell */
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.juno-app-shell__topbar {
|
|
606
|
+
flex-shrink: 0;
|
|
607
|
+
display: flex;
|
|
608
|
+
align-items: center;
|
|
609
|
+
gap: var(--juno-space-12);
|
|
610
|
+
min-block-size: var(--juno-app-shell-topbar-size, 46px);
|
|
611
|
+
padding-inline: var(--juno-pad-surface-inline);
|
|
612
|
+
padding-block-start: env(safe-area-inset-top, 0);
|
|
613
|
+
background: var(--juno-s1);
|
|
614
|
+
border-block-end: var(--juno-border-width-1) solid var(--juno-border);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.juno-app-shell__main {
|
|
618
|
+
flex: 1;
|
|
619
|
+
min-block-size: 0;
|
|
620
|
+
overflow: auto;
|
|
621
|
+
overscroll-behavior: contain;
|
|
622
|
+
padding: var(--juno-pad-surface-block) var(--juno-pad-surface-inline);
|
|
623
|
+
}
|
|
624
|
+
|
|
567
625
|
/* 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
|
-
|
|
626
|
+
can't express (e.g. swap a nav for a menu button). `hide-below-X` hides
|
|
627
|
+
under breakpoint X; `hide-from-X` hides at X and up. `show-*` are the
|
|
628
|
+
readable inverse aliases (show-from-X ≡ hide-below-X, show-below-X ≡
|
|
629
|
+
hide-from-X) — same media rule, name whichever reads clearer at the site. */
|
|
630
|
+
@media (width <= 639.98px) { .juno-hide-below-sm, .juno-show-from-sm { display: none !important; } }
|
|
631
|
+
|
|
632
|
+
@media (width >= 640px) { .juno-hide-from-sm, .juno-show-below-sm { display: none !important; } }
|
|
633
|
+
|
|
634
|
+
@media (width <= 767.98px) { .juno-hide-below-md, .juno-show-from-md { display: none !important; } }
|
|
635
|
+
|
|
636
|
+
@media (width >= 768px) { .juno-hide-from-md, .juno-show-below-md { display: none !important; } }
|
|
570
637
|
|
|
571
|
-
@media (width
|
|
638
|
+
@media (width <= 1023.98px) { .juno-hide-below-lg, .juno-show-from-lg { display: none !important; } }
|
|
639
|
+
|
|
640
|
+
@media (width >= 1024px) { .juno-hide-from-lg, .juno-show-below-lg { display: none !important; } }
|
|
572
641
|
|
|
573
642
|
/* ════════════════════════════════════════════════════════════════════
|
|
574
643
|
* junoui — density layer
|
|
@@ -1453,6 +1522,19 @@ button.juno-chip {
|
|
|
1453
1522
|
outline-offset: calc(-1 * var(--juno-space-2));
|
|
1454
1523
|
}
|
|
1455
1524
|
|
|
1525
|
+
/* Fixed — same story as the pillbar: sticky only pins while the column
|
|
1526
|
+
overflows, so on a short page the dock lands mid-content. Pin to the
|
|
1527
|
+
viewport foot in a page-scroll shell where content height varies. Since
|
|
1528
|
+
fixed leaves the flow, reserve its height at the page foot (e.g.
|
|
1529
|
+
padding-block-end) so it doesn't cover the last row. In-flow
|
|
1530
|
+
.juno-app-shell (main is the scroller, dock at the body foot) needs none
|
|
1531
|
+
of this — prefer it. */
|
|
1532
|
+
.juno-dock--fixed {
|
|
1533
|
+
position: fixed;
|
|
1534
|
+
inset-inline: 0;
|
|
1535
|
+
inset-block-end: 0;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1456
1538
|
/* ════════════════════════════════════════════════════════════════════
|
|
1457
1539
|
* Component — Status dot + label
|
|
1458
1540
|
* Usage:
|
|
@@ -1687,6 +1769,60 @@ button.juno-chip {
|
|
|
1687
1769
|
}
|
|
1688
1770
|
.juno-gauge--lg .juno-gauge__value { font-size: var(--juno-font-size-20); }
|
|
1689
1771
|
|
|
1772
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
1773
|
+
* Component — Icon loader (nav loading ring)
|
|
1774
|
+
* A destination icon ringed by the spinning arc while that section loads —
|
|
1775
|
+
* the "this is loading" affordance for a rail / dock / pillbar item. The
|
|
1776
|
+
* icon stays static on top; the arc sizes in em around it and eats no
|
|
1777
|
+
* pointer events, so the nav item still clicks through.
|
|
1778
|
+
*
|
|
1779
|
+
* The app owns the state: add .juno-arc--indeterminate to spin, drop it
|
|
1780
|
+
* when loaded (gate the blink to first-load, not every refetch). For a
|
|
1781
|
+
* determinate ring, set --juno-progress on the .juno-arc instead.
|
|
1782
|
+
* Usage:
|
|
1783
|
+
* <a class="juno-rail__item" href="…" aria-current="page">
|
|
1784
|
+
* <span class="juno-icon-loader">
|
|
1785
|
+
* <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-squares-four" /></svg>
|
|
1786
|
+
* <span class="juno-arc juno-arc--indeterminate" role="status" aria-label="Loading"></span>
|
|
1787
|
+
* </span>
|
|
1788
|
+
* <span class="juno-rail__label">Library</span>
|
|
1789
|
+
* </a>
|
|
1790
|
+
*
|
|
1791
|
+
* Gotcha encoded here: the arc's rotate animation overrides `transform`, so
|
|
1792
|
+
* the ring is centred with inset:0 + margin:auto — never translate(-50%,-50%),
|
|
1793
|
+
* which the spin would clobber.
|
|
1794
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
1795
|
+
|
|
1796
|
+
.juno-icon-loader {
|
|
1797
|
+
--juno-role: var(--juno-active);
|
|
1798
|
+
|
|
1799
|
+
/* Single-cell grid: the icon and the ring occupy the SAME cell and are
|
|
1800
|
+
both centred in it, so they're guaranteed concentric — no transform (the
|
|
1801
|
+
arc's rotation would clobber a translate) and no fragile margin math. The
|
|
1802
|
+
ring is the larger child, so the cell sizes to it (--juno-icon-loader-ring). */
|
|
1803
|
+
display: inline-grid;
|
|
1804
|
+
place-items: center;
|
|
1805
|
+
line-height: 0; /* drop the inline strut so the box hugs the ring */
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
.juno-icon-loader > .juno-icon,
|
|
1809
|
+
.juno-icon-loader > .juno-arc {
|
|
1810
|
+
grid-area: 1 / 1;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/* icon sits above the ring */
|
|
1814
|
+
.juno-icon-loader > .juno-icon {
|
|
1815
|
+
z-index: 1;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/* the ring: sized in em (relative to the icon), transparent to clicks */
|
|
1819
|
+
.juno-icon-loader > .juno-arc {
|
|
1820
|
+
--juno-arc-size: var(--juno-icon-loader-ring, 1.9em);
|
|
1821
|
+
--juno-arc-width: 0.14em;
|
|
1822
|
+
|
|
1823
|
+
pointer-events: none;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1690
1826
|
/* ════════════════════════════════════════════════════════════════════
|
|
1691
1827
|
* Component — Icon · inline SVG glyph
|
|
1692
1828
|
* A zero-JS icon primitive. Reference a symbol from the sprite; the glyph
|
|
@@ -2638,6 +2774,21 @@ button.juno-list__row:disabled {
|
|
|
2638
2774
|
flex-shrink: 0;
|
|
2639
2775
|
}
|
|
2640
2776
|
|
|
2777
|
+
/* Fixed — pin to the viewport instead of flowing at the column's end.
|
|
2778
|
+
Sticky only pins while the column overflows; on a short (non-scrolling)
|
|
2779
|
+
page the sticky pillbar lands mid-content. Use --fixed in a page-scroll
|
|
2780
|
+
shell where content height varies. Centered via inset-inline:0 + the base
|
|
2781
|
+
margin-inline:auto (NOT translateX, which the app-shell model avoids
|
|
2782
|
+
anyway). Leaves the viewport foot free — reserve space so it doesn't cover
|
|
2783
|
+
the last row (e.g. padding-block-end on the scroll region). Prefer the
|
|
2784
|
+
in-flow .juno-app-shell (main scrolls, pillbar sits at the body foot) —
|
|
2785
|
+
that needs no fixed at all. */
|
|
2786
|
+
.juno-pillbar--fixed {
|
|
2787
|
+
position: fixed;
|
|
2788
|
+
inset-inline: 0;
|
|
2789
|
+
z-index: var(--juno-z-anchored);
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2641
2792
|
/* reduced-transparency preference → solid surface, no blur */
|
|
2642
2793
|
@media (prefers-reduced-transparency: reduce) {
|
|
2643
2794
|
.juno-pillbar {
|
|
@@ -2817,6 +2968,14 @@ button.juno-list__row:disabled {
|
|
|
2817
2968
|
padding-inline: 0;
|
|
2818
2969
|
}
|
|
2819
2970
|
|
|
2971
|
+
/* Responsive — the rail self-hides below md, so you pair it with a
|
|
2972
|
+
.juno-dock (or .juno-pillbar) carrying .juno-hide-from-md and get the
|
|
2973
|
+
rail↔dock swap without hanging a juno-hide-below-md on the rail yourself.
|
|
2974
|
+
Reciprocal of the dock's md pairing; see dock.css / docs/layout.md#app-shell. */
|
|
2975
|
+
@media (width <= 767.98px) {
|
|
2976
|
+
.juno-rail--responsive { display: none !important; }
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2820
2979
|
/* ════════════════════════════════════════════════════════════════════
|
|
2821
2980
|
* Component — Data readout tile
|
|
2822
2981
|
* Usage:
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Accessibility
|
|
2
|
+
|
|
3
|
+
Accessibility is a primary goal of junoui, not an afterthought. The design system
|
|
4
|
+
ships the **look and the accessibility contract**; the _interaction behavior_ (focus
|
|
5
|
+
trapping, ARIA state updates, keyboard handlers) lives in your app or a sibling
|
|
6
|
+
`junoui-<framework>` package — but this page states exactly what junoui guarantees, to
|
|
7
|
+
which published standard, and what you must wire up.
|
|
8
|
+
|
|
9
|
+
## Standards we build to
|
|
10
|
+
|
|
11
|
+
junoui targets the international web accessibility standards and references the
|
|
12
|
+
specific criteria it meets — so a claim can be checked, not just trusted:
|
|
13
|
+
|
|
14
|
+
- **[WCAG 2.2](https://www.w3.org/TR/WCAG22/)** (W3C Recommendation) — **AA across the
|
|
15
|
+
board, AAA where it matters** (see the specific success criteria cited below).
|
|
16
|
+
- **[WAI-ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/)** roles/states/properties, and
|
|
17
|
+
the **[ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/patterns/)**
|
|
18
|
+
for the keyboard/interaction patterns in [the contract table](#the-aria-contract-per-component).
|
|
19
|
+
|
|
20
|
+
Criteria referenced on this page, by number:
|
|
21
|
+
|
|
22
|
+
| SC | Name | Level | Where |
|
|
23
|
+
| ---------- | --------------------------- | ----- | --------------------------------- |
|
|
24
|
+
| **1.4.1** | Use of Color | A | [Color](#color) |
|
|
25
|
+
| **1.4.3** | Contrast (Minimum) | AA | [Color](#color) |
|
|
26
|
+
| **1.4.6** | Contrast (Enhanced) | AAA | [Color](#color) |
|
|
27
|
+
| **1.4.11** | Non-text Contrast | AA | [Focus](#focus) |
|
|
28
|
+
| **2.4.7** | Focus Visible | AA | [Focus](#focus) |
|
|
29
|
+
| **2.4.11** | Focus Appearance | AA | [Focus](#focus) |
|
|
30
|
+
| **2.2.2** | Pause, Stop, Hide | A | [Motion](#motion) |
|
|
31
|
+
| **2.3.3** | Animation from Interactions | AAA | [Motion](#motion) |
|
|
32
|
+
| **2.5.8** | Target Size (Minimum) | AA | [Targets](#targets) |
|
|
33
|
+
| **2.5.5** | Target Size (Enhanced) | AAA | [Targets](#targets) |
|
|
34
|
+
| **1.4.12** | Text Spacing | AA | [Density](#density--text-spacing) |
|
|
35
|
+
|
|
36
|
+
This coverage is expected to grow: junoui adds standards references as components and
|
|
37
|
+
tokens land, and never removes a guarantee without a semver-major note.
|
|
38
|
+
|
|
39
|
+
## Color
|
|
40
|
+
|
|
41
|
+
- **Never make color the only signal** (WCAG **1.4.1** Use of Color, A). Every status
|
|
42
|
+
pairs color with a text label, icon, or shape. A badge says `WARNING`; a dot sits
|
|
43
|
+
beside its word.
|
|
44
|
+
- The **colorblind** palette (IBM Carbon universal set) is distinguishable across
|
|
45
|
+
deuteranopia, protanopia, and tritanopia. On the dark base–card surfaces (`s0`–`s2`)
|
|
46
|
+
every status role passes WCAG **1.4.6** Contrast (Enhanced), AAA (≥ 7:1); on the
|
|
47
|
+
selected/overlay surface (`s3`) it holds AA (≈ 7:1, ≥ 4.5:1). Use it for
|
|
48
|
+
accessibility-critical or universal audiences.
|
|
49
|
+
- Contrast (measured — regenerate with `node scripts/audit-contrast.mjs`):
|
|
50
|
+
- `data` (values) meets **1.4.6** (AAA, ≥ 7:1) on **every** surface `s0`–`s3`, both
|
|
51
|
+
themes and all three palettes (dark ≈ 13–17:1).
|
|
52
|
+
- `label` (secondary text) meets **1.4.3** Contrast (Minimum), AA (≥ 4.5:1) on **every**
|
|
53
|
+
surface `s0`–`s3` (dark ≈ 4.6–5.9:1). Never use it for values.
|
|
54
|
+
- Status roles (`nominal`/`active`/`caution`/`warning`/`target`) meet **1.4.3** AA on
|
|
55
|
+
`s0`–`s3`; most reach AAA on `s0`–`s2`.
|
|
56
|
+
- `muted` and `data-dim` are **decorative / WCAG-exempt roles** — disabled controls,
|
|
57
|
+
placeholders, faint tick labels. They intentionally sit below 4.5:1; never carry
|
|
58
|
+
meaningful text in them (a real timestamp or value uses `label`/`data`, not `data-dim`).
|
|
59
|
+
Exact values: [tokens-reference.md](./tokens-reference.md).
|
|
60
|
+
- **Surfaces & borders (dark).** Adjacent surface fills differ by only ~1.05:1, so a
|
|
61
|
+
block reads against its background through its **border**, not its fill. The dark
|
|
62
|
+
`border` / `border-strong` tokens are lifted (≈ 1.5:1 / 2.2:1 vs the base surface) so
|
|
63
|
+
cards, panels and dividers stay perceptible. Container boundaries are decorative and
|
|
64
|
+
exempt from **1.4.11** Non-text Contrast. Component-_identifying_ edges use the
|
|
65
|
+
`--juno-control-edge` family instead: input / checkbox / slider / avatar edges
|
|
66
|
+
(`-strong`, = `label`) clear ≥ 3:1 (dark ≈ 5.3:1) as **1.4.11** requires; table
|
|
67
|
+
gridlines (`--juno-control-edge`, = `muted`, ≈ 2.9:1) are supplementary rules — table
|
|
68
|
+
structure is carried by `<th>`/alignment, not the line — so they fall under the
|
|
69
|
+
decorative exemption.
|
|
70
|
+
|
|
71
|
+
## Focus
|
|
72
|
+
|
|
73
|
+
- A visible focus ring is defined in the base layer for links, buttons, inputs, and
|
|
74
|
+
anything with `[tabindex]`: 2px solid `--juno-active`, 2px offset. It tracks the
|
|
75
|
+
theme and is never removed (WCAG **2.4.7** Focus Visible, AA).
|
|
76
|
+
- The ring's size, offset and `active`-role contrast satisfy **2.4.11** Focus
|
|
77
|
+
Appearance (AA, WCAG 2.2) and **1.4.11** Non-text Contrast (AA) against adjacent
|
|
78
|
+
surfaces.
|
|
79
|
+
- Don't suppress `:focus-visible`. If you build a custom control, give it a tabstop
|
|
80
|
+
and let the ring apply.
|
|
81
|
+
- Text inputs / select / textarea swap the outline for an equivalent visible signal:
|
|
82
|
+
an `active` border + 1px ring. Never remove focus indication outright.
|
|
83
|
+
|
|
84
|
+
## Motion
|
|
85
|
+
|
|
86
|
+
- All animations respect `prefers-reduced-motion: reduce` (base layer slows them to
|
|
87
|
+
near-static). Loader pulses and the `--live` dot honor it automatically — covering
|
|
88
|
+
WCAG **2.2.2** Pause, Stop, Hide (A) and **2.3.3** Animation from Interactions (AAA).
|
|
89
|
+
- Use the `determinate` loaders when progress is known — less motion, more trust.
|
|
90
|
+
|
|
91
|
+
## Targets
|
|
92
|
+
|
|
93
|
+
- `--juno-size-tap-min` = 24px — WCAG **2.5.8** Target Size (Minimum), AA (WCAG 2.2).
|
|
94
|
+
- `--juno-size-tap-comfortable` = 44px — WCAG **2.5.5** Target Size (Enhanced), AAA;
|
|
95
|
+
recommended for primary actions on touch.
|
|
96
|
+
- `.juno-btn` already sets `min-height: var(--juno-size-tap-min)`.
|
|
97
|
+
- **On coarse pointers this is automatic:** under `@media (pointer: coarse)` the
|
|
98
|
+
base layer raises `--juno-size-tap-min` to the comfortable 44px, so every
|
|
99
|
+
control sized off the tap minimum grows on touch devices. A cascade override —
|
|
100
|
+
the token values themselves don't change.
|
|
101
|
+
- Hover-revealed affordances get a touch fallback: table row actions stay
|
|
102
|
+
visible under `@media (hover: none)`.
|
|
103
|
+
|
|
104
|
+
## Forced colors / high contrast
|
|
105
|
+
|
|
106
|
+
- The base layer opts components into the system palette under
|
|
107
|
+
`@media (forced-colors: active)`: borders fall back to `CanvasText`, focus to
|
|
108
|
+
`Highlight`. Badges keep their fill (`forced-color-adjust: none`) so status stays
|
|
109
|
+
legible.
|
|
110
|
+
|
|
111
|
+
## Writing direction (RTL)
|
|
112
|
+
|
|
113
|
+
- Components use **CSS logical properties** (`margin-inline`, `padding-inline`,
|
|
114
|
+
`inset`, `min-inline-size`) so they mirror correctly under `dir="rtl"` with no
|
|
115
|
+
extra CSS. Avoid reintroducing physical `left`/`right` in consuming code.
|
|
116
|
+
|
|
117
|
+
## Density & text spacing
|
|
118
|
+
|
|
119
|
+
- Type tokens set line-height and spacing with enough headroom that user or
|
|
120
|
+
browser-imposed **text spacing** (line-height 1.5×, paragraph/letter/word spacing)
|
|
121
|
+
doesn't clip or overlap content — WCAG **1.4.12** Text Spacing (AA).
|
|
122
|
+
- Compact density shrinks padding, never a control's `min-height` (the tap target
|
|
123
|
+
under [Targets](#targets)) and never the type scale — readability holds at every
|
|
124
|
+
density.
|
|
125
|
+
|
|
126
|
+
## The ARIA contract (per component)
|
|
127
|
+
|
|
128
|
+
junoui gives structure + style; you add roles/state:
|
|
129
|
+
|
|
130
|
+
| Component | You must add |
|
|
131
|
+
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
132
|
+
| Badge / status dot | Text label in the DOM (not color alone). For live values, `aria-live="polite"`. |
|
|
133
|
+
| Button | Use a real `<button>`; `disabled` for disabled (not just the class). |
|
|
134
|
+
| Card | Heading semantics (`<h2>`…) in `__head` if it titles a region. |
|
|
135
|
+
| Loader (indeterminate) | `role="status"` + `aria-label="Loading"`. |
|
|
136
|
+
| Loader (determinate) | `role="progressbar"` + `aria-valuenow/min/max`. |
|
|
137
|
+
| Input / select / textarea | A `<label>` tied by `for`/`id`. Errors: `aria-invalid="true"` + `aria-describedby` → the error text. Required: `aria-required`. |
|
|
138
|
+
| Checkbox / radio | Real `<input>` inside its `<label>`; radios share a `name`. State is native `checked`. |
|
|
139
|
+
| Switch | `role="switch"` on the input; app keeps `checked` + `aria-checked` in sync. |
|
|
140
|
+
| Toggle button | Real `<button>` with `aria-pressed`; app flips it. Legend text is in the DOM, not color alone. |
|
|
141
|
+
| Slider | Value-driven `div`: add `role="slider"`, `tabindex`, `aria-valuenow/min/max` (+ `aria-valuetext` for units); app wires drag + arrow/Page keys and keeps `--juno-slider-pct` in sync. |
|
|
142
|
+
| Modal / drawer | Native `<dialog>` + `showModal()` (focus-trap, ESC, inert background, scrim-click come free). Name it via `aria-labelledby` → the title. |
|
|
143
|
+
| Tooltip | Trigger needs a tabstop; bubble `role="tooltip"` + `aria-describedby` so it reveals on focus, not only hover. Promote to `popover="hint"` to escape ancestor clipping (top layer). |
|
|
144
|
+
| Popover | Native `popover` + `popovertarget` button (top layer; ESC + outside-click dismiss + `aria-expanded` come free). Keep `aria-haspopup`; move focus in on open, restore on close. |
|
|
145
|
+
| Menu / dropdown | `role="menu"` + `role="menuitem"`; native `popover` + `popovertarget` (free dismiss/ESC/`aria-expanded`), items `popovertargetaction="hide"`. App wires arrow-key roving focus + Enter. |
|
|
146
|
+
| Table / data grid | Real `<table>`/`<th>`/`<td>` (use `scope`). Sortable headers: `aria-sort` per column, kept in sync as you reorder. Selected rows: `aria-selected="true"`. Truncated cells: a `title` (or tooltip) with the full value. |
|
|
147
|
+
| Alert | `role="alert"` for urgent (assertive), `role="status"` for passive. Icon is `aria-hidden`; the message text carries the meaning. Dismiss is a real `<button>` with `aria-label`. |
|
|
148
|
+
| Toast | Stack is a live region: `aria-live="polite"` (`assertive` for errors). Each toast `role="status"`; keep it short. Dismiss `<button>` + `aria-label`; don't rely on the auto-timeout alone for critical info. |
|
|
149
|
+
| Tabs | `role="tablist"` + `role="tab"` (each `aria-controls` its `role="tabpanel"`, which is `aria-labelledby` the tab). App keeps one `aria-selected="true"`, hides the rest, moves roving `tabindex`, and wires ← / → / Home / End. |
|
|
150
|
+
| Accordion | Native `<details>`/`<summary>` — open state, focus, and Enter/Space are built in. For one-open-at-a-time, give the `<details>` a shared `name`. No ARIA to add. |
|
|
151
|
+
| Icon | Decorative: `aria-hidden="true"`. Meaningful (icon-only control): `role="img"` + `aria-label`, or label the surrounding control. Never the only signal for status. |
|
|
152
|
+
| Skeleton | Wrap the loading region in `aria-busy="true"` (+ `aria-live="polite"`); swap in real content when loaded. The shimmer itself is decorative. |
|
|
153
|
+
| Avatar | `<img>` needs a meaningful `alt`. The status ring is decorative — pair it with a `title` / visible label so status isn't color-only. |
|
|
154
|
+
| Divider | Semantic break: use `<hr>`. Purely decorative: a `<span>`/`<div>` (no role needed). |
|
|
155
|
+
| Chip / tag | Toggle chip: real `<button>` + `aria-pressed`. Removable: the `__remove` `<button>` needs an `aria-label` ("Remove X"). Meaning is in the text, not color. |
|
|
156
|
+
| Breadcrumb | `<nav aria-label="Breadcrumb">` around an `<ol>`; mark the last crumb `aria-current="page"` and drop its `href`. |
|
|
157
|
+
| Pagination | `<nav aria-label="Pagination">`; current page `aria-current="page"`; icon-only prev/next need `aria-label`; disable the ends with `disabled`. |
|
|
158
|
+
| Stepper | Real `<ol>`; set `data-state` per step and `aria-current="step"` on the active one. State drives the color — don't rely on color alone. |
|
|
159
|
+
| Rail / dock | `<nav aria-label="Primary">` of real links; active item `aria-current="page"`. Collapsed rail items (icons-only) need `aria-label`/`title`. When both navs render (rail + dock swap), the hidden one leaves the tree via `display:none`. |
|
|
160
|
+
| Pillbar | `<nav aria-label>` when it navigates; links get `aria-current="page"`, toggle buttons `aria-pressed`. Icon-only items need `aria-label`. Never render both a dock and a pillbar on one screen. |
|
|
161
|
+
| Navbar | Back is a real `<a>` (parent route) or `<button>` (history); `aria-label="Back"` when the caret has no text. Title is usually the screen's `<h1>`. The bar itself needs no role — it's a `<header>`. |
|
|
162
|
+
| List | Rows that navigate are `<a>` (or `<button>`) wrapping the whole row — never a bare chevron target. Keep `<ul>`/`<li>` when rows form a set. A trailing switch/checkbox is its own labeled control; don't nest it inside a link row. |
|
|
163
|
+
|
|
164
|
+
These are interaction concerns — junoui can't enforce them in CSS, so they're your
|
|
165
|
+
responsibility (or the widget package's). Each row follows the matching
|
|
166
|
+
[WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/patterns/) pattern;
|
|
167
|
+
when in doubt, implement the APG keyboard model for that role.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
Framework-agnostic elements built from tokens. On the web they're CSS classes in
|
|
4
|
+
`@junoput01/junoui/css`; on other platforms, build the same anatomy from
|
|
5
|
+
[tokens-reference.md](../tokens-reference.md).
|
|
6
|
+
|
|
7
|
+
## The role mechanism (web)
|
|
8
|
+
|
|
9
|
+
A component reads a single custom property, `--juno-role`. A role class sets it, so
|
|
10
|
+
**one class re-colors any component**:
|
|
11
|
+
|
|
12
|
+
```html
|
|
13
|
+
<span class="juno-badge juno--warning">…</span>
|
|
14
|
+
<!-- role = warning -->
|
|
15
|
+
<button class="juno-btn juno--nominal">…</button>
|
|
16
|
+
<!-- role = nominal -->
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Role classes: `.juno--nominal` `.juno--active` `.juno--target` `.juno--caution`
|
|
20
|
+
`.juno--warning` `.juno--muted`.
|
|
21
|
+
|
|
22
|
+
## Catalogue
|
|
23
|
+
|
|
24
|
+
| Component | Class | Spec |
|
|
25
|
+
| ------------------------------ | -------------------------------------- | -------------------------------------- |
|
|
26
|
+
| Badge / status indicator | `.juno-badge` | [badge.md](./badge.md) |
|
|
27
|
+
| Button | `.juno-btn` | [button.md](./button.md) |
|
|
28
|
+
| Card / panel | `.juno-card` | [card.md](./card.md) |
|
|
29
|
+
| Data readout | `.juno-readout` | [readout.md](./readout.md) |
|
|
30
|
+
| Status dot | `.juno-status` | [status.md](./status.md) |
|
|
31
|
+
| Loaders (arc · beacon · bar) | `.juno-arc` `.juno-beacon` `.juno-bar` | [loader.md](./loader.md) |
|
|
32
|
+
| Gauge (metric ring) | `.juno-gauge` | [gauge.md](./gauge.md) |
|
|
33
|
+
| Spark (sparkline contract) | `.juno-spark` | [spark.md](./spark.md) |
|
|
34
|
+
| Field wrapper | `.juno-field` | [field.md](./field.md) |
|
|
35
|
+
| Input / textarea | `.juno-input` | [input.md](./input.md) |
|
|
36
|
+
| Select | `.juno-select` | [select.md](./select.md) |
|
|
37
|
+
| Checkbox / radio | `.juno-checkbox` `.juno-radio` | [checkbox.md](./checkbox.md) |
|
|
38
|
+
| Switch | `.juno-switch` | [switch.md](./switch.md) |
|
|
39
|
+
| Segmented control | `.juno-seg` | [segmented.md](./segmented.md) |
|
|
40
|
+
| Toggle button | `.juno-toggle-btn` | [toggle-button.md](./toggle-button.md) |
|
|
41
|
+
| Slider | `.juno-slider` | [slider.md](./slider.md) |
|
|
42
|
+
| Modal / dialog | `.juno-modal` | [modal.md](./modal.md) |
|
|
43
|
+
| Drawer | `.juno-drawer` | [drawer.md](./drawer.md) |
|
|
44
|
+
| Tooltip | `.juno-tooltip` | [tooltip.md](./tooltip.md) |
|
|
45
|
+
| Popover | `.juno-popover` | [popover.md](./popover.md) |
|
|
46
|
+
| Menu / dropdown | `.juno-menu` | [menu.md](./menu.md) |
|
|
47
|
+
| Table / data grid | `.juno-table` | [table.md](./table.md) |
|
|
48
|
+
| Alert / inline notification | `.juno-alert` | [alert.md](./alert.md) |
|
|
49
|
+
| Toast / snackbar | `.juno-toast` | [toast.md](./toast.md) |
|
|
50
|
+
| Tabs | `.juno-tabs` | [tabs.md](./tabs.md) |
|
|
51
|
+
| Accordion / disclosure | `.juno-accordion` | [accordion.md](./accordion.md) |
|
|
52
|
+
| Icon | `.juno-icon` | [icon.md](./icon.md) |
|
|
53
|
+
| Skeleton / loading placeholder | `.juno-skeleton` | [skeleton.md](./skeleton.md) |
|
|
54
|
+
| Thumb / media placeholder | `.juno-thumb` | [thumb.md](./thumb.md) |
|
|
55
|
+
| Avatar | `.juno-avatar` | [avatar.md](./avatar.md) |
|
|
56
|
+
| Divider / separator | `.juno-divider` | [divider.md](./divider.md) |
|
|
57
|
+
| Chip / tag | `.juno-chip` | [chip.md](./chip.md) |
|
|
58
|
+
| Breadcrumb | `.juno-breadcrumb` | [breadcrumb.md](./breadcrumb.md) |
|
|
59
|
+
| Pagination | `.juno-pagination` | [pagination.md](./pagination.md) |
|
|
60
|
+
| Stepper | `.juno-stepper` | [stepper.md](./stepper.md) |
|
|
61
|
+
| Rail (app-shell nav) | `.juno-rail` | [rail.md](./rail.md) |
|
|
62
|
+
| Dock (bottom nav, narrow) | `.juno-dock` | [dock.md](./dock.md) |
|
|
63
|
+
| Pillbar (floating pill bar) | `.juno-pillbar` | [pillbar.md](./pillbar.md) |
|
|
64
|
+
| Icon loader (nav loading ring) | `.juno-icon-loader` | [icon-loader.md](./icon-loader.md) |
|
|
65
|
+
| Navbar (stack top bar) | `.juno-navbar` | [navbar.md](./navbar.md) |
|
|
66
|
+
| List (grouped rows) | `.juno-list` | [list.md](./list.md) |
|
|
67
|
+
|
|
68
|
+
Always pair a status color with a text label — color is never the only signal.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Accordion / disclosure
|
|
2
|
+
|
|
3
|
+
Stacked, expandable sections built on the native `<details>` / `<summary>` — keyboard and
|
|
4
|
+
screen-reader native, **zero JS**. A chevron rotates on open. Use a lone `<details>` for a
|
|
5
|
+
single disclosure, or group them in `.juno-accordion` for a bordered set.
|
|
6
|
+
|
|
7
|
+
## Web
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<div class="juno-accordion">
|
|
11
|
+
<details class="juno-accordion__item" open>
|
|
12
|
+
<summary class="juno-accordion__summary">Region · us-east-1</summary>
|
|
13
|
+
<div class="juno-accordion__body">Primary region. 3 replicas, auto-scaling on.</div>
|
|
14
|
+
</details>
|
|
15
|
+
<details class="juno-accordion__item">
|
|
16
|
+
<summary class="juno-accordion__summary">Region · eu-west-1</summary>
|
|
17
|
+
<div class="juno-accordion__body">Failover armed. 1 of 3 replicas reporting.</div>
|
|
18
|
+
</details>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- lone disclosure (frames itself) -->
|
|
22
|
+
<details class="juno-accordion__item">
|
|
23
|
+
<summary class="juno-accordion__summary">Advanced options</summary>
|
|
24
|
+
<div class="juno-accordion__body">…</div>
|
|
25
|
+
</details>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Class | Effect |
|
|
29
|
+
| -------------------------- | -------------------------------------------------------------- |
|
|
30
|
+
| `.juno-accordion` | Bordered group; clips its items' corners. |
|
|
31
|
+
| `.juno-accordion__item` | A `<details>`; row rule between items. `[open]` to start open. |
|
|
32
|
+
| `.juno-accordion__summary` | The `<summary>` header; native marker hidden, chevron added. |
|
|
33
|
+
| `.juno-accordion__body` | The revealed content (`s0`, `label` text). |
|
|
34
|
+
|
|
35
|
+
## Anatomy (any platform)
|
|
36
|
+
|
|
37
|
+
- Summary: `s1` row, `data` label, control-padding; hover → `s2`. A chevron (a rotated
|
|
38
|
+
corner) flips from ▸ to ▾ via `[open]`, `motion.duration.quick`.
|
|
39
|
+
- Body: `s0` panel under a 1px rule. Native open/close (no height animation by default).
|
|
40
|
+
- A lone `details.juno-accordion__item:only-child` draws its own border + radius.
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
- **Zero JS** — `<details>` toggles itself; `<summary>` is focusable and Enter/Space work.
|
|
45
|
+
For "only one open at a time", give the `<details>` a shared `name` attribute (native
|
|
46
|
+
exclusive accordion) — no script needed.
|
|
47
|
+
- Keep the summary a short label; put detail in the body.
|
|
48
|
+
- To animate open/close, progressively enhance with `::details-content` +
|
|
49
|
+
`interpolate-size` where supported; the component stays correct without it.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Alert / inline notification
|
|
2
|
+
|
|
3
|
+
A role-tinted banner for in-context messages — validation summaries, system status,
|
|
4
|
+
callouts. Soft-fill with a role accent rail; icon + title + text + optional actions and
|
|
5
|
+
dismiss. Color comes from `var(--juno-role)` (set by a `.juno--<role>` class).
|
|
6
|
+
|
|
7
|
+
## Web
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<div class="juno-alert juno--warning" role="alert">
|
|
11
|
+
<span class="juno-alert__icon" aria-hidden="true">⚠</span>
|
|
12
|
+
<div class="juno-alert__body">
|
|
13
|
+
<span class="juno-alert__title">Deploy blocked</span>
|
|
14
|
+
<span class="juno-alert__text">Two required checks are still failing.</span>
|
|
15
|
+
<div class="juno-alert__actions">
|
|
16
|
+
<button class="juno-btn juno--warning">RE-RUN</button>
|
|
17
|
+
<button class="juno-btn juno-btn--ghost">VIEW LOG</button>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<button class="juno-alert__close" aria-label="Dismiss">✕</button>
|
|
21
|
+
</div>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Class | Effect |
|
|
25
|
+
| ---------------------- | --------------------------------------------------------------------------- |
|
|
26
|
+
| `.juno-alert` | Soft role wash + role border + start accent rail. Default `active`. |
|
|
27
|
+
| `.juno-alert__icon` | Leading glyph, role-colored. Mark it `aria-hidden`. |
|
|
28
|
+
| `.juno-alert__body` | `__title` (data, semibold) + `__text` (label) column. |
|
|
29
|
+
| `.juno-alert__actions` | Row of buttons under the text. |
|
|
30
|
+
| `.juno-alert__close` | Ghost dismiss button (app removes the alert). |
|
|
31
|
+
| `.juno-alert--solid` | High-emphasis: full role fill, text on `s0`. |
|
|
32
|
+
| `.juno--<role>` | Sets the color (`active` info · `nominal` success · `caution` · `warning`). |
|
|
33
|
+
|
|
34
|
+
## Anatomy (any platform)
|
|
35
|
+
|
|
36
|
+
- Surface: `color-mix(role 10%)` wash, `color-mix(role 30%)` border, `border.width.3`
|
|
37
|
+
role rail on the inline-start edge, `radius.5`, surface padding.
|
|
38
|
+
- Icon + title (`data`, semibold) + text (`label`); `--solid` swaps to a full role fill.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
- **Inline, not floating** — alerts sit in the layout. For transient confirmations use a
|
|
43
|
+
[toast](./toast.md).
|
|
44
|
+
- `role="alert"` for urgent (assertive announce); `role="status"` for passive.
|
|
45
|
+
- Pair the role color with the icon **and** a word — never color alone.
|
|
46
|
+
- Dismiss is the app's job: remove the node (`showcase/app.js` wires `.juno-alert__close`).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Avatar
|
|
2
|
+
|
|
3
|
+
A round (or squared) portrait holding an `<img>`, **initials**, or an
|
|
4
|
+
[icon](./icon.md) fallback. One custom property drives the size; an optional role ring
|
|
5
|
+
marks status. Zero JS. Pairs with a [`.juno-skeleton--circle`](./skeleton.md) while the
|
|
6
|
+
image loads.
|
|
7
|
+
|
|
8
|
+
## Web
|
|
9
|
+
|
|
10
|
+
```html
|
|
11
|
+
<!-- image -->
|
|
12
|
+
<span class="juno-avatar"><img src="ada.jpg" alt="Ada Lovelace" /></span>
|
|
13
|
+
|
|
14
|
+
<!-- initials -->
|
|
15
|
+
<span class="juno-avatar juno-avatar--lg">AL</span>
|
|
16
|
+
|
|
17
|
+
<!-- icon fallback + status ring -->
|
|
18
|
+
<span class="juno-avatar juno-avatar--ring juno--nominal" title="Online">
|
|
19
|
+
<svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-user" /></svg>
|
|
20
|
+
</span>
|
|
21
|
+
|
|
22
|
+
<!-- overlapping cluster -->
|
|
23
|
+
<span class="juno-avatar-group">
|
|
24
|
+
<span class="juno-avatar"><img src="a.jpg" alt="Ada" /></span>
|
|
25
|
+
<span class="juno-avatar"><img src="g.jpg" alt="Grace" /></span>
|
|
26
|
+
<span class="juno-avatar">+3</span>
|
|
27
|
+
</span>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Class | Effect |
|
|
31
|
+
| ------------------------ | ----------------------------------------------------------- |
|
|
32
|
+
| `.juno-avatar` | Circle portrait; `--juno-avatar-size` (default `space.32`). |
|
|
33
|
+
| `.juno-avatar--sm/lg/xl` | `space.24` / `space.40` / `space.56`. |
|
|
34
|
+
| `.juno-avatar--square` | Rounded-rect instead of circle. |
|
|
35
|
+
| `.juno-avatar--ring` | Status ring in `var(--juno-role)` (+ soft halo). |
|
|
36
|
+
| `.juno-avatar-group` | Overlapping row; each tucks under the previous. |
|
|
37
|
+
| `--juno-avatar-size` | Override the size (initials scale to ~40% of it). |
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
- An `<img>` fills the box (`object-fit: cover`); always set a meaningful `alt`.
|
|
42
|
+
- Initials / `+N` overflow are plain text content — center-aligned, uppercase.
|
|
43
|
+
- The **ring is decorative**: pair it with a `title` / visible label so status isn't
|
|
44
|
+
color-only.
|
|
45
|
+
- In a group, set `alt` per avatar; the trailing `+N` chip is just another avatar.
|