@junoput01/junoui 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +219 -0
  2. package/README.md +23 -19
  3. package/dist/css/juno-custom-media.css +32 -0
  4. package/dist/css/juno.css +1216 -60
  5. package/dist/icons/inline.js +22 -0
  6. package/docs/accessibility.md +55 -34
  7. package/docs/boot-shell.md +295 -0
  8. package/docs/components/README.md +46 -43
  9. package/docs/components/dock.md +167 -8
  10. package/docs/components/drawer.md +47 -7
  11. package/docs/components/fold-slot.md +31 -0
  12. package/docs/components/icon-loader.md +85 -18
  13. package/docs/components/icon.md +21 -0
  14. package/docs/components/load-state.md +131 -0
  15. package/docs/components/loader.md +13 -5
  16. package/docs/components/pillbar.md +157 -10
  17. package/docs/components/reload.md +41 -0
  18. package/docs/components/skeleton.md +22 -15
  19. package/docs/components/thumb.md +34 -14
  20. package/docs/design-guidelines.md +26 -0
  21. package/docs/getting-started.md +23 -0
  22. package/docs/ios-conformance.md +224 -0
  23. package/docs/layout.md +71 -1
  24. package/docs/web.md +6 -0
  25. package/package.json +5 -2
  26. package/src/css/base.css +169 -6
  27. package/src/css/components/dock.css +322 -0
  28. package/src/css/components/drawer.css +40 -3
  29. package/src/css/components/fold-slot.css +44 -0
  30. package/src/css/components/icon-loader.css +32 -18
  31. package/src/css/components/icon.css +6 -4
  32. package/src/css/components/load-state.css +136 -0
  33. package/src/css/components/loader.css +6 -0
  34. package/src/css/components/menu.css +4 -0
  35. package/src/css/components/modal.css +24 -3
  36. package/src/css/components/navbar.css +5 -1
  37. package/src/css/components/pillbar.css +207 -7
  38. package/src/css/components/reload.css +48 -0
  39. package/src/css/components/skeleton.css +41 -13
  40. package/src/css/components/tabs.css +5 -0
  41. package/src/css/components/thumb.css +63 -1
  42. package/src/css/components/toast.css +5 -1
  43. package/src/css/density.css +22 -0
  44. package/src/css/layout.css +30 -2
  45. package/src/css/utilities.css +4 -1
@@ -21,48 +21,51 @@ Role classes: `.juno--nominal` `.juno--active` `.juno--target` `.juno--caution`
21
21
 
22
22
  ## Catalogue
23
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) |
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
+ | Load state (shimmer/fault/empty) | `.juno-shimmer` `.juno-fault` `.juno-empty` | [load-state.md](./load-state.md) |
55
+ | Reload (refetch-in-place) | `.juno-reload` | [reload.md](./reload.md) |
56
+ | Thumb / media placeholder | `.juno-thumb` | [thumb.md](./thumb.md) |
57
+ | Avatar | `.juno-avatar` | [avatar.md](./avatar.md) |
58
+ | Divider / separator | `.juno-divider` | [divider.md](./divider.md) |
59
+ | Chip / tag | `.juno-chip` | [chip.md](./chip.md) |
60
+ | Breadcrumb | `.juno-breadcrumb` | [breadcrumb.md](./breadcrumb.md) |
61
+ | Pagination | `.juno-pagination` | [pagination.md](./pagination.md) |
62
+ | Stepper | `.juno-stepper` | [stepper.md](./stepper.md) |
63
+ | Rail (app-shell nav) | `.juno-rail` | [rail.md](./rail.md) |
64
+ | Dock (bottom nav, narrow) | `.juno-dock` | [dock.md](./dock.md) |
65
+ | Pillbar (floating pill bar) | `.juno-pillbar` | [pillbar.md](./pillbar.md) |
66
+ | Fold slot (animated presence) | `.juno-fold` | [fold-slot.md](./fold-slot.md) |
67
+ | Icon loader (rings any control) | `.juno-icon-loader` | [icon-loader.md](./icon-loader.md) |
68
+ | Navbar (stack top bar) | `.juno-navbar` | [navbar.md](./navbar.md) |
69
+ | List (grouped rows) | `.juno-list` | [list.md](./list.md) |
67
70
 
68
71
  Always pair a status color with a text label — color is never the only signal.
@@ -19,14 +19,173 @@ destinations as icon-over-label tap targets. The phone-width counterpart of the
19
19
  </nav>
