@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.
- package/CHANGELOG.md +219 -0
- package/README.md +23 -19
- package/dist/css/juno-custom-media.css +32 -0
- package/dist/css/juno.css +1216 -60
- package/dist/icons/inline.js +22 -0
- package/docs/accessibility.md +55 -34
- package/docs/boot-shell.md +295 -0
- package/docs/components/README.md +46 -43
- package/docs/components/dock.md +167 -8
- package/docs/components/drawer.md +47 -7
- package/docs/components/fold-slot.md +31 -0
- package/docs/components/icon-loader.md +85 -18
- package/docs/components/icon.md +21 -0
- package/docs/components/load-state.md +131 -0
- package/docs/components/loader.md +13 -5
- package/docs/components/pillbar.md +157 -10
- package/docs/components/reload.md +41 -0
- package/docs/components/skeleton.md +22 -15
- package/docs/components/thumb.md +34 -14
- package/docs/design-guidelines.md +26 -0
- package/docs/getting-started.md +23 -0
- package/docs/ios-conformance.md +224 -0
- package/docs/layout.md +71 -1
- package/docs/web.md +6 -0
- package/package.json +5 -2
- package/src/css/base.css +169 -6
- package/src/css/components/dock.css +322 -0
- package/src/css/components/drawer.css +40 -3
- package/src/css/components/fold-slot.css +44 -0
- package/src/css/components/icon-loader.css +32 -18
- package/src/css/components/icon.css +6 -4
- package/src/css/components/load-state.css +136 -0
- package/src/css/components/loader.css +6 -0
- package/src/css/components/menu.css +4 -0
- package/src/css/components/modal.css +24 -3
- package/src/css/components/navbar.css +5 -1
- package/src/css/components/pillbar.css +207 -7
- package/src/css/components/reload.css +48 -0
- package/src/css/components/skeleton.css +41 -13
- package/src/css/components/tabs.css +5 -0
- package/src/css/components/thumb.css +63 -1
- package/src/css/components/toast.css +5 -1
- package/src/css/density.css +22 -0
- package/src/css/layout.css +30 -2
- package/src/css/utilities.css +4 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Load state
|
|
2
|
+
|
|
3
|
+
A vocabulary for every way a load can go, so nothing spins forever. junoui already had
|
|
4
|
+
"resolving" (arc/beacon/bar, both indeterminate and determinate) and "layout known, not
|
|
5
|
+
loaded yet" ([skeleton](./skeleton.md)). This adds the two treatments those don't cover —
|
|
6
|
+
**working with no ETA** and **failed** — plus a table-agnostic **empty** state and an
|
|
7
|
+
optional CSS-only switch to show exactly one at a time. Zero JS: junoui ships the look and
|
|
8
|
+
the ARIA contract per state; deciding _when_ a load becomes a fault or an empty result is
|
|
9
|
+
the app's job.
|
|
10
|
+
|
|
11
|
+
## The decision table
|
|
12
|
+
|
|
13
|
+
| State | Treatment | Why |
|
|
14
|
+
| ------------------------------- | ------------------------- | ------------------------------------------------------------ |
|
|
15
|
+
| No data yet, about to fetch | `.juno-beacon` | "no bytes yet" — a pulse, not a promise of progress. |
|
|
16
|
+
| Fetching, will resolve | `.juno-arc` / `.juno-bar` | Real progress or a bounded wait — see [loader](./loader.md). |
|
|
17
|
+
| Layout known, content pending | `.juno-skeleton` | Placeholder mirrors the shape of what's coming. |
|
|
18
|
+
| Server working, **no ETA** | `.juno-shimmer` | Motion _without_ a completion promise. |
|
|
19
|
+
| Failed — **terminal** | `.juno-fault` | Static. A spinner on a 404 spins forever. |
|
|
20
|
+
| Legitimately nothing — terminal | `.juno-empty` | Static. Not a failure — don't tint it like one. |
|
|
21
|
+
|
|
22
|
+
`.juno-shimmer` and `.juno-fault` are the two treatments this ticket adds; `.juno-empty` is
|
|
23
|
+
the generalized, table-agnostic form of the existing `.juno-table__empty` (same anatomy,
|
|
24
|
+
usable outside a table).
|
|
25
|
+
|
|
26
|
+
## Web
|
|
27
|
+
|
|
28
|
+
### Shimmer — work in progress, no completion promise
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<div class="juno-shimmer" style="block-size: 120px;" aria-busy="true" aria-live="polite"></div>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Shares `.juno-skeleton`'s compositor-only band (same `@keyframes juno-skeleton-shimmer`,
|
|
35
|
+
defined once in `skeleton.css`) — one shimmer implementation, applied to a solid fill
|
|
36
|
+
instead of a placeholder shape. Override cadence with `--juno-shimmer-dur` (default
|
|
37
|
+
`1.4s`).
|
|
38
|
+
|
|
39
|
+
### Fault — terminal, never animates
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<div class="juno-fault" role="status">
|
|
43
|
+
<span class="juno-fault__icon" aria-hidden="true">!</span>
|
|
44
|
+
<p>Couldn't load this image.</p>
|
|
45
|
+
</div>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Role defaults to `caution`; add a `.juno--<role>` class to recolor (`.juno--warning` for a
|
|
49
|
+
harder failure) — never hardcode a color. `role="status"` on the fault itself is a
|
|
50
|
+
per-region, polite announcement (this fired once, doesn't need to interrupt); for a
|
|
51
|
+
page-level urgent message use [`.juno-alert`](./alert.md) with `role="alert"` instead.
|
|
52
|
+
|
|
53
|
+
### Empty — legitimately nothing, terminal
|
|
54
|
+
|
|
55
|
+
```html
|
|
56
|
+
<div class="juno-empty">
|
|
57
|
+
<span class="juno-empty__icon" aria-hidden="true">∅</span>
|
|
58
|
+
<p>Nothing here yet.</p>
|
|
59
|
+
</div>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### The switch (optional)
|
|
63
|
+
|
|
64
|
+
CSS-only, one visible child at a time. The app sets `data-juno-state` on the parent as
|
|
65
|
+
its fetch/rendition state changes; junoui does no state inference.
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<div class="juno-state" data-juno-state="processing">
|
|
69
|
+
<div data-juno-when="loading">
|
|
70
|
+
<div class="juno-arc juno-arc--indeterminate" role="status" aria-label="Loading"></div>
|
|
71
|
+
</div>
|
|
72
|
+
<div data-juno-when="processing" aria-busy="true" aria-live="polite">
|
|
73
|
+
<div class="juno-shimmer" style="block-size: 120px;"></div>
|
|
74
|
+
</div>
|
|
75
|
+
<div data-juno-when="error">
|
|
76
|
+
<div class="juno-fault" role="status">
|
|
77
|
+
<span class="juno-fault__icon" aria-hidden="true">!</span>
|
|
78
|
+
<p>Couldn't load this.</p>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div data-juno-when="empty">
|
|
82
|
+
<div class="juno-empty">
|
|
83
|
+
<span class="juno-empty__icon" aria-hidden="true">∅</span>
|
|
84
|
+
<p>Nothing here yet.</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| Class / attribute | Effect |
|
|
91
|
+
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
92
|
+
| `.juno-state` | Switch container. |
|
|
93
|
+
| `[data-juno-when]` | A wrapper hidden by default; shown when its (space-separated) token list contains the parent's current state. |
|
|
94
|
+
| `.juno-state[data-juno-state='…']` | Set by the app: `loading` \| `processing` \| `error` \| `empty`. |
|
|
95
|
+
| `.juno-shimmer` | Compositor-only shimmer band on a solid fill (shares skeleton's `@keyframes`). |
|
|
96
|
+
| `--juno-shimmer-dur` | Shimmer cycle length (default `1.4s`). |
|
|
97
|
+
| `.juno-fault` / `.juno-fault__icon` | Terminal failure card. Role `caution` by default. |
|
|
98
|
+
| `.juno-empty` / `.juno-empty__icon` | Terminal empty-result card. Neutral (`label`/`muted`), same anatomy as `.juno-table__empty`. |
|
|
99
|
+
|
|
100
|
+
## Anatomy (any platform)
|
|
101
|
+
|
|
102
|
+
- Shimmer: solid `s2` fill, `s3` highlight band at 70% opacity sliding on `transform`
|
|
103
|
+
only, 1.4s ease-in-out loop — identical mechanism to skeleton, different semantic use.
|
|
104
|
+
- Fault: column, centered, `space.8` gap, `space.24` padding, role-colored text and icon
|
|
105
|
+
outline (`radius.8` box, `border.width.1`). Never animates.
|
|
106
|
+
- Empty: column, centered, `space.12` gap, `space.56`/`space.24` padding, `label`/`muted`
|
|
107
|
+
text and icon outline (`radius.8` box). Never animates.
|
|
108
|
+
|
|
109
|
+
## Usage
|
|
110
|
+
|
|
111
|
+
- **Shimmer vs. skeleton:** skeleton implies a known layout waiting for content (first
|
|
112
|
+
paint); shimmer implies ongoing server-side work with no bound on when it finishes
|
|
113
|
+
(transcoding, indexing, re-encoding). Don't use skeleton for the latter — the moment a
|
|
114
|
+
skeleton is on screen, the layout it promises should be about to land.
|
|
115
|
+
Reference: `web/src/media/loadview.ts` (nexora) — spinner only while nothing has
|
|
116
|
+
started, shimmer for `processing`, static fault for `error`; "a spinner on a 404 spins
|
|
117
|
+
forever."
|
|
118
|
+
- **Fault vs. alert:** a fault is a _region_ replacing content that failed to load
|
|
119
|
+
(an image tile, a card). An [alert](./alert.md) is a page-level message. Don't reach for
|
|
120
|
+
`.juno-alert` inside a grid cell.
|
|
121
|
+
- **Empty is not a failure:** legitimately-zero-results (an empty search, a fresh account)
|
|
122
|
+
is a distinct terminal state from an error — keep it neutral, offer a next action
|
|
123
|
+
(a button/link) rather than an apology.
|
|
124
|
+
- **The switch is optional.** Nothing stops rendering `.juno-shimmer` / `.juno-fault` /
|
|
125
|
+
`.juno-empty` directly and swapping them with app-side conditional rendering; `.juno-state`
|
|
126
|
+
exists for markup that's easier to keep all-present (e.g. server-rendered) and toggle
|
|
127
|
+
by attribute.
|
|
128
|
+
- **a11y, per state:** `loading`/`processing` → `aria-busy="true"` (+ `aria-live="polite"`
|
|
129
|
+
if the region isn't otherwise announced); `.juno-fault` → `role="status"` on itself;
|
|
130
|
+
`.juno-empty` → no extra role, it's static content. See
|
|
131
|
+
[accessibility.md](../accessibility.md) for the full per-component ARIA contract.
|
|
@@ -26,15 +26,23 @@ Color comes from `--juno-role` (default `active`); add a `.juno--<role>` class t
|
|
|
26
26
|
</div>
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
| Class / var | Effect
|
|
30
|
-
| --------------------------------------------------- |
|
|
31
|
-
| `.juno-arc` | Ring; determinate sweep = progress (conic gradient + ring mask).
|
|
32
|
-
| `.juno-arc--indeterminate` | 12-step mechanical rotation.
|
|
33
|
-
|
|
|
29
|
+
| Class / var | Effect |
|
|
30
|
+
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
31
|
+
| `.juno-arc` | Ring; determinate sweep = progress (conic gradient + ring mask). |
|
|
32
|
+
| `.juno-arc--indeterminate` | 12-step mechanical rotation. |
|
|
33
|
+
| `.juno-arc--smooth` | With `--indeterminate`: continuous (linear) rotation instead of the 12-step sweep — use under ~24px, where the steps read as jitter. |
|
|
34
|
+
| `--juno-arc-size` (76px) · `--juno-arc-width` (4px) | Diameter / stroke. |
|
|
34
35
|
|
|
35
36
|
> The ring uses a CSS mask, which clips children. Put `.juno-arc__label` as a
|
|
36
37
|
> **sibling** over a positioned wrapper, not inside `.juno-arc`.
|
|
37
38
|
|
|
39
|
+
> To ring an _arbitrary control_ (a button, a badge, an avatar) without
|
|
40
|
+
> changing its own box, don't reposition `.juno-arc` by hand — compose
|
|
41
|
+
> [`.juno-icon-loader`](./icon-loader.md), junoui's one ring-a-thing
|
|
42
|
+
> primitive. It stacks the arc and the control on a single grid cell sized
|
|
43
|
+
> off two custom props, so nothing you wrap ever resizes when the ring
|
|
44
|
+
> appears.
|
|
45
|
+
|
|
38
46
|
## Beacon — radiating pulse
|
|
39
47
|
|
|
40
48
|
```html
|
|
@@ -23,16 +23,161 @@ optional divider. Zero JS.
|
|
|
23
23
|
</nav>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
| Class / prop
|
|
27
|
-
|
|
|
28
|
-
| `.juno-pillbar`
|
|
29
|
-
| `.juno-pillbar__item`
|
|
30
|
-
| `.juno-pillbar__label`
|
|
31
|
-
| `.juno-pillbar__sep`
|
|
32
|
-
| `[aria-current]`
|
|
33
|
-
| `[aria-pressed]`
|
|
34
|
-
| `.juno-pillbar--fixed`
|
|
35
|
-
| `.juno
|
|
26
|
+
| Class / prop | Effect |
|
|
27
|
+
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
28
|
+
| `.juno-pillbar` | Sticky floating pill: blurred translucent `s1`, hairline, `shadow.2`. |
|
|
29
|
+
| `.juno-pillbar__item` | Round tap target ≥ `size.tap.comfortable`; icon with optional label. |
|
|
30
|
+
| `.juno-pillbar__label` | Inline text next to the icon — truncates at 12ch. |
|
|
31
|
+
| `.juno-pillbar__sep` | Vertical hairline between item groups (e.g. destinations vs. actions). |
|
|
32
|
+
| `[aria-current]` | Active destination: `s3` pill fill + role color. Attribute, not class. |
|
|
33
|
+
| `[aria-pressed]` | Same active look for toggle buttons. |
|
|
34
|
+
| `.juno-pillbar--fixed` | Fix to the viewport (`position: fixed`), floating its usual `space.16` + safe-area above the foot — for page-scroll shells where sticky won't pin. |
|
|
35
|
+
| `.juno-pillbar--top-right` / `--top-left` / `--bottom-right` / `--bottom-left` | Fix as a floating cluster in one viewport corner (safe-area-clamped), instead of the centered bottom bar. |
|
|
36
|
+
| `.juno-pillbar__input` | Borderless search field inside the pill (`min(52vw, 240px)`, 16px font floor). |
|
|
37
|
+
| `.juno-pillbar__overflow` | "More" trigger for items that don't fit — anchors a [menu](./menu.md) via `popovertarget`. |
|
|
38
|
+
| `.juno-pillbar--collapsible` | The pill folds into a single circle (`__toggle`) and animates back to full width — see [Collapsible](#collapsible). |
|
|
39
|
+
| `.juno-pillbar__toggle` | The collapsible pill's circular expand/collapse control; state on `aria-expanded`, same chrome as `__overflow`. |
|
|
40
|
+
| `.juno-pillbar__tray` | Wrapper whose width animates shut — holds one child wrapping the usual `__item` markup. |
|
|
41
|
+
| `.juno--<role>` | Active color (default `active`). |
|
|
42
|
+
|
|
43
|
+
### Geometry custom props
|
|
44
|
+
|
|
45
|
+
Read from `.juno-pillbar` (or its computed style) instead of hardcoding these
|
|
46
|
+
in an app-side capacity planner — they're the single source of truth for how
|
|
47
|
+
many `__item` fit before the rest should route to `__overflow`.
|
|
48
|
+
|
|
49
|
+
| Prop | Default | Meaning |
|
|
50
|
+
| --------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `--juno-pillbar-item` | `size.tap.comfortable` | Item tap-target side (square). |
|
|
52
|
+
| `--juno-pillbar-gap` | `space.2` | Gap between items. |
|
|
53
|
+
| `--juno-pillbar-pad` | `space.4` | Pill's own inner padding. |
|
|
54
|
+
| `--juno-pillbar-edge` | `space.16` | Base (bottom-center) placement's offset from the viewport edge. Not used by the corner modifiers, which keep their own distinct block/inline offsets. |
|
|
55
|
+
|
|
56
|
+
## Corner placement
|
|
57
|
+
|
|
58
|
+
The base pillbar is a centered bottom bar. The corner modifiers pin it as a
|
|
59
|
+
floating cluster in one viewport corner instead — e.g. a top-right
|
|
60
|
+
search/filter cluster over a full-bleed grid. Each flips to `position: fixed`
|
|
61
|
+
and clamps the corner with `env(safe-area-inset-*)` so it never lands under a
|
|
62
|
+
notch or the home indicator; the base blur/border/shadow carries over. For the
|
|
63
|
+
bottom corners, reserve scroll clearance with
|
|
64
|
+
`padding-block-end: var(--juno-pillbar-clearance)` if the pill overlaps
|
|
65
|
+
content.
|
|
66
|
+
|
|
67
|
+
## Input slot
|
|
68
|
+
|
|
69
|
+
`.juno-pillbar__input` is an expandable search field that lives inside the
|
|
70
|
+
pill, reading as part of it rather than a boxed control.
|
|
71
|
+
|
|
72
|
+
```html
|
|
73
|
+
<div class="juno-pillbar juno-pillbar--top-right">
|
|
74
|
+
<input
|
|
75
|
+
class="juno-pillbar__input"
|
|
76
|
+
type="search"
|
|
77
|
+
aria-label="Search library"
|
|
78
|
+
placeholder="Search…"
|
|
79
|
+
/>
|
|
80
|
+
<button class="juno-pillbar__item" aria-label="Search" aria-pressed="true">
|
|
81
|
+
<svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-magnifying-glass" /></svg>
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- The field is borderless/transparent; its font-size is held at
|
|
87
|
+
`max(16px, …)` because iOS Safari zooms the whole page onto any focused text
|
|
88
|
+
field under 16px (the same floor the base [input](./input.md) applies on
|
|
89
|
+
touch).
|
|
90
|
+
- **A placeholder is not a label** — give the input an `aria-label` or a
|
|
91
|
+
visually-hidden `<label>`.
|
|
92
|
+
|
|
93
|
+
## Overflow slot
|
|
94
|
+
|
|
95
|
+
`.juno-pillbar__overflow` is a "more" trigger, styled like `__item`, for
|
|
96
|
+
pills with variable membership (e.g. a top-right action pill whose item count
|
|
97
|
+
depends on the current view). It opens a [`.juno-menu`](./menu.md) holding
|
|
98
|
+
whatever didn't fit — zero JS, via the native Popover API:
|
|
99
|
+
|
|
100
|
+
```html
|
|
101
|
+
<div class="juno-pillbar juno-pillbar--top-right">
|
|
102
|
+
<button class="juno-pillbar__item" aria-label="Search" aria-pressed="true">
|
|
103
|
+
<svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-magnifying-glass" /></svg>
|
|
104
|
+
</button>
|
|
105
|
+
<button class="juno-pillbar__item" aria-label="Filter">
|
|
106
|
+
<svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-funnel" /></svg>
|
|
107
|
+
</button>
|
|
108
|
+
<button
|
|
109
|
+
class="juno-pillbar__overflow"
|
|
110
|
+
popovertarget="pillbar-more"
|
|
111
|
+
aria-haspopup="menu"
|
|
112
|
+
aria-expanded="false"
|
|
113
|
+
aria-label="More"
|
|
114
|
+
>
|
|
115
|
+
<svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-dots-three" /></svg>
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
<ul class="juno-menu" id="pillbar-more" popover role="menu">
|
|
119
|
+
<li>
|
|
120
|
+
<button
|
|
121
|
+
class="juno-menu__item"
|
|
122
|
+
role="menuitem"
|
|
123
|
+
popovertarget="pillbar-more"
|
|
124
|
+
popovertargetaction="hide"
|
|
125
|
+
>
|
|
126
|
+
<span class="juno-menu__icon">⤓</span>Export
|
|
127
|
+
</button>
|
|
128
|
+
</li>
|
|
129
|
+
</ul>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- **junoui ships the docking point, not the collapse policy.** Deciding
|
|
133
|
+
_which_ items overflow and _when_ is the app's job — read the
|
|
134
|
+
[geometry custom props](#geometry-custom-props) to do the capacity math,
|
|
135
|
+
then conditionally render `.juno-pillbar__overflow` and move the spilled
|
|
136
|
+
`__item`s into the menu. This keeps one source of truth for the pixel
|
|
137
|
+
constants instead of an app re-declaring them in JS.
|
|
138
|
+
- The invoker (`popovertarget`) is the menu's implicit anchor, so
|
|
139
|
+
`.juno-menu`'s own `position-try-fallbacks` handles flipping it clear of the
|
|
140
|
+
viewport edge — no extra positioning needed in the pill.
|
|
141
|
+
- `aria-expanded` mirrors the menu's open state like `[aria-pressed]` does for
|
|
142
|
+
toggle items; toggle it from the app or a stateless enhancer (junoui itself
|
|
143
|
+
ships no JS).
|
|
144
|
+
|
|
145
|
+
## Collapsible
|
|
146
|
+
|
|
147
|
+
`.juno-pillbar--collapsible` lets the whole pill fold into a single circular
|
|
148
|
+
button and expand back to full width on demand — for toolbars that should get
|
|
149
|
+
out of the content's way (e.g. a media viewer's action bar). State lives on
|
|
150
|
+
the toggle's `aria-expanded`; junoui ships no JS, the app flips the attribute.
|
|
151
|
+
|
|
152
|
+
```html
|
|
153
|
+
<nav class="juno-pillbar juno-pillbar--collapsible" aria-label="Tools">
|
|
154
|
+
<button class="juno-pillbar__toggle" aria-expanded="false" aria-label="Show toolbar">
|
|
155
|
+
<svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-dots-three" /></svg>
|
|
156
|
+
</button>
|
|
157
|
+
<div class="juno-pillbar__tray">
|
|
158
|
+
<div>
|
|
159
|
+
<button class="juno-pillbar__item" aria-label="Share">…</button>
|
|
160
|
+
<span class="juno-pillbar__sep"></span>
|
|
161
|
+
<button class="juno-pillbar__item" aria-label="Delete">…</button>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</nav>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- **The tray needs exactly one child wrapper** (any element). The expansion is
|
|
168
|
+
a `grid-template-columns: 0fr ↔ 1fr` transition — the only widely-supported
|
|
169
|
+
way to animate to an intrinsic width (Safari 16+) — and the track needs a
|
|
170
|
+
single shrinkable child to collapse.
|
|
171
|
+
- DOM order is free: toggle-first reads naturally for a left-anchored pill,
|
|
172
|
+
toggle-last for a right-anchored corner (`--bottom-right`), so the circle
|
|
173
|
+
stays put and the tray grows out of it.
|
|
174
|
+
- Collapsed, the tray goes `visibility: hidden` at the end of the slide, which
|
|
175
|
+
also removes its items from the tab order. **Collapsing while focus is
|
|
176
|
+
inside the tray is the app's edge:** move focus to the toggle first.
|
|
177
|
+
- The toggle's glyph rotates a quarter-turn while expanded; swap the icon from
|
|
178
|
+
the app instead if you want e.g. dots → ×.
|
|
179
|
+
- `prefers-reduced-motion` is handled by the base layer (every transition
|
|
180
|
+
collapses to ~0ms) — the states still apply, just without the slide.
|
|
36
181
|
|
|
37
182
|
## Anatomy (any platform)
|
|
38
183
|
|
|
@@ -47,6 +192,8 @@ optional divider. Zero JS.
|
|
|
47
192
|
|
|
48
193
|
- Same rules as the dock: 3–5 destinations, overflow goes behind a "More" item.
|
|
49
194
|
Pick **one** bottom pattern per screen — dock _or_ pillbar, never both.
|
|
195
|
+
- Variable-membership action pills (not fixed destinations) use the
|
|
196
|
+
[overflow slot](#overflow-slot) instead of hand-rolling a "more" menu.
|
|
50
197
|
- Sticky, not fixed: place it last inside the scrolling column; it floats over
|
|
51
198
|
content while staying in flow. Best inside the
|
|
52
199
|
[`.juno-app-shell`](../layout.md#app-shell) frame, whose `__main` is the
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Reload
|
|
2
|
+
|
|
3
|
+
The non-blocking counterpart to the [skeleton](./skeleton.md). A skeleton
|
|
4
|
+
stands in for content that isn't there yet (first paint); the reload indicator
|
|
5
|
+
signals a refresh happening _over_ content that's already on screen — the stale
|
|
6
|
+
data stays readable and interactive while fresh data lands.
|
|
7
|
+
|
|
8
|
+
It's a small pulsing dot with a soft halo, centered as a fixed overlay that
|
|
9
|
+
eats no pointer events, so the page underneath stays fully usable.
|
|
10
|
+
|
|
11
|
+
## Web
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<div class="juno-reload" role="status" aria-label="Reloading">
|
|
15
|
+
<span class="juno-reload__dot"></span>
|
|
16
|
+
</div>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| Class / prop | Effect |
|
|
20
|
+
| ------------------- | ------------------------------------------------------------------- |
|
|
21
|
+
| `.juno-reload` | Fixed, centered, `pointer-events: none` overlay. |
|
|
22
|
+
| `.juno-reload__dot` | `space.16` role-colored dot with a soft halo, gentle opacity pulse. |
|
|
23
|
+
| `.juno--<role>` | Dot color (default `active`). |
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
- Render it while a **background refetch is in flight over existing content**;
|
|
28
|
+
drop it when the request settles. For first-paint (no content yet) use the
|
|
29
|
+
[skeleton](./skeleton.md) instead.
|
|
30
|
+
- The app owns the state (zero JS in the component) — mount/unmount it, or
|
|
31
|
+
toggle a `hidden` attribute.
|
|
32
|
+
- The pulse uses the shared `juno-pulse` keyframe — a soft dim, not a full
|
|
33
|
+
blink. `prefers-reduced-motion` stops the pulse (via the base layer); the
|
|
34
|
+
dot stays visible so the "refreshing" state is still conveyed.
|
|
35
|
+
|
|
36
|
+
## Accessibility
|
|
37
|
+
|
|
38
|
+
- `role="status"` + `aria-label` announces the refresh **politely**, without
|
|
39
|
+
stealing focus — it's a status, not an alert. Don't use `role="alert"`.
|
|
40
|
+
- Keep it non-blocking: `pointer-events: none` ensures it never traps clicks on
|
|
41
|
+
the content it floats over.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Skeleton
|
|
2
2
|
|
|
3
3
|
A shimmering placeholder that stands in for content not yet loaded — same shimmer as
|
|
4
|
-
the [table](./table.md) row skeleton, generalized to text lines, blocks, and
|
|
5
|
-
Zero JS; honors `prefers-reduced-motion` (shimmer → static)
|
|
4
|
+
the [table](./table.md) row skeleton, generalized to text lines, blocks, circles, and
|
|
5
|
+
aspect-ratio tiles. Zero JS; honors `prefers-reduced-motion` (shimmer → static) via the
|
|
6
|
+
global motion contract.
|
|
6
7
|
|
|
7
8
|
## Web
|
|
8
9
|
|
|
@@ -12,27 +13,33 @@ Zero JS; honors `prefers-reduced-motion` (shimmer → static).
|
|
|
12
13
|
<span class="juno-skeleton juno-skeleton--text" style="inline-size: 60%"></span>
|
|
13
14
|
<span class="juno-skeleton juno-skeleton--text"></span>
|
|
14
15
|
<span class="juno-skeleton juno-skeleton--block"></span>
|
|
16
|
+
<span class="juno-skeleton juno-skeleton--tile"></span>
|
|
15
17
|
</div>
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
| Class | Effect
|
|
19
|
-
| ------------------------ |
|
|
20
|
-
| `.juno-skeleton` | Shimmering bar; height `--juno-skeleton-h` (default `space.16`).
|
|
21
|
-
| `.juno-skeleton--text` | One text line (`1em` tall, soft radius).
|
|
22
|
-
| `.juno-skeleton--circle` | Round avatar / icon placeholder (square via the height).
|
|
23
|
-
| `.juno-skeleton--block` | Larger surface (card / media), `space.56` tall.
|
|
24
|
-
| `--juno-skeleton-
|
|
20
|
+
| Class | Effect |
|
|
21
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
22
|
+
| `.juno-skeleton` | Shimmering bar; height `--juno-skeleton-h` (default `space.16`). |
|
|
23
|
+
| `.juno-skeleton--text` | One text line (`1em` tall, soft radius). |
|
|
24
|
+
| `.juno-skeleton--circle` | Round avatar / icon placeholder (square via the height). |
|
|
25
|
+
| `.juno-skeleton--block` | Larger surface (card / media), `space.56` tall. |
|
|
26
|
+
| `.juno-skeleton--tile` | Content-box mode: sized by `--juno-skeleton-ratio` instead of a fixed height — the media-grid case (a tile matching a real image/card's aspect ratio). |
|
|
27
|
+
| `--juno-skeleton-h` | Override the height (any length). |
|
|
28
|
+
| `--juno-skeleton-ratio` | `.juno-skeleton--tile` only — the aspect ratio (default `1`, e.g. `16 / 9`). |
|
|
25
29
|
|
|
26
30
|
## Usage
|
|
27
31
|
|
|
28
32
|
- Wrap the loading region in `aria-busy="true"` (and `aria-live="polite"`) so assistive
|
|
29
33
|
tech announces the pending state; swap in the real content when it arrives.
|
|
30
|
-
- Mirror the **shape** of the content it replaces — line widths, an avatar circle
|
|
31
|
-
the layout doesn't jump on load.
|
|
32
|
-
- Width is layout-driven: set `inline-size` (e.g. `60%`) per line; height via the variant
|
|
33
|
-
or `--juno-skeleton-
|
|
34
|
+
- Mirror the **shape** of the content it replaces — line widths, an avatar circle, a
|
|
35
|
+
tile's aspect ratio — so the layout doesn't jump on load.
|
|
36
|
+
- Width is layout-driven: set `inline-size` (e.g. `60%`) per line; height via the variant,
|
|
37
|
+
`--juno-skeleton-h`, or (for `--tile`) `--juno-skeleton-ratio`.
|
|
34
38
|
|
|
35
39
|
## Anatomy (any platform)
|
|
36
40
|
|
|
37
|
-
- `s2
|
|
38
|
-
|
|
41
|
+
- Solid `s2` fill; the shimmer is a separate overlay layer sliding across on `transform`
|
|
42
|
+
only (never `background-position`), so it animates on the compositor instead of
|
|
43
|
+
repainting the gradient every frame — cheap even with hundreds of skeletons on screen.
|
|
44
|
+
Highlight band `s3` at 70% opacity, 1.2s ease-in-out loop. Radius `3` (block/tile `4`,
|
|
45
|
+
text `2`). Static fill under reduced motion.
|
package/docs/components/thumb.md
CHANGED
|
@@ -8,37 +8,54 @@ that is **missing or failed**.
|
|
|
8
8
|
## Web
|
|
9
9
|
|
|
10
10
|
```html
|
|
11
|
-
<!-- normal: image covers the placeholder -->
|
|
12
|
-
<figure class="juno-thumb"
|
|
11
|
+
<!-- normal: image covers the placeholder; aspect defaults to square -->
|
|
12
|
+
<figure class="juno-thumb">
|
|
13
13
|
<img src="…" alt="Sunset clip" onerror="this.remove()" />
|
|
14
14
|
</figure>
|
|
15
15
|
|
|
16
|
-
<!--
|
|
16
|
+
<!-- non-square: override the ratio, not the aspect-ratio property -->
|
|
17
17
|
<figure
|
|
18
18
|
class="juno-thumb juno-thumb--video"
|
|
19
|
-
style="
|
|
19
|
+
style="--juno-thumb-ratio: 16/9"
|
|
20
20
|
role="img"
|
|
21
21
|
aria-label="Preview unavailable"
|
|
22
22
|
>
|
|
23
23
|
<figcaption class="juno-thumb__label">Unavailable</figcaption>
|
|
24
24
|
</figure>
|
|
25
|
+
|
|
26
|
+
<!-- selected, with corner overlays: a check top-left, a duration top-right -->
|
|
27
|
+
<figure class="juno-thumb juno-thumb--selected">
|
|
28
|
+
<img src="…" alt="Sunset clip" onerror="this.remove()" />
|
|
29
|
+
<span class="juno-thumb__corner juno-thumb__corner--top-start badge badge--sm">✓</span>
|
|
30
|
+
<span class="juno-thumb__corner juno-thumb__corner--top-end badge badge--sm">0:42</span>
|
|
31
|
+
</figure>
|
|
25
32
|
```
|
|
26
33
|
|
|
27
|
-
| Class / attr
|
|
28
|
-
|
|
|
29
|
-
| `.juno-thumb`
|
|
30
|
-
| `.juno-thumb--
|
|
31
|
-
| `.juno-
|
|
32
|
-
|
|
|
33
|
-
| `
|
|
34
|
+
| Class / attr | Effect |
|
|
35
|
+
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
36
|
+
| `.juno-thumb` | Frame: `s2` fill, hairline border, `radius.3`, centered glyph, aspect locked to `--juno-thumb-ratio` (default `1`). |
|
|
37
|
+
| `.juno-thumb--flush` | Drops the border/radius — full-bleed tiles in a tight wall. |
|
|
38
|
+
| `.juno-thumb--selected` | Inset outline (`--juno-active`) — never a border, so nothing reflows on toggle. Apps own the state; junoui never adds/removes the class. |
|
|
39
|
+
| `.juno-thumb--video` | Play glyph instead of the image glyph. |
|
|
40
|
+
| `.juno-thumb__label` | Optional uppercase micro-caption under the glyph. |
|
|
41
|
+
| `.juno-thumb__corner` | Absolutely-positioned slot over the media, `space.4` inset from the frame edge, above the media (`z-index: 1`). Pair with one position modifier. |
|
|
42
|
+
| `.juno-thumb__corner--top-start` / `--top-right` / `--bottom-left` / `--bottom-right` | Anchors the slot to a corner using logical `inset-block-*` / `inset-inline-*` — start/end swap correctly under `dir="rtl"`. |
|
|
43
|
+
| `> img` / `> video` | Covers the frame (`object-fit: cover`). |
|
|
44
|
+
| `onerror="this.remove()"` | The whole JS contract — optional, stateless, one attribute. |
|
|
34
45
|
|
|
35
46
|
## Anatomy (any platform)
|
|
36
47
|
|
|
37
|
-
- Frame: surface `s2`, 1px `border`, radius `3`;
|
|
38
|
-
|
|
48
|
+
- Frame: surface `s2`, 1px `border`, radius `3`; aspect locked by
|
|
49
|
+
`--juno-thumb-ratio` (default square) so a media wall's scroll height is
|
|
50
|
+
stable before anything loads — override per instance
|
|
51
|
+
(`style="--juno-thumb-ratio: 16/9"`) or via the [tiles grid](../layout.md).
|
|
39
52
|
- Glyph: 28% of the frame (capped `space.32`), `muted` color — a missing thumb
|
|
40
53
|
is **not** a warning state; the placeholder stays neutral.
|
|
41
54
|
- Media covers the full frame, center-cropped.
|
|
55
|
+
- Selection is an inset outline, not a border — the frame's box never
|
|
56
|
+
changes size, so a wall re-flowing selection doesn't jitter.
|
|
57
|
+
- Corner slots are presentational anchors only; what occupies them (check
|
|
58
|
+
icon, duration chip, storage-tier badge) is app vocabulary.
|
|
42
59
|
|
|
43
60
|
## Failure contract
|
|
44
61
|
|
|
@@ -58,4 +75,7 @@ explicit:
|
|
|
58
75
|
video poster slots, attachment previews.
|
|
59
76
|
- Keep the placeholder neutral; if failure _matters_ (broken pipeline), say it
|
|
60
77
|
with a [badge](./badge.md) or [alert](./alert.md) next to the thumb.
|
|
61
|
-
-
|
|
78
|
+
- Multi-select galleries: toggle `--selected` per tile and put a check in a
|
|
79
|
+
`__corner` slot; duration/storage-tier chips are `__corner` occupants too.
|
|
80
|
+
- Retry, lazy-load, LQIP/blur-up, and the selection/click behavior itself:
|
|
81
|
+
app or `junoui-<framework>` territory.
|
|
@@ -40,6 +40,15 @@ Neutral / structural roles carry no status meaning:
|
|
|
40
40
|
stop value-change jitter. Use `tabular-nums`.
|
|
41
41
|
- **B612** — all non-numeric UI: headings, labels, navigation, buttons.
|
|
42
42
|
- Headings are uppercase with wide tracking; values are mono and bright (`data`).
|
|
43
|
+
- `.juno-label` reads an optional `--juno-label-size` knob (falls back to
|
|
44
|
+
`--juno-font-size-13`) so a context can resize labels — e.g. a compact list —
|
|
45
|
+
without forking the class: set the custom property on an ancestor, never on
|
|
46
|
+
`.juno-label` itself.
|
|
47
|
+
```html
|
|
48
|
+
<div style="--juno-label-size: var(--juno-font-size-11)">
|
|
49
|
+
<span class="juno-label">Signal strength</span>
|
|
50
|
+
</div>
|
|
51
|
+
```
|
|
43
52
|
|
|
44
53
|
## Date & time
|
|
45
54
|
|
|
@@ -123,6 +132,23 @@ edges. Interactive controls keep their `min-height` (WCAG tap target); only padd
|
|
|
123
132
|
shrinks. New components should use the aliases for internal padding to inherit
|
|
124
133
|
density for free; add a new archetype only when one is genuinely needed.
|
|
125
134
|
|
|
135
|
+
A third value, `auto`, re-densifies for small **coarse-pointer** viewports (phone,
|
|
136
|
+
`pointer: coarse` and width ≤ 640px) — a data-heavy layout gets its compact-ish
|
|
137
|
+
padding back on a phone without the consumer hand-tracking breakpoints:
|
|
138
|
+
|
|
139
|
+
```html
|
|
140
|
+
<body data-juno-density="auto">
|
|
141
|
+
<!-- comfortable everywhere else; re-densifies only on a narrow touch phone -->
|
|
142
|
+
</body>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`auto` is **opt-in only** — existing `comfortable`/compact-pinned consumers render
|
|
146
|
+
byte-identical, nothing changes silently. It never touches a control's
|
|
147
|
+
`min-height`; `--juno-size-tap-min` still only grows (never shrinks) under
|
|
148
|
+
`base.css`'s own `@media (pointer: coarse)` rule, so `auto` can't undo the WCAG
|
|
149
|
+
tap-target work. Only `--juno-tile-min` / `--juno-gap-content` /
|
|
150
|
+
`--juno-pad-surface-inline` shrink, same as `compact`.
|
|
151
|
+
|
|
126
152
|
## Accessibility
|
|
127
153
|
|
|
128
154
|
Accessibility is a core design goal, held to published standards — **WCAG 2.2** and
|
package/docs/getting-started.md
CHANGED
|
@@ -25,6 +25,29 @@ cd vendor/junoui && npm install # `prepare` builds dist/
|
|
|
25
25
|
|
|
26
26
|
`npm install` runs the `prepare` script, so `dist/` is always built for you.
|
|
27
27
|
|
|
28
|
+
## Required: the viewport meta
|
|
29
|
+
|
|
30
|
+
Ship this on every page. It is not optional if you use anything that touches a
|
|
31
|
+
phone edge — the dock, pillbar, navbar, drawer, bottom sheet, toast, or the
|
|
32
|
+
app-shell:
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Two things depend on it, and both fail **silently**:
|
|
39
|
+
|
|
40
|
+
- **`viewport-fit=cover` turns the safe area on.** iOS defaults `viewport-fit`
|
|
41
|
+
to `auto`, and WebKit reports every `env(safe-area-inset-*)` as `0` unless you
|
|
42
|
+
opt in with `cover` (`contain` does _not_ opt out). junoui is a stylesheet — it
|
|
43
|
+
cannot set this meta for you. Without it, every safe-area guarantee in the
|
|
44
|
+
library quietly becomes a no-op and content sits under the home indicator.
|
|
45
|
+
- **`width=device-width, initial-scale=1` makes 1 CSS px equal 1 Apple point**,
|
|
46
|
+
which is what makes junoui's `44px` tap targets actually 44pt on the device.
|
|
47
|
+
Without it iOS Safari lays out at ~980px wide and every metric is off.
|
|
48
|
+
|
|
49
|
+
Details and sources: [ios-conformance.md](./ios-conformance.md).
|
|
50
|
+
|
|
28
51
|
## The model
|
|
29
52
|
|
|
30
53
|
- **Palette** — `standard` · `colorblind` · `soft`
|