@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/css/juno.css +162 -3
  3. package/docs/accessibility.md +167 -0
  4. package/docs/components/README.md +68 -0
  5. package/docs/components/accordion.md +49 -0
  6. package/docs/components/alert.md +46 -0
  7. package/docs/components/avatar.md +45 -0
  8. package/docs/components/badge.md +35 -0
  9. package/docs/components/breadcrumb.md +31 -0
  10. package/docs/components/button.md +32 -0
  11. package/docs/components/card.md +41 -0
  12. package/docs/components/checkbox.md +41 -0
  13. package/docs/components/chip.md +44 -0
  14. package/docs/components/divider.md +32 -0
  15. package/docs/components/dock.md +56 -0
  16. package/docs/components/drawer.md +60 -0
  17. package/docs/components/field.md +44 -0
  18. package/docs/components/gauge.md +48 -0
  19. package/docs/components/icon-loader.md +55 -0
  20. package/docs/components/icon.md +49 -0
  21. package/docs/components/input.md +45 -0
  22. package/docs/components/list.md +76 -0
  23. package/docs/components/loader.md +80 -0
  24. package/docs/components/menu.md +54 -0
  25. package/docs/components/modal.md +52 -0
  26. package/docs/components/navbar.md +53 -0
  27. package/docs/components/pagination.md +42 -0
  28. package/docs/components/pillbar.md +64 -0
  29. package/docs/components/popover.md +43 -0
  30. package/docs/components/rail.md +54 -0
  31. package/docs/components/readout.md +39 -0
  32. package/docs/components/segmented.md +46 -0
  33. package/docs/components/select.md +33 -0
  34. package/docs/components/skeleton.md +38 -0
  35. package/docs/components/slider.md +48 -0
  36. package/docs/components/spark.md +51 -0
  37. package/docs/components/status.md +30 -0
  38. package/docs/components/stepper.md +46 -0
  39. package/docs/components/switch.md +38 -0
  40. package/docs/components/table.md +126 -0
  41. package/docs/components/tabs.md +61 -0
  42. package/docs/components/thumb.md +61 -0
  43. package/docs/components/toast.md +48 -0
  44. package/docs/components/toggle-button.md +40 -0
  45. package/docs/components/tooltip.md +63 -0
  46. package/docs/design-guidelines.md +147 -0
  47. package/docs/flutter.md +42 -0
  48. package/docs/getting-started.md +70 -0
  49. package/docs/integration.md +95 -0
  50. package/docs/layout.md +201 -0
  51. package/docs/native.md +52 -0
  52. package/docs/roadmap.md +106 -0
  53. package/docs/tokens-reference.md +223 -0
  54. package/docs/web.md +89 -0
  55. package/package.json +3 -2
  56. package/src/css/base.css +7 -0
  57. package/src/css/components/dock.css +13 -0
  58. package/src/css/components/icon-loader.css +53 -0
  59. package/src/css/components/pillbar.css +15 -0
  60. package/src/css/components/rail.css +8 -0
  61. 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
- @media (width <= 767.98px) { .juno-hide-below-md { display: none !important; } }
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 >= 768px) { .juno-hide-from-md { display: none !important; } }
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.