20
20
  ```
21
21
 
22
- | Class / prop | Effect |
23
- | ------------------- | --------------------------------------------------------------------------------------------- |
24
- | `.juno-dock` | Sticky bottom `s1` bar, 1px `border` seam on top, safe-area pad. |
25
- | `.juno-dock__item` | Equal-width icon-over-label target, ≥ `size.tap.comfortable` tall. |
26
- | `.juno-dock__label` | The text — truncates with an ellipsis, never wraps. |
27
- | `[aria-current]` | Active item: `s2` fill + 2px role edge on top. Attribute, not class. |
28
- | `.juno-dock--fixed` | Pin to the viewport foot (`position: fixed`) — for page-scroll shells where sticky won't pin. |
29
- | `.juno--<role>` | Active-edge color (default `active`). |
22
+ | Class / prop | Effect |
23
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
24
+ | `.juno-dock` | Sticky bottom `s1` bar, 1px `border` seam on top, safe-area pad. |
25
+ | `.juno-dock__item` | Equal-width icon-over-label target, ≥ `size.tap.comfortable` tall. |
26
+ | `.juno-dock__label` | The text — truncates with an ellipsis, never wraps. |
27
+ | `[aria-current]` | Active item: `s2` fill + 2px role edge on top. Attribute, not class. |
28
+ | `.juno-dock--fixed` | Pin to the viewport foot (`position: fixed`) — for page-scroll shells where sticky won't pin. |
29
+ | `.juno-dock--pill` | Floating rounded pill: big glyphs in circular bubbles, labels hidden, active = bubble fill. |
30
+ | `.juno-dock--float` | Pillbar-style floating capsule chrome (fixed, rounded, blurred, shadowed) — keeps icon+label items and the full-item active fill. Combine with `--icon` for the `--pill` look, decomposed. |
31
+ | `.juno-dock--icon` | Labels hidden, glyphs grow, active = bubble fill — standalone (no floating chrome required). |
32
+ | `.juno-dock__bubble` | Circular icon holder (`--pill`/`--icon`); pair with `.juno-icon-loader` to host the loading ring. |
33
+ | `.juno-dock--collapsible` | Fold-to-a-circle variant driven by `--juno-dock-fold` — see [Collapsible](#collapsible-fold-driven). |
34
+ | `.juno-dock__tray` | The collapsible bar's item row — fades out as the fold closes. |
35
+ | `.juno-dock__knob` | The collapsed circle's face (a button); shown only under `data-juno-collapsed`. |
36
+ | `.juno--<role>` | Active-edge color (default `active`). |
37
+ | `--juno-dock-scale` | Consumer-set scale factor (default `1`) applied to the whole bar — e.g. shrink on scroll. |
38
+
39
+ ## Pill variant
40
+
41
+ `.juno-dock--pill` turns the full-width bar into a floating rounded pill: big
42
+ glyphs sit in circular `.juno-dock__bubble`s, the labels drop, and only the
43
+ active tab's bubble fills. It floats out of flow (fixed), so the page scrolls
44
+ _under_ it — reserve room on the scroller with
45
+ `padding-block-end: var(--juno-dock-clearance)` (a published token that folds
46
+ in the pill height + safe area, so it stays correct if the geometry changes).
47
+
48
+ ```html
49
+ <nav class="juno-dock juno-dock--pill juno-hide-from-md" aria-label="Primary">
50
+ <a class="juno-dock__item" href="/library" aria-current="page" aria-label="Library">
51
+ <span class="juno-dock__bubble juno-icon-loader">
52
+ <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-squares-four" /></svg>
53
+ </span>
54
+ </a>
55
+ <button class="juno-dock__item" aria-label="Nodes">
56
+ <span class="juno-dock__bubble juno-icon-loader">
57
+ <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-hexagon" /></svg>
58
+ </span>
59
+ </button>
60
+ </nav>
61
+ ```
62
+
63
+ - **Labels are hidden — every item MUST carry an `aria-label`** (on the
64
+ `<a>`/`<button>`). The glyph alone is not an accessible name.
65
+ - Focus lands on the bubble, so the ring hugs the round target.
66
+ - Pair every bubble with [`.juno-icon-loader`](./icon-loader.md) — it's the
67
+ concentric-stack contract, and having it on from the start means a loading
68
+ arc can appear later without touching the class list.
69
+
70
+ ### Section-loading ring
71
+
72
+ The bubble doesn't invent its own ring: it is an
73
+ [icon-loader](./icon-loader.md) host that overrides the ring's two dimensions
74
+ (`--juno-icon-loader-ring` = the bubble diameter, `--juno-icon-loader-ring-width`
75
+ = 2px), so the arc rings the bubble's **edge** rather than the glyph. Drop an
76
+ indeterminate [arc](./loader.md) inside while the section loads; it eats no
77
+ clicks, so the item still activates, and because the bubble's box is definite
78
+ the arc appearing never resizes it. The app owns the state — add/remove
79
+ `.juno-arc--indeterminate`; give the arc `role="status"` + `aria-label`.
80
+
81
+ ```html
82
+ <span class="juno-dock__bubble juno-icon-loader">
83
+ <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-gear" /></svg>
84
+ <span class="juno-arc juno-arc--indeterminate" role="status" aria-label="Loading"></span>
85
+ </span>
86
+ ```
87
+
88
+ ## Float variant
89
+
90
+ `.juno-dock--float` lifts the pillbar's floating capsule chrome (fixed
91
+ position, rounded shell, blur, shadow, off-edge margin) onto the dock's own
92
+ item model — icon+label items stay, and the active item still fills (`s2` +
93
+ role edge), just clipped to the capsule's rounded corners. It's `--pill`'s
94
+ exterior without `--pill`'s interior; combine with `--icon` below to
95
+ reproduce the original `--pill` look from two composable pieces. Out of flow
96
+ (fixed), so reserve room on the scroller with
97
+ `padding-block-end: var(--juno-dock-clearance)`.
98
+
99
+ ```html
100
+ <nav class="juno-dock juno-dock--float juno-hide-from-md" aria-label="Primary">
101
+ <a class="juno-dock__item" href="/library" aria-current="page">
102
+ <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-squares-four" /></svg>
103
+ <span class="juno-dock__label">Library</span>
104
+ </a>
105
+ <a class="juno-dock__item" href="/nodes">
106
+ <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-hexagon" /></svg>
107
+ <span class="juno-dock__label">Nodes</span>
108
+ </a>
109
+ </nav>
110
+ ```
111
+
112
+ ## Icon variant
113
+
114
+ `.juno-dock--icon` hides labels, grows the glyph, and moves the active state
115
+ onto a circular `.juno-dock__bubble` — usable standalone on a plain
116
+ sticky/`--fixed` bar for a compact, icon-only dock that stays in flow. Follows
117
+ the same bubble contract as `--pill` (pair with
118
+ [`.juno-icon-loader`](./icon-loader.md); see the pill section above for the
119
+ section-loading ring).
120
+
121
+ ```html
122
+ <nav class="juno-dock juno-dock--icon" aria-label="Primary">
123
+ <a class="juno-dock__item" href="/library" aria-current="page" aria-label="Library">
124
+ <span class="juno-dock__bubble juno-icon-loader">
125
+ <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-squares-four" /></svg>
126
+ </span>
127
+ </a>
128
+ <button class="juno-dock__item" aria-label="Nodes">
129
+ <span class="juno-dock__bubble juno-icon-loader">
130
+ <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-hexagon" /></svg>
131
+ </span>
132
+ </button>
133
+ </nav>
134
+ ```
135
+
136
+ - **Labels are hidden — every item MUST carry an `aria-label`.**
137
+ - Combine with `.juno-dock--float` for a floating icon-only capsule (the
138
+ original `--pill` treatment, built from the two composable variants).
139
+
140
+ ## Shrink on scroll
141
+
142
+ `--juno-dock-scale` (default `1`) drives a `transform: scale()` on the whole
143
+ bar, anchored `bottom center`, so a consumer can compact the dock as the page
144
+ scrolls without junoui shipping any scroll-listener JS:
145
+
146
+ ```js
147
+ dockEl.style.setProperty('--juno-dock-scale', shrink ? '0.92' : '1');
148
+ ```
149
+
150
+ The transition duration is authored through the motion scale, so
151
+ `prefers-reduced-motion` collapses it to an instant snap with no
152
+ component-specific media query.
153
+
154
+ ## Collapsible (fold-driven)
155
+
156
+ `.juno-dock--collapsible` folds the whole bar into a single circular
157
+ `.juno-dock__knob` at the inline-end edge, driven by one inherited custom prop
158
+ the app writes — `--juno-dock-fold` (`0` open … `1` the circle). Write it per
159
+ scroll frame for a gesture-tracked fold, or flip it `0`/`1` for a toggle.
160
+ Compose with a fixed placement (`--pill`, `--float`, or `--fixed`).
161
+
162
+ ```html
163
+ <nav class="juno-dock juno-dock--pill juno-dock--collapsible" aria-label="Primary">
164
+ <div class="juno-dock__tray">…the usual __item children…</div>
165
+ <button class="juno-dock__knob" aria-label="Show navigation">
166
+ <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-dots-three" /></svg>
167
+ </button>
168
+ </nav>
169
+ ```
170
+
171
+ Two phases, split at `--juno-dock-fold-split` (default `0.35`): first the bar
172
+ **shrinks** in place to `--juno-dock-fold-scale` (default `0.78`, transform
173
+ only — no relayout), then it **slides** shut to `--juno-dock-collapsed-size`.
174
+ The width interpolates between two definite lengths (a transition cannot run
175
+ to an intrinsic size); `--juno-dock-fold-smoothing` (default `90ms`) smooths
176
+ over scroll-event discretization — anything longer visibly lags the finger.
177
+
178
+ - `data-juno-collapsed` is the **end state**, set by the app only at fold = 1:
179
+ it hides the tray (`visibility` — the items leave the tab order) and reveals
180
+ the knob. Remove it the moment the fold reopens.
181
+ - The knob **must** carry an `aria-label`. Collapsing while focus is inside
182
+ the tray is the app's edge — move focus to the knob first.
183
+ - The fold's scale **replaces** the base `--juno-dock-scale` hook (one
184
+ transform slot). Transform-origin is the physical bottom right; RTL
185
+ consumers flip the origin and inset overrides together.
186
+ - Knobs: `--juno-dock-collapsed-size` (default: one tap-comfortable bubble
187
+ plus the pill's padding and border), `--juno-dock-edge-gap` (what the
188
+ floating margins take from `100%` — default `2 × space.12`).
30
189
 
31
190
  ## Anatomy (any platform)
32
191
 
@@ -15,11 +15,11 @@ surface; `.juno-drawer` re-pins and re-slides it.
15
15
  <dialog class="juno-modal juno-drawer juno-drawer--bottom">…</dialog>
16
16
  ```
