@junoput01/junoui 0.4.0 → 0.6.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 +97 -0
- package/README.md +29 -0
- package/dist/css/juno.css +144 -21
- package/dist/icons/inline.js +8 -10
- package/dist/icons/install.js +26 -0
- package/dist/icons/juno-icons.svg +1 -0
- package/docs/browser-support.md +173 -0
- package/docs/getting-started.md +22 -0
- package/docs/icon-subsetting.md +88 -0
- package/docs/ios-conformance.md +206 -4
- package/docs/layout.md +35 -0
- package/package.json +13 -2
- package/src/css/base.css +79 -20
- package/src/css/components/input.css +12 -0
- package/src/css/components/pillbar.css +1 -1
- package/src/css/overrides.css +51 -0
- package/src/icons/cloud-slash.svg +1 -0
- package/tools/subset-sprite.mjs +56 -0
package/src/css/base.css
CHANGED
|
@@ -19,13 +19,48 @@
|
|
|
19
19
|
/* Floating-nav scroll clearance — a page/scroller that a fixed dock or
|
|
20
20
|
pillbar floats over reserves this much room at its foot so the last row
|
|
21
21
|
clears the overlay. Consumers write `padding-block-end:
|
|
22
|
-
var(--juno-dock-clearance)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
var(--juno-dock-clearance)`. Web-only (they carry env()), so they live in
|
|
23
|
+
the CSS layer, not the cross-platform token set.
|
|
24
|
+
|
|
25
|
+
DERIVED, not constant. These used to be `space-72 + space-20 + env()`,
|
|
26
|
+
which promised in this very comment to "stay correct when the dock
|
|
27
|
+
geometry changes" and could not: the floating pill measures 62px with the
|
|
28
|
+
default 44px bubble, and junoui explicitly invites a consumer to
|
|
29
|
+
parameterize that bubble (--juno-size-tap-comfortable). Past a 58px bubble
|
|
30
|
+
the old constant reserved LESS than the pill's own height plus its margin
|
|
31
|
+
— content hid under the dock, silently, on exactly the consumers who took
|
|
32
|
+
the invitation (20260815-026).
|
|
33
|
+
|
|
34
|
+
So the parts are named and the arithmetic is visible:
|
|
35
|
+
--juno-dock-h the pill's laid-out height: the bubble, the item's
|
|
36
|
+
padding-block, the pill's own padding, its border.
|
|
37
|
+
* -clearance-scale consumers that SHRINK the bar while scrolling
|
|
38
|
+
reserve at the small size, not the live one — a
|
|
39
|
+
reservation that tracks a value which changes after
|
|
40
|
+
the last scroll event relayouts content under a
|
|
41
|
+
finger that has already stopped moving. Default 1.
|
|
42
|
+
+ space-16 + env() the pill's own bottom margin (dock.css), additive
|
|
43
|
+
because a floating element sits OFF the edge —
|
|
44
|
+
docs/ios-conformance.md, "max() vs addition".
|
|
45
|
+
+ space-8 breathing room, so the last row is not flush. */
|
|
46
|
+
--juno-dock-h: calc(
|
|
47
|
+
var(--juno-size-tap-comfortable) + 4 * var(--juno-space-4) + 2 * var(--juno-border-width-1)
|
|
48
|
+
);
|
|
49
|
+
--juno-dock-clearance-scale: 1;
|
|
50
|
+
--juno-dock-clearance: calc(
|
|
51
|
+
var(--juno-dock-h) * var(--juno-dock-clearance-scale) + var(--juno-space-16) +
|
|
52
|
+
var(--juno-space-8) + env(safe-area-inset-bottom, 0px)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/* The pillbar is the same shape one size down: no bubble, so its height is
|
|
56
|
+
the tap target plus its own padding and border. */
|
|
57
|
+
--juno-pillbar-h: calc(
|
|
58
|
+
var(--juno-size-tap-comfortable) + 2 * var(--juno-space-4) + 2 * var(--juno-border-width-1)
|
|
59
|
+
);
|
|
60
|
+
--juno-pillbar-clearance: calc(
|
|
61
|
+
var(--juno-pillbar-h) * var(--juno-dock-clearance-scale) + var(--juno-space-16) +
|
|
62
|
+
var(--juno-space-8) + env(safe-area-inset-bottom, 0px)
|
|
63
|
+
);
|
|
29
64
|
|
|
30
65
|
/* JS-readable motion contract. CSS-only `prefers-reduced-motion` never reaches
|
|
31
66
|
imperative JS (scrollTo/scrollIntoView smooth behavior, rAF-driven transforms,
|
|
@@ -127,8 +162,11 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
127
162
|
is harmless either way: hold text-entry controls (.juno-input covers input,
|
|
128
163
|
textarea and the select's inner control) at a 16px floor on touch. max()
|
|
129
164
|
keeps the floor even under a scaled-down --juno-font-scale, and still grows
|
|
130
|
-
when scaled up.
|
|
131
|
-
|
|
165
|
+
when scaled up.
|
|
166
|
+
THE RULE ITSELF LIVES IN components/input.css, not here: a media query adds
|
|
167
|
+
no specificity, so this file's `.juno-input` would lose to input.css's own
|
|
168
|
+
`.juno-input` font-size later in the bundle. It did, silently, until
|
|
169
|
+
20260815-006's coarse-pointer project measured it. */
|
|
132
170
|
|
|
133
171
|
/* Kill the UA tap-highlight square on the interactive surfaces so it never
|
|
134
172
|
flashes past a rounded control on tap. Consumers were adding this by hand
|
|
@@ -229,17 +267,11 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
229
267
|
:root { --juno-border: var(--juno-border-strong); }
|
|
230
268
|
}
|
|
231
269
|
|
|
232
|
-
/* Windows High Contrast / forced-colors:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
.juno-badge,
|
|
238
|
-
.juno-btn,
|
|
239
|
-
.juno-card,
|
|
240
|
-
.juno-readout { border: 1px solid CanvasText; }
|
|
241
|
-
.juno-badge { forced-color-adjust: none; } /* keep status fill meaningful */
|
|
242
|
-
}
|
|
270
|
+
/* Windows High Contrast / forced-colors: the opt-in lives in
|
|
271
|
+
overrides.css, NOT here. A media query adds no specificity, and this file is
|
|
272
|
+
bundled BEFORE components/, so `.juno-btn { border: … }` in button.css beat
|
|
273
|
+
the forced-colors border every time — same 0,1,0 selector, later in the
|
|
274
|
+
bundle. See src/css/overrides.css and ticket 20260815-029. */
|
|
243
275
|
|
|
244
276
|
/* ── iOS standalone letterbox unlock ─────────────────────────────────────────
|
|
245
277
|
iOS WebKit (observed through 26.6, iPhone 16 Pro, iOS 18.7) sizes a
|
|
@@ -288,3 +320,30 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
288
320
|
}
|
|
289
321
|
}
|
|
290
322
|
}
|
|
323
|
+
|
|
324
|
+
/* ── Popover API absent (Safari/iOS < 17.0) ──────────────────────────────────
|
|
325
|
+
junoui's overlay surfaces are built on the native Popover API. Without it
|
|
326
|
+
`popovertarget` does nothing, so they cannot open — an inconvenience. The
|
|
327
|
+
part that is a BUG is that the UA rule which hides a closed popover
|
|
328
|
+
(`[popover]:not(:popover-open) { display: none }`) does not exist either, so
|
|
329
|
+
the panel participates in layout: .juno-menu and .juno-popover are
|
|
330
|
+
`position: fixed` with `opacity: 0` and no pointer-events reset, i.e. a
|
|
331
|
+
256-280px INVISIBLE panel parked at its static position, swallowing taps on
|
|
332
|
+
whatever it covers. (.juno-tooltip__bubble escapes it — it inherits
|
|
333
|
+
`pointer-events: none` from the CSS-only tooltip rule — and is listed anyway
|
|
334
|
+
so the set matches the docs.)
|
|
335
|
+
|
|
336
|
+
So: hide them. Absent beats invisibly-present. Apps detect the same condition
|
|
337
|
+
with CSS.supports('selector(:popover-open)') and render a fallback.
|
|
338
|
+
|
|
339
|
+
@supports selector() is itself Safari 14.1+; below that this condition is
|
|
340
|
+
invalid, the block is skipped, and behaviour is what it is today. Fail-open,
|
|
341
|
+
and far below any supported floor. Guards live here rather than per component
|
|
342
|
+
so the next popover-based component inherits it — see docs/browser-support.md
|
|
343
|
+
for the rule about when a feature gap earns a guard at all (functional
|
|
344
|
+
failures only; a missing animation does not get one). */
|
|
345
|
+
@supports not selector(:popover-open) {
|
|
346
|
+
.juno-menu[popover],
|
|
347
|
+
.juno-popover[popover],
|
|
348
|
+
.juno-tooltip__bubble[popover] { display: none; }
|
|
349
|
+
}
|
|
@@ -28,6 +28,18 @@
|
|
|
28
28
|
color: var(--juno-data);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/* The iOS focus-zoom floor, and it has to live HERE, not in base.css's
|
|
32
|
+
`@media (pointer: coarse)` block where it was written. A media query adds no
|
|
33
|
+
specificity, so `.juno-input { font-size: var(--juno-font-size-14) }` above —
|
|
34
|
+
same 0,1,0 selector, later in the bundle (base.css sorts before components/)
|
|
35
|
+
— simply won it, and the floor never applied on any touch device. Nothing
|
|
36
|
+
caught it because the suite had no coarse-pointer project until 20260815-006
|
|
37
|
+
added one; the numeric assertion in test/visual/tap-targets.spec.mjs is what
|
|
38
|
+
surfaced it. See base.css for the sourcing caveat on the behavior itself. */
|
|
39
|
+
@media (pointer: coarse) {
|
|
40
|
+
.juno-input { font-size: max(16px, var(--juno-font-size-16)); }
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
.juno-input--sans { font-family: var(--juno-font-family-sans); }
|
|
32
44
|
|
|
33
45
|
.juno-input::placeholder { color: var(--juno-muted); }
|
|
@@ -257,7 +257,7 @@
|
|
|
257
257
|
<svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-dots-three" /></svg>
|
|
258
258
|
</button>
|
|
259
259
|
<div class="juno-pillbar__tray">
|
|
260
|
-
<div
|
|
260
|
+
<div>\3c !-- single wrapper, any element -->
|
|
261
261
|
…the usual __item / __sep / __input children…
|
|
262
262
|
</div>
|
|
263
263
|
</div>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
2
|
+
* Overrides — cross-cutting gates, bundled LAST
|
|
3
|
+
* ════════════════════════════════════════════════════════════════════
|
|
4
|
+
* A @media or @supports block adds NO specificity. So a gate written in
|
|
5
|
+
* base.css — which the bundler emits before components/ — loses to any
|
|
6
|
+
* component rule declaring the same property on the same selector, purely
|
|
7
|
+
* on source order, and loses SILENTLY: the gate looks right in the file it
|
|
8
|
+
* was written in, and there is no error anywhere.
|
|
9
|
+
*
|
|
10
|
+
* Four instances of that shipped before this file existed (2026-08-15):
|
|
11
|
+
* - .juno-input's 16px iOS focus-zoom floor, beaten by input.css. The floor
|
|
12
|
+
* never applied on any touch device, in any release (20260815-011).
|
|
13
|
+
* - the forced-colors border below, beaten by button.css / badge.css /
|
|
14
|
+
* card.css (20260815-029).
|
|
15
|
+
* - the Popover fallback, which happens to win today only because nothing
|
|
16
|
+
* later declares `display` on those selectors — pinned by a test rather
|
|
17
|
+
* than by structure (20260815-013).
|
|
18
|
+
*
|
|
19
|
+
* So: gates whose whole job is to BEAT a component default live here, after
|
|
20
|
+
* everything they guard. Same reasoning the bundler already applies to
|
|
21
|
+
* utilities.css (role helpers must outrank component defaults) — see
|
|
22
|
+
* scripts/bundle-css.mjs.
|
|
23
|
+
*
|
|
24
|
+
* What does NOT belong here: a gate that only has to beat the UA or another
|
|
25
|
+
* rule in its own file. Keep those next to what they modify; this file is for
|
|
26
|
+
* the cross-file case, or it becomes a dumping ground and the cascade gets
|
|
27
|
+
* harder to read rather than easier.
|
|
28
|
+
*
|
|
29
|
+
* A build test (test/build.test.mjs) asserts the bundle contains no gated
|
|
30
|
+
* declaration that a later ungated rule overrides at equal-or-lower
|
|
31
|
+
* specificity. It asserts an EMPTY SET — no allowlist, because an allowlist
|
|
32
|
+
* is where violations go to be forgotten.
|
|
33
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
34
|
+
|
|
35
|
+
/* Windows High Contrast / forced-colors: opt into the system palette so
|
|
36
|
+
borders and focus stay visible when the user's palette replaces ours.
|
|
37
|
+
|
|
38
|
+
.juno-badge additionally sets `forced-color-adjust: none` to keep its status
|
|
39
|
+
fill meaningful — and that opt-out is why the badge was the one element where
|
|
40
|
+
the lost cascade actually reached the screen: it also disables the UA's own
|
|
41
|
+
repaint, which was silently rescuing .juno-btn and .juno-card. Measured, not
|
|
42
|
+
inferred (20260815-029). */
|
|
43
|
+
@media (forced-colors: active) {
|
|
44
|
+
*:focus-visible { outline-color: Highlight; }
|
|
45
|
+
|
|
46
|
+
.juno-badge,
|
|
47
|
+
.juno-btn,
|
|
48
|
+
.juno-card,
|
|
49
|
+
.juno-readout { border: 1px solid CanvasText; }
|
|
50
|
+
.juno-badge { forced-color-adjust: none; } /* keep status fill meaningful */
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M56.88,31.93A12,12,0,1,0,39.12,48.07L71.79,84A68,68,0,0,0,72,220h88a91.26,91.26,0,0,0,30.66-5.24l8.46,9.31a12,12,0,0,0,17.76-16.14ZM160,196H72a44,44,0,0,1-1.8-87.95A91.91,91.91,0,0,0,68,128a12,12,0,0,0,24,0,68.22,68.22,0,0,1,2.66-18.84l77.88,85.67A68.67,68.67,0,0,1,160,196Zm92-68a91.32,91.32,0,0,1-17.53,54,12,12,0,1,1-19.41-14.11,68,68,0,0,0-89.57-98.53,12,12,0,0,1-12.2-20.66A92,92,0,0,1,252,128Z"/></svg>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// junoui — icon sprite subsetter (build tooling for consumers)
|
|
3
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
4
|
+
// The sprite ships every icon junoui has (65 and growing). An app that
|
|
5
|
+
// inlines it — which is what Safari's flaky external-<use> rendering forces
|
|
6
|
+
// (see scripts/build-icons.mjs) — therefore ships every icon in its main
|
|
7
|
+
// bundle, not the ~20 it draws. This module is the missing half: a pure,
|
|
8
|
+
// dependency-free subsetter a consumer's build step (Vite/webpack plugin,
|
|
9
|
+
// npm script) runs over the shipped sprite.
|
|
10
|
+
//
|
|
11
|
+
// Node-only tooling, deliberately NOT part of the browser CSS/JS surface:
|
|
12
|
+
// subsetting at runtime would defeat the point.
|
|
13
|
+
//
|
|
14
|
+
// import { subsetSprite, spriteSymbolNames } from '@junoput01/junoui/subset';
|
|
15
|
+
// const svg = subsetSprite(readFileSync(spritePath, 'utf8'), ['gear', 'x']);
|
|
16
|
+
//
|
|
17
|
+
// Contract: names are bare icon names ('gear'), never prefixed ids. The
|
|
18
|
+
// result keeps the sprite's banner and wrapper attributes verbatim, so the
|
|
19
|
+
// license notice travels with every subset (the icons are MIT Phosphor and
|
|
20
|
+
// the notice is a condition of redistribution).
|
|
21
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
22
|
+
|
|
23
|
+
const SYMBOL_RE = /<symbol\s+id="juno-i-([^"]+)"[\s\S]*?<\/symbol>/g;
|
|
24
|
+
|
|
25
|
+
/** Every icon name the sprite defines, in document order. */
|
|
26
|
+
export function spriteSymbolNames(sprite) {
|
|
27
|
+
return [...sprite.matchAll(SYMBOL_RE)].map((m) => m[1]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The same sprite carrying only `names`.
|
|
32
|
+
*
|
|
33
|
+
* Throws on a name the sprite does not define: a silently dropped icon
|
|
34
|
+
* renders as an empty <svg> in the consuming app — visible to nobody until a
|
|
35
|
+
* user reports a blank space — so an unknown name is a build failure, not a
|
|
36
|
+
* warning. Unused names in the sprite are simply omitted; order follows the
|
|
37
|
+
* sprite, not the request, so the output is stable under a reordered list.
|
|
38
|
+
*/
|
|
39
|
+
export function subsetSprite(sprite, names) {
|
|
40
|
+
const wanted = new Set(names);
|
|
41
|
+
const have = new Set(spriteSymbolNames(sprite));
|
|
42
|
+
const missing = [...wanted].filter((n) => !have.has(n));
|
|
43
|
+
if (missing.length) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`subsetSprite: the sprite defines no icon named ${missing.map((n) => `'${n}'`).join(', ')} — ` +
|
|
46
|
+
`available: ${[...have].join(', ')}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const kept = [];
|
|
50
|
+
for (const m of sprite.matchAll(SYMBOL_RE)) if (wanted.has(m[1])) kept.push(m[0]);
|
|
51
|
+
// Keep everything outside the symbols verbatim (banner + <svg> open/close),
|
|
52
|
+
// so the license notice and the wrapper's fill/display attributes survive.
|
|
53
|
+
const head = sprite.slice(0, sprite.indexOf('<symbol'));
|
|
54
|
+
const tail = sprite.slice(sprite.lastIndexOf('</symbol>') + '</symbol>'.length);
|
|
55
|
+
return `${head}${kept.join('\n')}${tail}`;
|
|
56
|
+
}
|