@junoput01/junoui 0.3.0 → 0.5.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 +232 -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/dist/icons/juno-icons.svg +1 -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/icon-subsetting.md +59 -0
- package/docs/ios-conformance.md +224 -0
- package/docs/layout.md +71 -1
- package/docs/web.md +6 -0
- package/package.json +7 -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
- package/src/icons/cloud-slash.svg +1 -0
- package/tools/subset-sprite.mjs +56 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
2
|
+
* Component — Load state · a vocabulary so nothing spins forever
|
|
3
|
+
* Names the treatment for every way a load can go, so no consumer reaches
|
|
4
|
+
* for a spinner on a state that will never resolve:
|
|
5
|
+
* resolving, ETA known → .juno-arc / .juno-beacon / .juno-bar (loader.css)
|
|
6
|
+
* layout known, no data → .juno-skeleton (skeleton.css)
|
|
7
|
+
* working, NO ETA → .juno-shimmer — motion without a completion promise
|
|
8
|
+
* failed (terminal) → .juno-fault — static, never animates
|
|
9
|
+
* legitimately none → .juno-empty — static, never animates
|
|
10
|
+
* Full decision table + a11y contract: docs/components/load-state.md.
|
|
11
|
+
*
|
|
12
|
+
* Also ships an optional CSS-only switch: mark each treatment's wrapper
|
|
13
|
+
* with data-juno-when, set data-juno-state on the .juno-state parent, and
|
|
14
|
+
* the matching wrapper alone gets display:flex. Zero JS in junoui — the
|
|
15
|
+
* app still decides WHEN a fetch becomes an error or an empty result.
|
|
16
|
+
* Usage:
|
|
17
|
+
* <div class="juno-state" data-juno-state="processing">
|
|
18
|
+
* <div data-juno-when="loading">
|
|
19
|
+
* <div class="juno-arc juno-arc--indeterminate" role="status" aria-label="Loading"></div>
|
|
20
|
+
* </div>
|
|
21
|
+
* <div data-juno-when="processing" aria-busy="true" aria-live="polite">
|
|
22
|
+
* <div class="juno-shimmer" style="block-size: 120px;"></div>
|
|
23
|
+
* </div>
|
|
24
|
+
* <div data-juno-when="error">
|
|
25
|
+
* <div class="juno-fault" role="status">
|
|
26
|
+
* <span class="juno-fault__icon" aria-hidden="true">!</span>
|
|
27
|
+
* <p>Couldn't load this.</p>
|
|
28
|
+
* </div>
|
|
29
|
+
* </div>
|
|
30
|
+
* <div data-juno-when="empty">
|
|
31
|
+
* <div class="juno-empty">
|
|
32
|
+
* <span class="juno-empty__icon" aria-hidden="true">∅</span>
|
|
33
|
+
* <p>Nothing here yet.</p>
|
|
34
|
+
* </div>
|
|
35
|
+
* </div>
|
|
36
|
+
* </div>
|
|
37
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
38
|
+
|
|
39
|
+
/* ── STATE SWITCH — mechanical only ─────────────────────────────────────
|
|
40
|
+
Hide every marked child; show only the one(s) whose data-juno-when token
|
|
41
|
+
list contains the parent's current data-juno-state. */
|
|
42
|
+
.juno-state > [data-juno-when] { display: none; }
|
|
43
|
+
|
|
44
|
+
.juno-state[data-juno-state='loading'] > [data-juno-when~='loading'],
|
|
45
|
+
.juno-state[data-juno-state='processing'] > [data-juno-when~='processing'],
|
|
46
|
+
.juno-state[data-juno-state='error'] > [data-juno-when~='error'],
|
|
47
|
+
.juno-state[data-juno-state='empty'] > [data-juno-when~='empty'] { display: flex; }
|
|
48
|
+
|
|
49
|
+
/* ── SHIMMER — work in progress, no completion promise ──────────────────
|
|
50
|
+
For server-side work with no ETA (transcoding, indexing…): a spinner
|
|
51
|
+
would spin forever, and a .juno-skeleton implies a known layout that
|
|
52
|
+
doesn't exist here either — just "something is happening". Shares
|
|
53
|
+
.juno-skeleton's compositor-only band verbatim (same @keyframes,
|
|
54
|
+
defined once in skeleton.css — not redefined here) so there is one
|
|
55
|
+
shimmer implementation, not two. Mark the region aria-busy="true". */
|
|
56
|
+
.juno-shimmer {
|
|
57
|
+
position: relative;
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
background: var(--juno-s2);
|
|
60
|
+
border-radius: var(--juno-radius-3);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.juno-shimmer::before {
|
|
64
|
+
content: '';
|
|
65
|
+
position: absolute;
|
|
66
|
+
inset: 0;
|
|
67
|
+
background: linear-gradient(
|
|
68
|
+
100deg,
|
|
69
|
+
transparent 20%,
|
|
70
|
+
color-mix(in srgb, var(--juno-s3) 70%, transparent) 50%,
|
|
71
|
+
transparent 80%
|
|
72
|
+
);
|
|
73
|
+
animation: juno-skeleton-shimmer var(--juno-shimmer-dur, 1.4s) ease-in-out infinite;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ── FAULT — terminal, never animates ────────────────────────────────────
|
|
77
|
+
The load will not resolve on its own (404, decode failure, permanent
|
|
78
|
+
backend error). Static by design — motion here would lie about the
|
|
79
|
+
state. Role defaults to caution; recolor with a .juno--<role> class for
|
|
80
|
+
a different severity, never hardcode a color. This is a per-region
|
|
81
|
+
fault, not a page-level alert — .juno-alert (role="alert") is for that;
|
|
82
|
+
here the app wires role="status" so AT announces the failure once,
|
|
83
|
+
politely, without interrupting. */
|
|
84
|
+
.juno-fault {
|
|
85
|
+
--juno-role: var(--juno-caution);
|
|
86
|
+
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
align-items: center;
|
|
90
|
+
justify-content: center;
|
|
91
|
+
gap: var(--juno-space-8);
|
|
92
|
+
padding: var(--juno-space-24);
|
|
93
|
+
text-align: center;
|
|
94
|
+
color: var(--juno-role);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.juno-fault__icon {
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
inline-size: var(--juno-space-40);
|
|
102
|
+
block-size: var(--juno-space-40);
|
|
103
|
+
border-radius: var(--juno-radius-8);
|
|
104
|
+
border: var(--juno-border-width-1) solid currentcolor;
|
|
105
|
+
font-family: var(--juno-font-family-mono);
|
|
106
|
+
font-size: var(--juno-font-size-18);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ── EMPTY — legitimately nothing, terminal ──────────────────────────────
|
|
110
|
+
The load resolved; there is no data, and none will appear without new
|
|
111
|
+
input from the user. Generalized, table-agnostic form of the existing
|
|
112
|
+
.juno-table__empty (table.css) — same anatomy, any region. Neutral
|
|
113
|
+
(muted) — an empty result isn't a failure, don't tint it like one. */
|
|
114
|
+
.juno-empty {
|
|
115
|
+
display: flex;
|
|
116
|
+
flex-direction: column;
|
|
117
|
+
align-items: center;
|
|
118
|
+
justify-content: center;
|
|
119
|
+
gap: var(--juno-space-12);
|
|
120
|
+
padding: var(--juno-space-56) var(--juno-space-24);
|
|
121
|
+
text-align: center;
|
|
122
|
+
color: var(--juno-label);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.juno-empty__icon {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
justify-content: center;
|
|
129
|
+
inline-size: var(--juno-space-40);
|
|
130
|
+
block-size: var(--juno-space-40);
|
|
131
|
+
border-radius: var(--juno-radius-8);
|
|
132
|
+
border: var(--juno-border-width-1) solid var(--juno-control-edge);
|
|
133
|
+
color: var(--juno-muted);
|
|
134
|
+
font-family: var(--juno-font-family-mono);
|
|
135
|
+
font-size: var(--juno-font-size-18);
|
|
136
|
+
}
|
|
@@ -41,6 +41,12 @@
|
|
|
41
41
|
animation: juno-arc-rot 1.1s steps(12) infinite; /* mechanical, 12-step sweep */
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/* the 12-step sweep reads as jitter under ~24px (e.g. rung around a small
|
|
45
|
+
badge via .juno-icon-loader) — swap to a continuous rotation there */
|
|
46
|
+
.juno-arc--smooth.juno-arc--indeterminate {
|
|
47
|
+
animation-timing-function: linear;
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
/* centred readout for determinate arcs — place a span inside */
|
|
45
51
|
.juno-arc__label {
|
|
46
52
|
position: absolute;
|
|
@@ -65,6 +65,10 @@
|
|
|
65
65
|
align-items: center;
|
|
66
66
|
gap: var(--juno-gap-control);
|
|
67
67
|
inline-size: 100%;
|
|
68
|
+
|
|
69
|
+
/* Dock overflow routes here on phones, making this a touch surface; hold the
|
|
70
|
+
tap minimum (auto-promotes to 44px on coarse pointers). See 20260802-020. */
|
|
71
|
+
min-block-size: var(--juno-size-tap-min);
|
|
68
72
|
padding: var(--juno-space-8) var(--juno-space-10);
|
|
69
73
|
background: transparent;
|
|
70
74
|
border: none;
|
|
@@ -45,6 +45,11 @@
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
.juno-modal[open] {
|
|
48
|
+
/* Column so __body can be the bounded scroll port: the surface caps its own
|
|
49
|
+
height (85dvh as a sheet) and clips, so without this a tall body is simply
|
|
50
|
+
cut off and unreachable. See 20260803-029. */
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: column;
|
|
48
53
|
opacity: 1;
|
|
49
54
|
transform: translateY(0) scale(1);
|
|
50
55
|
}
|
|
@@ -60,6 +65,7 @@
|
|
|
60
65
|
.juno-modal::backdrop {
|
|
61
66
|
background: rgb(0 0 0);
|
|
62
67
|
opacity: var(--juno-opacity-scrim);
|
|
68
|
+
-webkit-backdrop-filter: blur(1.5px);
|
|
63
69
|
backdrop-filter: blur(1.5px);
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -100,7 +106,19 @@
|
|
|
100
106
|
|
|
101
107
|
.juno-modal__close:hover { background: var(--juno-s2); color: var(--juno-data); }
|
|
102
108
|
|
|
103
|
-
.juno-modal__body {
|
|
109
|
+
.juno-modal__body {
|
|
110
|
+
padding: var(--juno-space-12) var(--juno-pad-surface-inline) var(--juno-pad-surface-inline);
|
|
111
|
+
|
|
112
|
+
/* The scroll port (the docs already promised it "scrolls on its own").
|
|
113
|
+
min-block-size:0 lets a flex item shrink below its content so overflow-y
|
|
114
|
+
actually engages. overscroll-behavior stops the scroll chaining to the page
|
|
115
|
+
behind once the body hits its end — the rubber-band-under-the-sheet effect
|
|
116
|
+
on iOS. NOTE: overscroll-behavior is community convention here, not Apple
|
|
117
|
+
guidance; no primary Apple source covers it. See 20260803-029. */
|
|
118
|
+
min-block-size: 0;
|
|
119
|
+
overflow-y: auto;
|
|
120
|
+
overscroll-behavior: contain;
|
|
121
|
+
}
|
|
104
122
|
|
|
105
123
|
.juno-modal__title {
|
|
106
124
|
font-family: var(--juno-font-family-sans);
|
|
@@ -151,9 +169,12 @@
|
|
|
151
169
|
.juno-modal:not(.juno-drawer)[open] { transform: translateY(100%); }
|
|
152
170
|
}
|
|
153
171
|
|
|
154
|
-
/* keep the foot's actions above the home indicator
|
|
172
|
+
/* keep the foot's actions above the home indicator.
|
|
173
|
+
The env() fallback MUST carry a unit: inside calc(), a unitless `0` is a
|
|
174
|
+
<number>, the addition is invalid, and the whole declaration is dropped —
|
|
175
|
+
silently losing the padding on every device without a safe area. */
|
|
155
176
|
.juno-modal:not(.juno-drawer) .juno-modal__body {
|
|
156
|
-
padding-block-end: calc(var(--juno-pad-surface-inline) + env(safe-area-inset-bottom,
|
|
177
|
+
padding-block-end: calc(var(--juno-pad-surface-inline) + env(safe-area-inset-bottom, 0px));
|
|
157
178
|
}
|
|
158
179
|
|
|
159
180
|
/* full-width actions read better on a sheet */
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* </a>
|
|
17
17
|
* <h1 class="juno-navbar__title">Album</h1>
|
|
18
18
|
* <div class="juno-navbar__actions">
|
|
19
|
-
* <button class="juno-btn juno-btn--
|
|
19
|
+
* <button class="juno-btn juno-btn--ghost">EDIT</button>
|
|
20
20
|
* </div>
|
|
21
21
|
* </header>
|
|
22
22
|
* ════════════════════════════════════════════════════════════════════ */
|
|
@@ -97,6 +97,10 @@
|
|
|
97
97
|
gap: var(--juno-space-4);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/* Top-bar action controls are a touch surface: hold the tap minimum
|
|
101
|
+
(auto-promotes to 44px on coarse pointers). See 20260802-020. */
|
|
102
|
+
.juno-navbar__actions > * { min-block-size: var(--juno-size-tap-min); }
|
|
103
|
+
|
|
100
104
|
.juno-navbar__back:focus-visible {
|
|
101
105
|
outline: var(--juno-border-width-2) solid var(--juno-active);
|
|
102
106
|
outline-offset: calc(-1 * var(--juno-space-2));
|
|
@@ -19,23 +19,38 @@
|
|
|
19
19
|
* <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-arrows-clockwise" /></svg>
|
|
20
20
|
* </button>
|
|
21
21
|
* </nav>
|
|
22
|
+
* Overflow: for variable-membership pills (an app-owned capacity planner
|
|
23
|
+
* decides how many __item fit, reading --juno-pillbar-item/-gap/-pad below),
|
|
24
|
+
* swap the last __item for .juno-pillbar__overflow anchoring a .juno-menu —
|
|
25
|
+
* see docs/components/pillbar.md#overflow-slot and menu.css.
|
|
22
26
|
* ════════════════════════════════════════════════════════════════════ */
|
|
23
27
|
|
|
24
28
|
.juno-pillbar {
|
|
25
29
|
--juno-role: var(--juno-active);
|
|
26
30
|
|
|
31
|
+
/* Geometry contract — published so a consumer's overflow/capacity planner
|
|
32
|
+
(deciding how many __item fit before routing the rest to __overflow)
|
|
33
|
+
reads real values instead of re-declaring junoui's px constants in JS.
|
|
34
|
+
Base-rule values only; the corner variants below keep their own literal
|
|
35
|
+
offsets (distinct block vs. inline edges) and are not wired to -edge. */
|
|
36
|
+
--juno-pillbar-item: var(--juno-size-tap-comfortable);
|
|
37
|
+
--juno-pillbar-gap: var(--juno-space-2);
|
|
38
|
+
--juno-pillbar-pad: var(--juno-space-4);
|
|
39
|
+
--juno-pillbar-edge: var(--juno-space-16);
|
|
40
|
+
|
|
27
41
|
position: sticky;
|
|
28
|
-
inset-block-end: calc(var(--juno-
|
|
42
|
+
inset-block-end: calc(var(--juno-pillbar-edge) + env(safe-area-inset-bottom, 0px));
|
|
29
43
|
z-index: var(--juno-z-raised);
|
|
30
44
|
margin-inline: auto;
|
|
31
|
-
margin-block-end: var(--juno-
|
|
45
|
+
margin-block-end: var(--juno-pillbar-edge);
|
|
32
46
|
inline-size: fit-content;
|
|
33
|
-
max-inline-size: calc(100% - 2 * var(--juno-
|
|
47
|
+
max-inline-size: calc(100% - 2 * var(--juno-pillbar-edge));
|
|
34
48
|
display: flex;
|
|
35
49
|
align-items: center;
|
|
36
|
-
gap: var(--juno-
|
|
37
|
-
padding: var(--juno-
|
|
50
|
+
gap: var(--juno-pillbar-gap);
|
|
51
|
+
padding: var(--juno-pillbar-pad);
|
|
38
52
|
background: color-mix(in srgb, var(--juno-s1) 88%, transparent);
|
|
53
|
+
-webkit-backdrop-filter: blur(12px);
|
|
39
54
|
backdrop-filter: blur(12px);
|
|
40
55
|
border: var(--juno-border-width-1) solid var(--juno-border);
|
|
41
56
|
border-radius: 999px;
|
|
@@ -46,8 +61,8 @@
|
|
|
46
61
|
appearance: none;
|
|
47
62
|
margin: 0;
|
|
48
63
|
border: none;
|
|
49
|
-
min-inline-size: var(--juno-
|
|
50
|
-
min-block-size: var(--juno-
|
|
64
|
+
min-inline-size: var(--juno-pillbar-item);
|
|
65
|
+
min-block-size: var(--juno-pillbar-item);
|
|
51
66
|
display: inline-flex;
|
|
52
67
|
align-items: center;
|
|
53
68
|
justify-content: center;
|
|
@@ -94,6 +109,54 @@
|
|
|
94
109
|
cursor: not-allowed;
|
|
95
110
|
}
|
|
96
111
|
|
|
112
|
+
/* Overflow slot — the "more" trigger for items that don't fit the pill.
|
|
113
|
+
Styled like __item (same tap target, radius, hover/focus) but a dedicated
|
|
114
|
+
element because it opens a menu instead of acting as a nav/toggle item.
|
|
115
|
+
Pair with .juno-menu (menu.css) via popovertarget — the invoker is the
|
|
116
|
+
menu's implicit anchor, so no extra anchor-positioning is needed here.
|
|
117
|
+
junoui ships the docking point only: DECIDING which items overflow (and
|
|
118
|
+
when) is app policy — mount/unmount this button and move the spilled
|
|
119
|
+
__item markup into the menu, sizing the decision off the
|
|
120
|
+
--juno-pillbar-item/-gap/-pad custom props above instead of hardcoded px.
|
|
121
|
+
Zero JS in this file; aria-expanded is toggled by the app or a stateless
|
|
122
|
+
enhancer, same convention as __item's [aria-pressed].
|
|
123
|
+
.juno-pillbar__toggle (the --collapsible variant's expand/collapse control,
|
|
124
|
+
below) shares this exact chrome: same circle, same states. */
|
|
125
|
+
.juno-pillbar__overflow,
|
|
126
|
+
.juno-pillbar__toggle {
|
|
127
|
+
appearance: none;
|
|
128
|
+
margin: 0;
|
|
129
|
+
border: none;
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
min-inline-size: var(--juno-pillbar-item);
|
|
132
|
+
min-block-size: var(--juno-pillbar-item);
|
|
133
|
+
display: inline-flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
justify-content: center;
|
|
136
|
+
background: transparent;
|
|
137
|
+
border-radius: 999px;
|
|
138
|
+
color: var(--juno-label);
|
|
139
|
+
cursor: pointer;
|
|
140
|
+
transition:
|
|
141
|
+
color var(--juno-motion-duration-quick) var(--juno-motion-ease-standard),
|
|
142
|
+
background-color var(--juno-motion-duration-quick) var(--juno-motion-ease-standard);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.juno-pillbar__overflow:hover,
|
|
146
|
+
.juno-pillbar__toggle:hover { color: var(--juno-data); }
|
|
147
|
+
|
|
148
|
+
.juno-pillbar__overflow[aria-expanded='true'],
|
|
149
|
+
.juno-pillbar__toggle[aria-expanded='true'] {
|
|
150
|
+
background: var(--juno-s3);
|
|
151
|
+
color: var(--juno-role);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.juno-pillbar__overflow:focus-visible,
|
|
155
|
+
.juno-pillbar__toggle:focus-visible {
|
|
156
|
+
outline: var(--juno-border-width-2) solid var(--juno-active);
|
|
157
|
+
outline-offset: calc(-1 * var(--juno-space-2));
|
|
158
|
+
}
|
|
159
|
+
|
|
97
160
|
.juno-pillbar__sep {
|
|
98
161
|
align-self: center;
|
|
99
162
|
inline-size: var(--juno-border-width-1);
|
|
@@ -117,10 +180,147 @@
|
|
|
117
180
|
z-index: var(--juno-z-anchored);
|
|
118
181
|
}
|
|
119
182
|
|
|
183
|
+
/* Corner placement — a floating cluster pinned to one viewport corner rather
|
|
184
|
+
than the centered bottom bar (e.g. a top-right search/filter cluster, or a
|
|
185
|
+
bottom-right action pill). Each variant is self-contained: it flips to
|
|
186
|
+
fixed, clears the base's bottom-center geometry (auto margins / bottom
|
|
187
|
+
inset), and clamps the corner with safe-area insets so it never lands under
|
|
188
|
+
a notch or the home indicator. Keep the base blur/border/shadow. Pair with a
|
|
189
|
+
scroll-clearance reservation on the scroller if the corner overlaps content
|
|
190
|
+
(var(--juno-pillbar-clearance) for the bottom corners). */
|
|
191
|
+
.juno-pillbar--top-right,
|
|
192
|
+
.juno-pillbar--top-left,
|
|
193
|
+
.juno-pillbar--bottom-right,
|
|
194
|
+
.juno-pillbar--bottom-left {
|
|
195
|
+
position: fixed;
|
|
196
|
+
inset-block: auto;
|
|
197
|
+
inset-inline: auto;
|
|
198
|
+
z-index: var(--juno-z-anchored);
|
|
199
|
+
margin: 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.juno-pillbar--top-right {
|
|
203
|
+
inset-block-start: calc(var(--juno-space-10) + env(safe-area-inset-top, 0px));
|
|
204
|
+
inset-inline-end: calc(var(--juno-space-12) + env(safe-area-inset-right, 0px));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.juno-pillbar--top-left {
|
|
208
|
+
inset-block-start: calc(var(--juno-space-10) + env(safe-area-inset-top, 0px));
|
|
209
|
+
inset-inline-start: calc(var(--juno-space-12) + env(safe-area-inset-left, 0px));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.juno-pillbar--bottom-right {
|
|
213
|
+
inset-block-end: calc(var(--juno-space-16) + env(safe-area-inset-bottom, 0px));
|
|
214
|
+
inset-inline-end: calc(var(--juno-space-12) + env(safe-area-inset-right, 0px));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.juno-pillbar--bottom-left {
|
|
218
|
+
inset-block-end: calc(var(--juno-space-16) + env(safe-area-inset-bottom, 0px));
|
|
219
|
+
inset-inline-start: calc(var(--juno-space-12) + env(safe-area-inset-left, 0px));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* Input slot — an expandable search field living inside the pill. Borderless
|
|
223
|
+
and transparent so it reads as part of the pill, not a boxed control. The
|
|
224
|
+
font-size floor matters: iOS Safari zooms the whole page onto any focused
|
|
225
|
+
text field under 16px, so hold this at max(16px, …) exactly like the base
|
|
226
|
+
.juno-input touch rule. Give it an accessible label (aria-label or a
|
|
227
|
+
visually-hidden <label>) — the placeholder is not a label. */
|
|
228
|
+
.juno-pillbar__input {
|
|
229
|
+
flex: 0 1 auto;
|
|
230
|
+
min-inline-size: 0;
|
|
231
|
+
inline-size: min(52vw, 240px);
|
|
232
|
+
margin: 0;
|
|
233
|
+
padding-inline: var(--juno-space-8);
|
|
234
|
+
border: none;
|
|
235
|
+
background: transparent;
|
|
236
|
+
outline: none;
|
|
237
|
+
font-family: var(--juno-font-family-sans);
|
|
238
|
+
font-size: max(16px, var(--juno-font-size-16));
|
|
239
|
+
color: var(--juno-data);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.juno-pillbar__input::placeholder { color: var(--juno-muted); }
|
|
243
|
+
|
|
244
|
+
.juno-pillbar__input:focus-visible {
|
|
245
|
+
outline: none;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* Collapsible — the whole pill folds into a single circular toggle, and a
|
|
249
|
+
tap expands it back to full width. State lives on the toggle's
|
|
250
|
+
aria-expanded (app-owned, like __item's aria-pressed — zero JS here);
|
|
251
|
+
the bar reads it via :has(), so DOM order of toggle vs. tray is free
|
|
252
|
+
(toggle-first for a left-anchored pill, toggle-last for right-anchored).
|
|
253
|
+
|
|
254
|
+
Usage:
|
|
255
|
+
<nav class="juno-pillbar juno-pillbar--collapsible" aria-label="Tools">
|
|
256
|
+
<button class="juno-pillbar__toggle" aria-expanded="false" aria-label="Show toolbar">
|
|
257
|
+
<svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-dots-three" /></svg>
|
|
258
|
+
</button>
|
|
259
|
+
<div class="juno-pillbar__tray">
|
|
260
|
+
<div><!-- single wrapper, any element -->
|
|
261
|
+
…the usual __item / __sep / __input children…
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</nav>
|
|
265
|
+
|
|
266
|
+
Mechanism: the tray is a one-column grid whose track animates 0fr ↔ 1fr —
|
|
267
|
+
the only widely-supported way to TRANSITION to an intrinsic width (Safari 16+;
|
|
268
|
+
width: fit-content and interpolate-size aren't animatable there). The track
|
|
269
|
+
needs one shrinkable child, hence the mandatory single wrapper (any element:
|
|
270
|
+
it's selected as > *, so there is no class to forget). visibility rides the
|
|
271
|
+
same transition — discrete animation keeps the tray visible while it slides
|
|
272
|
+
shut, then hides it, which also drops the hidden items from the tab order
|
|
273
|
+
(collapsing while focus is inside the tray is the app's edge to handle:
|
|
274
|
+
move focus to the toggle first). prefers-reduced-motion needs nothing
|
|
275
|
+
component-local — the base layer zeroes every transition duration. */
|
|
276
|
+
.juno-pillbar--collapsible {
|
|
277
|
+
transition: gap var(--juno-motion-duration-deliberate) var(--juno-motion-ease-standard);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.juno-pillbar__tray {
|
|
281
|
+
display: grid;
|
|
282
|
+
grid-template-columns: 1fr;
|
|
283
|
+
transition:
|
|
284
|
+
grid-template-columns var(--juno-motion-duration-deliberate) var(--juno-motion-ease-standard),
|
|
285
|
+
opacity var(--juno-motion-duration-deliberate) var(--juno-motion-ease-standard),
|
|
286
|
+
visibility var(--juno-motion-duration-deliberate) var(--juno-motion-ease-standard);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.juno-pillbar__tray > * {
|
|
290
|
+
display: flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
gap: var(--juno-pillbar-gap);
|
|
293
|
+
min-inline-size: 0;
|
|
294
|
+
overflow: hidden;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.juno-pillbar--collapsible:has(> .juno-pillbar__toggle[aria-expanded='false']) {
|
|
298
|
+
/* gap → 0 so the collapsed pill is exactly the toggle's circle, whichever
|
|
299
|
+
side the tray sits on (a negative margin would have to know the side) */
|
|
300
|
+
gap: 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.juno-pillbar--collapsible:has(> .juno-pillbar__toggle[aria-expanded='false']) .juno-pillbar__tray {
|
|
304
|
+
grid-template-columns: 0fr;
|
|
305
|
+
opacity: 0;
|
|
306
|
+
visibility: hidden;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* the toggle's glyph turns a quarter as the tray opens — motion reads as
|
|
310
|
+
"this button did it" even when the expansion pushes the pill off-center */
|
|
311
|
+
.juno-pillbar__toggle .juno-icon {
|
|
312
|
+
transition: transform var(--juno-motion-duration-deliberate) var(--juno-motion-ease-standard);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.juno-pillbar__toggle[aria-expanded='true'] .juno-icon {
|
|
316
|
+
transform: rotate(90deg);
|
|
317
|
+
}
|
|
318
|
+
|
|
120
319
|
/* reduced-transparency preference → solid surface, no blur */
|
|
121
320
|
@media (prefers-reduced-transparency: reduce) {
|
|
122
321
|
.juno-pillbar {
|
|
123
322
|
background: var(--juno-s1);
|
|
323
|
+
-webkit-backdrop-filter: none;
|
|
124
324
|
backdrop-filter: none;
|
|
125
325
|
}
|
|
126
326
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
2
|
+
* Component — Reload indicator (refetch-in-place)
|
|
3
|
+
* The non-blocking counterpart to the skeleton. Skeletons stand in for
|
|
4
|
+
* content that isn't there yet (first paint); the reload dot signals a
|
|
5
|
+
* refresh happening OVER content that's already on screen — the stale data
|
|
6
|
+
* stays readable and interactive while fresh data lands.
|
|
7
|
+
*
|
|
8
|
+
* A small pulsing dot with a soft halo, centered as a fixed overlay that
|
|
9
|
+
* eats no pointer events (the page underneath stays fully usable). Color
|
|
10
|
+
* comes from --juno-role (default = active). The app owns the state: render
|
|
11
|
+
* it while a background refetch is in flight, drop it when it settles.
|
|
12
|
+
*
|
|
13
|
+
* a11y: role="status" + aria-label so assistive tech announces the refresh
|
|
14
|
+
* politely without stealing focus. Keep it a live status, not an alert.
|
|
15
|
+
* Usage:
|
|
16
|
+
* <div class="juno-reload" role="status" aria-label="Reloading">
|
|
17
|
+
* <span class="juno-reload__dot"></span>
|
|
18
|
+
* </div>
|
|
19
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
20
|
+
|
|
21
|
+
.juno-reload {
|
|
22
|
+
--juno-role: var(--juno-active);
|
|
23
|
+
|
|
24
|
+
position: fixed;
|
|
25
|
+
inset-block-start: 50%;
|
|
26
|
+
inset-inline-start: 50%;
|
|
27
|
+
transform: translate(-50%, -50%);
|
|
28
|
+
z-index: var(--juno-z-anchored);
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
pointer-events: none; /* refresh is non-blocking — never trap clicks */
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.juno-reload__dot {
|
|
36
|
+
inline-size: var(--juno-space-16);
|
|
37
|
+
block-size: var(--juno-space-16);
|
|
38
|
+
border-radius: 50%;
|
|
39
|
+
background: var(--juno-role);
|
|
40
|
+
|
|
41
|
+
/* soft halo so the dot reads over any surface without a hard edge */
|
|
42
|
+
box-shadow: 0 0 0 var(--juno-space-8) color-mix(in srgb, var(--juno-role) 20%, transparent);
|
|
43
|
+
animation: juno-pulse 1.4s ease-in-out infinite;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* prefers-reduced-motion: the base layer already slows every animation to a
|
|
47
|
+
stop; the dot + halo stay visible (steady), so the "refreshing" state is
|
|
48
|
+
still conveyed without the pulse. */
|
|
@@ -2,14 +2,20 @@
|
|
|
2
2
|
* Component — Skeleton · loading placeholder
|
|
3
3
|
* A shimmering block that stands in for content not yet loaded. Same
|
|
4
4
|
* shimmer language as the table's row skeleton, generalized: text lines,
|
|
5
|
-
* blocks,
|
|
6
|
-
* --juno-skeleton-h
|
|
7
|
-
*
|
|
5
|
+
* blocks, circles (avatars), and aspect-ratio tiles (media grids). Zero
|
|
6
|
+
* JS — size it with width / height, --juno-skeleton-h, or (for --tile)
|
|
7
|
+
* --juno-skeleton-ratio. The shimmer is a `::before` overlay animated on
|
|
8
|
+
* transform/opacity only — never background-position — so it stays on
|
|
9
|
+
* the compositor instead of repainting the gradient every frame; the
|
|
10
|
+
* base fill is a plain solid color. Reduced motion is handled globally
|
|
11
|
+
* (base.css collapses all animation-duration to ~0), no local override
|
|
12
|
+
* needed here. Mark the loading region aria-busy="true" so AT announces it.
|
|
8
13
|
* Usage:
|
|
9
14
|
* <div aria-busy="true" aria-live="polite">
|
|
10
15
|
* <span class="juno-skeleton juno-skeleton--circle" style="--juno-skeleton-h: 48px;"></span>
|
|
11
16
|
* <span class="juno-skeleton juno-skeleton--text" style="inline-size: 60%;"></span>
|
|
12
17
|
* <span class="juno-skeleton juno-skeleton--text"></span>
|
|
18
|
+
* <span class="juno-skeleton juno-skeleton--tile"></span>
|
|
13
19
|
* </div>
|
|
14
20
|
* ════════════════════════════════════════════════════════════════════ */
|
|
15
21
|
|
|
@@ -17,11 +23,27 @@
|
|
|
17
23
|
--juno-skeleton-h: var(--juno-space-16);
|
|
18
24
|
|
|
19
25
|
display: block;
|
|
26
|
+
position: relative;
|
|
27
|
+
overflow: hidden;
|
|
20
28
|
inline-size: 100%;
|
|
21
29
|
block-size: var(--juno-skeleton-h);
|
|
22
30
|
border-radius: var(--juno-radius-3);
|
|
23
|
-
background:
|
|
24
|
-
|
|
31
|
+
background: var(--juno-s2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* the shimmer itself: a diagonal highlight band that slides across via
|
|
35
|
+
transform only (compositor-only — no layout/paint per frame). Clipped to
|
|
36
|
+
the parent's border-radius by the overflow: hidden above. */
|
|
37
|
+
.juno-skeleton::before {
|
|
38
|
+
content: '';
|
|
39
|
+
position: absolute;
|
|
40
|
+
inset: 0;
|
|
41
|
+
background: linear-gradient(
|
|
42
|
+
100deg,
|
|
43
|
+
transparent 20%,
|
|
44
|
+
color-mix(in srgb, var(--juno-s3) 70%, transparent) 50%,
|
|
45
|
+
transparent 80%
|
|
46
|
+
);
|
|
25
47
|
animation: juno-skeleton-shimmer 1.2s ease-in-out infinite;
|
|
26
48
|
}
|
|
27
49
|
|
|
@@ -45,18 +67,24 @@
|
|
|
45
67
|
border-radius: var(--juno-radius-4);
|
|
46
68
|
}
|
|
47
69
|
|
|
70
|
+
/* content-box mode: size from an aspect ratio instead of a fixed height —
|
|
71
|
+
the dominant case for media-grid tiles standing in for a real image/card
|
|
72
|
+
of known proportions. Default 1 (square); override per instance with
|
|
73
|
+
--juno-skeleton-ratio (e.g. "16 / 9"). */
|
|
74
|
+
.juno-skeleton--tile {
|
|
75
|
+
--juno-skeleton-ratio: 1;
|
|
76
|
+
|
|
77
|
+
aspect-ratio: var(--juno-skeleton-ratio);
|
|
78
|
+
block-size: auto;
|
|
79
|
+
border-radius: var(--juno-radius-4);
|
|
80
|
+
}
|
|
81
|
+
|
|
48
82
|
@keyframes juno-skeleton-shimmer {
|
|
49
83
|
from {
|
|
50
|
-
|
|
84
|
+
transform: translateX(-100%);
|
|
51
85
|
}
|
|
52
86
|
|
|
53
87
|
to {
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
@media (prefers-reduced-motion: reduce) {
|
|
59
|
-
.juno-skeleton {
|
|
60
|
-
animation: none;
|
|
88
|
+
transform: translateX(100%);
|
|
61
89
|
}
|
|
62
90
|
}
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
scrollbar-width: none;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/* scrollbar-width only reached Safari 18.2, so the scrollable strip still shows
|
|
33
|
+
a bar on older iOS — keep the WebKit pseudo-element alongside it (same pair
|
|
34
|
+
.juno-scroller--bare uses). */
|
|
35
|
+
.juno-tabs__list::-webkit-scrollbar { display: none; }
|
|
36
|
+
|
|
32
37
|
.juno-tabs__tab {
|
|
33
38
|
appearance: none;
|
|
34
39
|
margin: 0;
|