17
17
 
18
- | Class | Effect |
19
- | ---------------------- | ------------------------------------------------------ |
20
- | `.juno-drawer` | 380px, full height, pinned to the **inline-end** edge. |
21
- | `.juno-drawer--start` | Pins to the inline-start edge. |
22
- | `.juno-drawer--bottom` | Full width, 60dvh, pinned to the bottom edge. |
18
+ | Class | Effect |
19
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
20
+ | `.juno-drawer` | 380px, full height, pinned to the **inline-end** edge. |
21
+ | `.juno-drawer--start` | Pins to the inline-start edge. |
22
+ | `.juno-drawer--bottom` | Full width, real bottom sheet: height via `--juno-sheet-h` (default `60dvh`), grab handle, rounded top corners, pinned to the bottom edge. |
23
23
 
24
24
  ## Anatomy (any platform)
25
25
 
@@ -27,6 +27,45 @@ surface; `.juno-drawer` re-pins and re-slides it.
27
27
  (`opacity.scrim`) and shadow (`shadow.3`) as the modal.
28
28
  - Edge anchoring is logical (`margin-inline`), so start/end mirror under RTL.
29
29
 
30
+ ## Bottom sheet (`.juno-drawer--bottom`)
31
+
32
+ ```html
33
+ <dialog class="juno-modal juno-drawer juno-drawer--bottom" aria-labelledby="sheet-tag">
34
+ <div class="juno-sheet__handle" aria-hidden="true"></div>
35
+ <div class="juno-modal__stripe"></div>
36
+ <div class="juno-modal__head">
37
+ <span class="juno-modal__tag" id="sheet-tag">FILTERS</span>
38
+ <button class="juno-modal__close" autofocus aria-label="Close">✕</button>
39
+ </div>
40
+ <div class="juno-modal__body"><!-- scrolls on its own, pads past the home indicator --></div>
41
+ </dialog>
42
+ ```
43
+
44
+ - **Height knob:** `--juno-sheet-h` (default `60dvh`) sets the sheet's block-size;
45
+ `--juno-sheet-max` (default `92dvh`) caps it. Both are plain custom properties an
46
+ app can swap per open (or per breakpoint) for peek/half/full snap points — this is
47
+ CSS only. junoui does **not** ship the drag gesture that would let a user pull
48
+ between those points; that's stateful interaction and belongs to the app (or a
49
+ sibling `junoui-<framework>` package).
50
+ - **Grab handle:** `.juno-sheet__handle` is a decorative bar the app renders as the
51
+ first child. Mark it `aria-hidden="true"` — the dialog's own `aria-labelledby`
52
+ (pointing at the title) remains the accessible name. It is not a button and has no
53
+ built-in drag behavior.
54
+ - **Safe area lives on the body, not the sheet.** `.juno-drawer--bottom` itself has
55
+ `padding-block-end: 0`; `.juno-modal__body` carries
56
+ `calc(var(--juno-pad-surface-inline) + env(safe-area-inset-bottom, 0px))` so a
57
+ scrolling sheet never hides its last row under the home indicator (a sheet that
58
+ doesn't scroll still gets the clearance, since the padding sits on the body box
59
+ either way).
60
+ - **`<dialog>` is the only supported sheet root.** `showModal()` gives you the
61
+ top-layer stacking, `::backdrop` scrim, ESC handling, scroll lock, and — critically —
62
+ focus trap **and** `inert` background for free: the rest of the page becomes
63
+ unfocusable and unclickable the moment the sheet opens, with no extra markup.
64
+ Hand-rolling a bottom sheet from a positioned `<div>` + a manual scrim `<button>`
65
+ reproduces the visual but silently drops all of that: no focus trap, no `inert`,
66
+ ESC and outside-dismiss have to be wired by hand and are easy to get wrong. Don't
67
+ do it — use `<dialog class="juno-modal juno-drawer juno-drawer--bottom">`.
68
+
30
69
  ## The slide-over pattern
31
70
 
32
71
  The full production composition — scrim, end-anchored panel on `s1`, header row,
@@ -56,5 +95,6 @@ parts; the drawer class only re-pins and re-slides the surface.
56
95
  - Settings panels, filters, detail editors that don't warrant leaving context.
57
96
  - Same `<dialog>` + `showModal()` contract as the modal.
58
97
  - **Phone widths (< `bp.sm`) are automatic:** side drawers cap at `85vw` so a
59
- sliver of scrim stays visible (and tappable) for dismissal. The bottom
60
- drawer always pads past the home indicator (`safe-area-inset-bottom`).
98
+ sliver of scrim stays visible (and tappable) for dismissal. The bottom drawer's
99
+ body always pads past the home indicator (`safe-area-inset-bottom`) — see
100
+ [Bottom sheet](#bottom-sheet-juno-drawer--bottom) above.
@@ -0,0 +1,31 @@
1
+ # Fold slot
2
+
3
+ Animated presence for a member of any flex/grid row: instead of popping in and
4
+ out of the DOM, the slot's width folds to zero, it fades, and at the end of the
5
+ fold it leaves the tab order (`visibility`, discretely transitioned — instant
6
+ coming in, end-of-fade going out). Keep the element mounted; flip
7
+ `data-juno-in`. Zero JS.
8
+
9
+ ## Web
10
+
11
+ ```html
12
+ <div class="juno-pillbar">
13
+ <button class="juno-fold" data-juno-in aria-label="Scroll to top">
14
+ <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-arrow-up" /></svg>
15
+ </button>
16
+ …other members…
17
+ </div>
18
+ ```
19
+
20
+ | Class / prop | Effect |
21
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
22
+ | `.juno-fold` | The slot: definite width (`--juno-fold-size`), folds shut when `data-juno-in` is absent. |
23
+ | `[data-juno-in]` | Present state — app-set. Absent = folded away, invisible, out of the tab order. |
24
+ | `--juno-fold-size` | The slot's open width (default `size.tap.comfortable`) — must be definite, transitions cannot run to an intrinsic size. |
25
+ | `--juno-fold-gap` | The row's flex/grid gap (default `0px`) — the folded slot swallows one gap with a negative margin so the row closes completely. |
26
+
27
+ - The row's own width follows the slot, so the whole row slides open/closed.
28
+ - `prefers-reduced-motion`: handled by the base layer (states still apply,
29
+ transition durations collapse).
30
+ - Canonical use: transient toolbar actions — a scroll-to-top arrow, a
31
+ contextual button — whose arrival should slide the row rather than jump it.
@@ -1,11 +1,16 @@
1
1
  # Icon loader
2
2
 
3
- A nav destination's icon ringed by the spinning [arc](./loader.md#arc) while
4
- that section loads — the "this section is loading" affordance for a
5
- [rail](./rail.md), [dock](./dock.md), or [pillbar](./pillbar.md) item. The icon
6
- stays static on top; the arc sizes in `em` around it and takes no pointer
7
- events, so the item still clicks through. Zero JS in the CSS — the app toggles
8
- the arc's `--indeterminate` class to start/stop the spin.
3
+ A control ringed by the spinning [arc](./loader.md#arc) while its section
4
+ loads — the "this section is loading" affordance for a [rail](./rail.md),
5
+ [dock](./dock.md), or [pillbar](./pillbar.md) item. What it wraps stays static
6
+ on top; the arc rings it and takes no pointer events, so the item still clicks
7
+ through. Zero JS in the CSS — the app toggles the arc's `--indeterminate` class
8
+ to start/stop the spin.
9
+
10
+ This is junoui's **only** concentric-ring mechanism. Anything that wants an arc
11
+ around it composes `.juno-icon-loader` rather than re-rolling the geometry — the
12
+ [dock](./dock.md#section-loading-ring)'s `__bubble` is the same wrapper with the
13
+ ring resized to the bubble.
9
14
 
10
15
  ## Web
11
16
 
@@ -23,28 +28,90 @@ When the section finishes, remove `.juno-arc--indeterminate` (or the whole
23
28
  `.juno-arc`). For a progress ring instead of an endless spin, drop
24
29
  `--indeterminate` and set `--juno-progress` (0–100) on the `.juno-arc`.
25
30
 
26
- | Class / prop | Effect |
27
- | ------------------------- | -------------------------------------------------------------- |
28
- | `.juno-icon-loader` | Positioned wrapper; sizes to the icon, centers the ring on it. |
29
- | `> .juno-icon` | The static glyph, held above the ring. |
30
- | `> .juno-arc` | The ring — `1.9em`, thin (`0.14em`), pointer-transparent. |
31
- | `--juno-icon-loader-ring` | Override the ring diameter (default `1.9em`). |
32
- | `.juno--<role>` | Ring color (default `active`). |
31
+ | Class / prop | Effect |
32
+ | ------------------------------- | ------------------------------------------------------------------- |
33
+ | `.juno-icon-loader` | Single-cell grid wrapper every child is centred on the same cell. |
34
+ | `> *` | The ringed content, held above the ring. |
35
+ | `> .juno-arc` | The ring — pointer-transparent, painted under the content. |
36
+ | `--juno-icon-loader-ring` | Ring diameter (default `1.9em`, i.e. relative to the glyph). |
37
+ | `--juno-icon-loader-ring-width` | Ring stroke (default `0.14em`). |
38
+ | `.juno--<role>` | Ring color (default `active`). |
39
+
40
+ ### Ringing something bigger than a glyph
41
+
42
+ Both dimensions are custom props, so the same wrapper rings a 14px glyph and a
43
+ 44px bubble. Give the host a **definite** box and set the diameter to it — then
44
+ the ring hugs the edge and the box does not resize when the arc appears:
45
+
46
+ ```css
47
+ .my-avatar-loader {
48
+ --juno-icon-loader-ring: var(--juno-size-tap-comfortable);
49
+ --juno-icon-loader-ring-width: 2px;
50
+
51
+ inline-size: var(--juno-size-tap-comfortable);
52
+ block-size: var(--juno-size-tap-comfortable);
53
+ }
54
+ ```
55
+
56
+ That is exactly how `.juno-dock__bubble` gets its section-loading ring.
57
+
58
+ ### Ringing an arbitrary control (button, badge, avatar)
59
+
60
+ The wrapper doesn't care what it wraps — swap the icon for any interactive
61
+ control and the ring still hugs it without resizing its box. Set the ring
62
+ diameter to the control's own size (its `--juno-size-*` or `em` value) and
63
+ the control keeps its own footprint; the wrapper is the only thing that
64
+ grows to fit the ring:
65
+
66
+ ```html
67
+ <!-- 40px circular icon button -->
68
+ <span
69
+ class="juno-icon-loader"
70
+ style="--juno-icon-loader-ring:40px;--juno-icon-loader-ring-width:2px"
71
+ >
72
+ <button class="juno-btn juno-btn--icon" aria-label="More">&#8943;</button>
73
+ <span class="juno-arc juno-arc--indeterminate" role="status" aria-label="Loading"></span>
74
+ </span>
75
+
76
+ <!-- 14px inline badge — small ring, use --smooth to avoid stepped jitter -->
77
+ <span
78
+ class="juno-icon-loader"
79
+ style="--juno-icon-loader-ring:14px;--juno-icon-loader-ring-width:2px"
80
+ >
81
+ <span class="juno-badge juno-badge--micro">3</span>
82
+ <span
83
+ class="juno-arc juno-arc--indeterminate juno-arc--smooth"
84
+ role="status"
85
+ aria-label="Loading"
86
+ ></span>
87
+ </span>
88
+ ```
89
+
90
+ The ring is `pointer-events: none` and sits under the content (`z-index: 0`
91
+ vs. `1`), so a wrapped `<button>` stays fully clickable.
33
92
 
34
93
  ## Anatomy (any platform)
35
94
 
36
- - A wrapper the size of the icon; the arc is absolutely positioned and centered
37
- over it, at a larger diameter so it rings the glyph.
95
+ - A wrapper that stacks the ring and what it rings on one centred cell; with no
96
+ explicit size the wrapper sizes to the larger of the two (normally the ring).
38
97
  - The ring color is the semantic role; a `.juno--loading`/`.juno--active`
39
98
  ancestor tints it.
40
99
 
41
100
  ## Usage
42
101
 
43
- - **Concentric via a single-cell grid, never `translate(-50%, -50%)`** — the
44
- icon and ring share one grid cell (`place-items: center`), so they're centered
45
- on each other without any `transform`. The arc's rotation animation writes
102
+ - **Concentric via a single-cell grid, never `translate(-50%, -50%)`** — every
103
+ child shares one grid cell (`place-items: center`), so they're centered on
104
+ each other without any `transform`. The arc's rotation animation writes
46
105
  `transform`, so a translate-based centering would be clobbered; keep the grid
47
106
  approach if you re-roll the markup.
107
+ - **Don't hand-roll a second ring.** If a component needs an arc around it,
108
+ compose this class and override the two ring props — one primitive keeps the
109
+ centring gotcha, the pointer-events rule, and the paint order in one place.
110
+ That includes bespoke overlay CSS elsewhere for a "this control is loading"
111
+ ring — delete it and wrap the control in `.juno-icon-loader` instead.
112
+ - Below ~24px, add `.juno-arc--smooth` next to `--indeterminate` — the
113
+ default 12-step sweep (see [loader](./loader.md)) reads as jitter at that
114
+ size; `--smooth` makes it a continuous rotation.
48
115
  - Gate the spin to first load, not every background refetch — a nav icon that
49
116
  blinks on every poll reads as broken. Add `--indeterminate` when a section
50
117
  has no data yet; remove it once loaded.
@@ -24,6 +24,27 @@ and a banner in the generated sprite; keep both when you ship.
24
24
 
25
25
  Import path: `@junoput01/junoui/icons` resolves to the sprite.
26
26
 
27
+ ## Same-document refs (Safari fix)
28
+
29
+ An **external** sprite reference (`<use href="…file.svg#id">`) intermittently
30
+ fails to render in Safari — icons randomly vanish and reappear. The reliable
31
+ path is a **same-document** reference (`<use href="#juno-i-gear">`), which needs
32
+ the sprite living in the current document. Rather than hand-roll that injection
33
+ in every app, import the shipped helper once:
34
+
35
+ ```js
36
+ import '@junoput01/junoui/icons/inline'; // injects the sprite into the document once
37
+ ```
38
+
39
+ ```html
40
+ <svg class="juno-icon" aria-hidden="true"><use href="#juno-i-gear" /></svg>
41
+ ```
42
+
43
+ The module auto-installs on import in a browser (no-op server-side / before
44
+ hydration) and is id-guarded, so importing it from many modules injects only
45
+ once. It also exports `installJunoIcons(doc)` (default + named) for manual or
46
+ multi-document (iframe) control. No bundler `?raw` loader needed.
47
+
27
48
  | Class | Effect |
28
49
  | ------------------ | ------------------------------------------------------------- |
29
50
  | `.juno-icon` | 1.25em square, `fill: currentColor`, baseline-aligned inline. |