@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,224 @@
|
|
|
1
|
+
# iOS conformance
|
|
2
|
+
|
|
3
|
+
What junoui encodes for iOS, **with sources**, and — just as important — what it
|
|
4
|
+
deliberately does _not_ encode because no primary source supports it.
|
|
5
|
+
|
|
6
|
+
Most numbers the design community attributes to Apple are not in Apple's text.
|
|
7
|
+
This page exists so nobody re-derives folklore, and so nobody "fixes" a correct
|
|
8
|
+
value into a wrong one later.
|
|
9
|
+
|
|
10
|
+
> **Verifying anything here.** Apple's HIG is a JavaScript app: a plain `curl`
|
|
11
|
+
> returns an empty shell. Check the backing DocC JSON instead —
|
|
12
|
+
> `developer.apple.com/tutorials/data/design/human-interface-guidelines/<page>.json`.
|
|
13
|
+
> Also pin a Wayback snapshot: the HIG silently drops guidance (see the 44pt
|
|
14
|
+
> story below). State verified 2026-08-03.
|
|
15
|
+
|
|
16
|
+
## The units question, first
|
|
17
|
+
|
|
18
|
+
**One Apple point = one CSS pixel.** Apple's `pt` is a density-independent
|
|
19
|
+
point, not the CSS typographic point; `devicePixelRatio` absorbs @2x/@3x. So
|
|
20
|
+
Apple's 44pt is CSS `44px`.
|
|
21
|
+
|
|
22
|
+
Converting through the CSS unit (`1pt = 1/72in`, `1px = 1/96in` → 44pt =
|
|
23
|
+
58.67px) is **wrong**. If you ever see 58.67 in this codebase, it is a bug.
|
|
24
|
+
|
|
25
|
+
This identity holds only when the page ships
|
|
26
|
+
`<meta name="viewport" content="width=device-width, initial-scale=1">`. Without
|
|
27
|
+
it iOS Safari lays out at ~980px and every metric drifts.
|
|
28
|
+
|
|
29
|
+
- Source: [css-values-4](https://www.w3.org/TR/css-values-4/) for the CSS units,
|
|
30
|
+
[WebKit iPhone X guide](https://webkit.org/blog/7929/designing-websites-for-iphone-x/)
|
|
31
|
+
for the viewport model.
|
|
32
|
+
|
|
33
|
+
## Touch targets
|
|
34
|
+
|
|
35
|
+
| What | Value | Status |
|
|
36
|
+
| --------------------------------------------- | -------------------------------------------------------- | --------------------------- |
|
|
37
|
+
| WCAG 2.2 SC 2.5.8 Target Size (Minimum) | **24×24 CSS px** | **Hard requirement** for AA |
|
|
38
|
+
| WCAG 2.2 SC 2.5.5 Target Size (Enhanced) | **44×44 CSS px** | AAA (recommendation tier) |
|
|
39
|
+
| HIG Buttons — hit region | **44×44 pt** ("needs a hit region of at least 44×44 pt") | Recommendation |
|
|
40
|
+
| HIG Accessibility — control size (iOS/iPadOS) | **44×44 pt default, 28×28 pt minimum** | Recommendation |
|
|
41
|
+
| HIG Accessibility — padding between controls | **~12 pt** bezeled, **~24 pt** non-bezeled | Recommendation, hedged |
|
|
42
|
+
|
|
43
|
+
junoui encodes `--juno-size-tap-min` = 24px (the AA floor) and
|
|
44
|
+
`--juno-size-tap-comfortable` = 44px, and promotes the former to the latter
|
|
45
|
+
under `@media (pointer: coarse)` — see [accessibility.md](./accessibility.md).
|
|
46
|
+
|
|
47
|
+
- Sources: [WCAG 2.5.8](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html),
|
|
48
|
+
[WCAG 2.5.5](https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html),
|
|
49
|
+
[HIG Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility),
|
|
50
|
+
[HIG Buttons](https://developer.apple.com/design/human-interface-guidelines/buttons).
|
|
51
|
+
|
|
52
|
+
### Folklore, named
|
|
53
|
+
|
|
54
|
+
- **"Apple mandates a 44pt _minimum_ tap target — see HIG Layout."** That
|
|
55
|
+
sentence has been **deleted**. The current Layout page contains no tap-target
|
|
56
|
+
number at all (`"44x44"` and `"tappable"` both return zero hits); a 2023
|
|
57
|
+
Wayback snapshot of the same endpoint still has it. Every checklist citing
|
|
58
|
+
that URL cites a dead page. What survives is the Buttons wording above.
|
|
59
|
+
- **"44px because WCAG."** Wrong at AA — the AA floor is 24px. 44 is Apple's
|
|
60
|
+
number, which WCAG AAA happens to match. W3C never says it derived 44 from
|
|
61
|
+
Apple.
|
|
62
|
+
|
|
63
|
+
## Spacing
|
|
64
|
+
|
|
65
|
+
**Apple publishes no numeric spacing scale for iOS.** No 4pt or 8pt grid, no
|
|
66
|
+
named spacing steps, no layout-margin or gutter constants, no readable-width
|
|
67
|
+
number. Standard margins are delegated to system layout guides.
|
|
68
|
+
|
|
69
|
+
The only Apple-published numbers found anywhere:
|
|
70
|
+
|
|
71
|
+
- **~12 pt / ~24 pt** control padding (HIG Accessibility, hedged: "in general",
|
|
72
|
+
"about", "works well").
|
|
73
|
+
- **8 pt** — `UIView.directionalLayoutMargins` default. This is a **UIKit API
|
|
74
|
+
default, not an HIG design rule**; cite it as such.
|
|
75
|
+
|
|
76
|
+
So: **"Apple's 8pt grid" and "HIG standard margins are 16–20pt" are folklore.**
|
|
77
|
+
junoui's spacing scale is junoui's own; do not claim Apple provenance for it.
|
|
78
|
+
|
|
79
|
+
- Source: [HIG Layout](https://developer.apple.com/design/human-interface-guidelines/layout),
|
|
80
|
+
[UIView.directionalLayoutMargins](https://developer.apple.com/documentation/uikit/uiview/directionallayoutmargins).
|
|
81
|
+
|
|
82
|
+
## Safe areas
|
|
83
|
+
|
|
84
|
+
`env(safe-area-inset-top|right|bottom|left)` — four variables, iOS 11 (shipped
|
|
85
|
+
as `constant()` in 11.0, renamed `env()` in 11.2). There is no numeric constant
|
|
86
|
+
to hardcode; values are system-supplied and vary by device.
|
|
87
|
+
|
|
88
|
+
**They are inert unless the page opts in.** `viewport-fit` defaults to `auto`,
|
|
89
|
+
and WebKit reports every inset as `0` until you set `cover`. `contain` does
|
|
90
|
+
_not_ opt out — only `cover` does. junoui is a stylesheet and cannot set this
|
|
91
|
+
for you, so it is stated as a hard requirement in
|
|
92
|
+
[getting-started.md](./getting-started.md), every showcase page carries it, and
|
|
93
|
+
a build test enforces that.
|
|
94
|
+
|
|
95
|
+
- Sources: [WebKit iPhone X guide](https://webkit.org/blog/7929/designing-websites-for-iphone-x/),
|
|
96
|
+
[WebKit bug 272779](https://bugs.webkit.org/show_bug.cgi?id=272779),
|
|
97
|
+
[css-env-1](https://www.w3.org/TR/css-env-1/).
|
|
98
|
+
- Spec ownership, for citation hygiene: `env()` and `safe-area-inset-*` are
|
|
99
|
+
**css-env-1**; `viewport-fit` is **css-round-display-1**. CSS Viewport Module
|
|
100
|
+
L1 defines none of them — citing it is misattribution.
|
|
101
|
+
|
|
102
|
+
### `max()` vs addition — the rule
|
|
103
|
+
|
|
104
|
+
WebKit's documented pattern is `padding-left: max(12px, env(safe-area-inset-left))`
|
|
105
|
+
— "the default padding **or** the safe area inset, whichever is greater". (The
|
|
106
|
+
`12px` there is an arbitrary demo value, not an Apple metric.)
|
|
107
|
+
|
|
108
|
+
That pattern applies to **padding on a surface that reaches the screen edge**:
|
|
109
|
+
the inset's job is to push content clear of the cutout, so it _replaces_ your
|
|
110
|
+
baseline rather than stacking on it.
|
|
111
|
+
|
|
112
|
+
It does **not** apply to a **floating element positioned off the edge**, where
|
|
113
|
+
addition is correct:
|
|
114
|
+
|
|
115
|
+
```css
|
|
116
|
+
/* floating pill: sit 16px ABOVE the home-indicator region */
|
|
117
|
+
inset-block-end: calc(var(--juno-space-16) + env(safe-area-inset-bottom, 0px));
|
|
118
|
+
|
|
119
|
+
/* NOT max() — that would park the pill flush against the indicator */
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Nor to the **clearance tokens** (`--juno-dock-clearance`,
|
|
123
|
+
`--juno-pillbar-clearance`), where content must clear the control's height _and_
|
|
124
|
+
the inset beneath it — genuinely additive.
|
|
125
|
+
|
|
126
|
+
junoui's call sites were audited against this rule (2026-08-03) and the additive
|
|
127
|
+
ones are correct as written. Do not "fix" them to `max()` without re-reading
|
|
128
|
+
this section.
|
|
129
|
+
|
|
130
|
+
### The unit trap
|
|
131
|
+
|
|
132
|
+
Inside `calc()`, an env() fallback **must carry a unit**:
|
|
133
|
+
|
|
134
|
+
```css
|
|
135
|
+
/* WRONG — unitless 0 is a <number>, the sum is invalid, and the whole
|
|
136
|
+
DECLARATION is dropped (it does not evaluate to zero) */
|
|
137
|
+
padding-block-end: calc(var(--juno-space-12) + env(safe-area-inset-bottom, 0));
|
|
138
|
+
|
|
139
|
+
/* RIGHT */
|
|
140
|
+
padding-block-end: calc(var(--juno-space-12) + env(safe-area-inset-bottom, 0px));
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A bare (non-`calc`) value may keep a unitless `0`. A build test enforces this.
|
|
144
|
+
Note this follows from ordinary CSS type rules; no Apple/WebKit source states it
|
|
145
|
+
specifically. The adjacent _confirmed_ unit pitfall is that the
|
|
146
|
+
`@supports (padding: max(0px))` feature test needs a unit — and that `@supports`
|
|
147
|
+
wrapper is itself obsolete boilerplate in 2026.
|
|
148
|
+
|
|
149
|
+
## Viewport units
|
|
150
|
+
|
|
151
|
+
CSS defines three families by how dynamic browser chrome is treated:
|
|
152
|
+
`sv*` (chrome assumed **expanded** — smallest), `lv*` (assumed **retracted** —
|
|
153
|
+
largest), `dv*` (tracked live). **The unprefixed `vh`/`vw` are normatively equal
|
|
154
|
+
to `lv*`** — that is the spec-level cause of the classic `100vh` overflow: a
|
|
155
|
+
`100vh` box is sized as if the toolbar were retracted.
|
|
156
|
+
|
|
157
|
+
junoui uses **zero raw `vh`**. Full-height surfaces use `dvh`
|
|
158
|
+
(`layout.css`, `drawer.css`) and `85dvh` caps the bottom sheet.
|
|
159
|
+
|
|
160
|
+
Caveats worth knowing before changing any of that:
|
|
161
|
+
|
|
162
|
+
- `dv*` is explicitly **not stable** and not guaranteed to update every frame,
|
|
163
|
+
so it can churn while the address bar collapses. `sv*` is the calm choice when
|
|
164
|
+
a surface must never overflow.
|
|
165
|
+
- iOS shipped viewport-unit bugs into the **iOS 26** era: Safari 26.0 fixed
|
|
166
|
+
`lvh`/`vh` being sized against the _small_ viewport in `SFSafariViewController`.
|
|
167
|
+
The underlying WebKit bug (255708, filed 2023) is **still open**, so Apple's
|
|
168
|
+
"Fixed" is stronger than the tracker supports. Scope is the in-app browser used
|
|
169
|
+
by Slack/X — not standalone Safari, and not `WKWebView`-based in-app browsers
|
|
170
|
+
(Instagram/Facebook), a separate unfixed path.
|
|
171
|
+
- Do **not** reach for `env(safe-area-max-inset-*)` as a fix. It is in css-env-1
|
|
172
|
+
but could not be confirmed shipping in any engine.
|
|
173
|
+
|
|
174
|
+
- Sources: [css-values-4 §6.1.2.1](https://www.w3.org/TR/css-values-4/),
|
|
175
|
+
[csswg-drafts#6454](https://github.com/w3c/csswg-drafts/issues/6454),
|
|
176
|
+
[Safari 26.0 release notes](https://developer.apple.com/documentation/safari-release-notes/safari-26-release-notes),
|
|
177
|
+
[WebKit bug 255708](https://bugs.webkit.org/show_bug.cgi?id=255708).
|
|
178
|
+
|
|
179
|
+
## Typography
|
|
180
|
+
|
|
181
|
+
iOS: **17 pt default body size, 11 pt minimum**; Dynamic Type must accommodate
|
|
182
|
+
enlargement to **200%**. Contrast minimums are 4.5:1 up to 17pt, 3:1 at 18pt or
|
|
183
|
+
bold. Since 1pt = 1 CSS px, that is 17px / 11px.
|
|
184
|
+
|
|
185
|
+
Confidence: medium — extracted from the HIG Accessibility page but not
|
|
186
|
+
adversarially re-verified. Re-check before encoding as a hard constraint.
|
|
187
|
+
|
|
188
|
+
### The 16px input rule
|
|
189
|
+
|
|
190
|
+
junoui holds text-entry controls at a 16px floor on coarse pointers, because
|
|
191
|
+
iOS Safari is widely observed to zoom the page onto a focused field under 16px.
|
|
192
|
+
|
|
193
|
+
**No primary WebKit or Apple source for this was found.** It is empirical
|
|
194
|
+
behavior, not published spec. The mitigation is harmless, so it stays — but do
|
|
195
|
+
not cite it as documented, and re-verify it on iOS 26.
|
|
196
|
+
|
|
197
|
+
## Things a stylesheet controls that Apple says nothing about
|
|
198
|
+
|
|
199
|
+
No primary Apple source was found for any of: `touch-action`,
|
|
200
|
+
`-webkit-tap-highlight-color`, `overscroll-behavior`, momentum scrolling, or
|
|
201
|
+
`scroll-snap` on iOS. Where junoui uses these (the coarse-pointer tap-highlight
|
|
202
|
+
reset, `overscroll-behavior: contain` on the modal body), it is **community
|
|
203
|
+
convention** — sensible, but do not attribute it to Apple.
|
|
204
|
+
|
|
205
|
+
`-webkit-overflow-scrolling: touch` is legacy and must not be reintroduced.
|
|
206
|
+
|
|
207
|
+
## Open risk: iOS 26
|
|
208
|
+
|
|
209
|
+
The claim "iOS 26 changed nothing about safe areas, viewport-fit, touch
|
|
210
|
+
behavior, or focus zoom" was **refuted** during verification, so junoui's iOS 26
|
|
211
|
+
behavior is unverified rather than confirmed-safe.
|
|
212
|
+
|
|
213
|
+
One confirmed change raises the stakes: as of iOS/iPadOS 26, **every website
|
|
214
|
+
added to the Home Screen opens as a web app by default** — "there are now zero
|
|
215
|
+
requirements for 'installability'". junoui's CSS may therefore run in a
|
|
216
|
+
standalone context, where `viewport-fit` and `env()` govern home-indicator and
|
|
217
|
+
Dynamic Island clearance, for sites that never opted in.
|
|
218
|
+
|
|
219
|
+
Unconfirmed leads, tracked in ticket 20260803-034: `vh` reportedly pinning to
|
|
220
|
+
`window.outerHeight`; three new tab modes yielding different `innerHeight`; a
|
|
221
|
+
reported iPadOS 26 windowed-mode bug where `env(safe-area-inset-*)` returns
|
|
222
|
+
nothing. Resolving these needs a physical device, not more documentation.
|
|
223
|
+
|
|
224
|
+
- Source: [WebKit features in Safari 26.0](https://webkit.org/blog/17333/webkit-features-in-safari-26-0/).
|
package/docs/layout.md
CHANGED
|
@@ -36,6 +36,7 @@ default — override per instance inline.
|
|
|
36
36
|
| `.juno-grid-auto--tiles` | Media wall wired to the density layer | `--juno-tile-min` / `--juno-gap-content` |
|
|
37
37
|
| `.juno-sidebar` | Aside + fluid content, stacks when tight | `--juno-sidebar-width` (`280px`) |
|
|
38
38
|
| `.juno-switcher` | N-up or all-stacked at a threshold | `--juno-switcher-threshold` (`bp-sm`) |
|
|
39
|
+
| `.juno-scroller` | Bare scroll container (axis/snap/bar) | `--juno-scroller-snap` (`none`) |
|
|
39
40
|
| `.juno-reel` | Horizontal scroll-snap row | `--juno-reel-space` (`space-12`) |
|
|
40
41
|
|
|
41
42
|
### Examples
|
|
@@ -75,6 +76,73 @@ default — override per instance inline.
|
|
|
75
76
|
`data-juno-density` attribute re-densifies controls, surfaces **and** content
|
|
76
77
|
grids together.
|
|
77
78
|
|
|
79
|
+
## Scroller
|
|
80
|
+
|
|
81
|
+
Every scrolling region in the library — `.juno-reel`, `.juno-app-shell__main`,
|
|
82
|
+
tab strips — is the same three knobs: overflow axis, overscroll containment,
|
|
83
|
+
snap type. `.juno-scroller` ships them as overridable custom props instead of
|
|
84
|
+
each consumer re-deriving (and usually forgetting `overscroll-behavior`, which
|
|
85
|
+
lets iOS pull-to-refresh/scroll-chain through an inner scroller into the
|
|
86
|
+
page). Defaults: `overflow: auto` (both axes), `overscroll-behavior: contain`,
|
|
87
|
+
no snap.
|
|
88
|
+
|
|
89
|
+
```html
|
|
90
|
+
<!-- vertical list scroller with containment, no snap -->
|
|
91
|
+
<div class="juno-scroller juno-scroller--y">…</div>
|
|
92
|
+
|
|
93
|
+
<!-- horizontal, scrollbar hidden, softer "proximity" snap, opt-in stops -->
|
|
94
|
+
<div
|
|
95
|
+
class="juno-scroller juno-scroller--x juno-scroller--bare"
|
|
96
|
+
style="--juno-scroller-snap: x proximity;"
|
|
97
|
+
>
|
|
98
|
+
<div class="juno-snap">…</div>
|
|
99
|
+
<div class="juno-snap">…</div>
|
|
100
|
+
</div>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- `.juno-scroller--x` — horizontal axis only (`overflow: auto hidden`)
|
|
104
|
+
- `.juno-scroller--y` — vertical axis only (`overflow: hidden auto`)
|
|
105
|
+
- `.juno-scroller--bare` — hides the scrollbar (Firefox + WebKit)
|
|
106
|
+
- `.juno-snap` — on a child: opts into `scroll-snap-align`
|
|
107
|
+
|
|
108
|
+
`.juno-reel` is `.juno-scroller`'s horizontal-snap preset baked into one
|
|
109
|
+
class: it now reads its `scroll-snap-type` from the same `--juno-scroller-snap`
|
|
110
|
+
prop (default unchanged: `inline mandatory`), so the mode is overridable
|
|
111
|
+
per instance instead of hardcoded — e.g. a "magnet" strip that wants
|
|
112
|
+
`proximity` instead of a stepped `mandatory` feel:
|
|
113
|
+
|
|
114
|
+
```html
|
|
115
|
+
<div class="juno-reel" style="--juno-scroller-snap: inline proximity;">…</div>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Gesture surfaces
|
|
119
|
+
|
|
120
|
+
For an element whose pointer events an app JS layer owns outright — drag-pan,
|
|
121
|
+
pinch-zoom, swipe classification, anything that is a state machine rather than
|
|
122
|
+
native scrolling — junoui ships the CSS side of that contract as one class.
|
|
123
|
+
The gesture handler itself is the app's job (or a sibling `junoui-<framework>`
|
|
124
|
+
package): junoui declares the surface, never the logic.
|
|
125
|
+
|
|
126
|
+
```html
|
|
127
|
+
<div class="juno-gesture-surface" id="viewport"><!-- JS drag/pinch handlers attach here --></div>
|
|
128
|
+
|
|
129
|
+
<!-- only needs the touch-action axis lock, not the full reset -->
|
|
130
|
+
<li class="juno-list__item juno-pan-x"><!-- swipe-to-reveal row --></li>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`.juno-gesture-surface` sets `touch-action: var(--juno-touch-action, none)`,
|
|
134
|
+
`-webkit-touch-callout: none`, `user-select: none` and
|
|
135
|
+
`-webkit-tap-highlight-color: transparent` — so the UA never fights the
|
|
136
|
+
handler with its own scroll/zoom recognition, long-press callout, text
|
|
137
|
+
selection or tap flash. Override `--juno-touch-action` per instance to hand
|
|
138
|
+
back one axis (`pan-x` / `pan-y`) instead of all of them. `.juno-pan-x` /
|
|
139
|
+
`.juno-pan-y` are standalone single-axis classes for elements that want the
|
|
140
|
+
axis lock alone.
|
|
141
|
+
|
|
142
|
+
These four properties are **community convention, not Apple-documented
|
|
143
|
+
behavior** — see [ios-conformance.md](./ios-conformance.md) before citing them
|
|
144
|
+
as a platform requirement.
|
|
145
|
+
|
|
78
146
|
## App shell
|
|
79
147
|
|
|
80
148
|
Every product app assembles the same frame; `.juno-app-shell` ships it as
|
|
@@ -117,7 +185,9 @@ What the primitive encodes so you don't have to:
|
|
|
117
185
|
short-page pitfall of sticky nav bars (below) can't happen here.
|
|
118
186
|
- **Safe-area insets** — the shell pads for landscape notches
|
|
119
187
|
(`inset-left`/`right`), the topbar for `inset-top`, the dock for
|
|
120
|
-
`inset-bottom`.
|
|
188
|
+
`inset-bottom`. **This requires `viewport-fit=cover` in your page's viewport
|
|
189
|
+
meta** ([getting-started](./getting-started.md#required-the-viewport-meta)) —
|
|
190
|
+
without it iOS reports every inset as `0` and none of this padding happens.
|
|
121
191
|
|
|
122
192
|
Collapse the rail by toggling `.juno-rail--collapsed` (one class; the width
|
|
123
193
|
transition and label hiding are built in). Trays/detail panels: the
|
package/docs/web.md
CHANGED
|
@@ -87,3 +87,9 @@ import '@junoput01/junoui/fonts.css'; // opt-in: self-hosted B612 woff2, no netw
|
|
|
87
87
|
|
|
88
88
|
…or bring your own B612 (e.g. `@fontsource/b612`). Without either, the UI falls back to
|
|
89
89
|
system sans/mono. See [integration.md](./integration.md#2-fonts-opt-in-csp-safe).
|
|
90
|
+
|
|
91
|
+
## Boot performance
|
|
92
|
+
|
|
93
|
+
An app that wants chrome on screen at the first HTML parse — before `juno.css`
|
|
94
|
+
or any bundle arrives — and everything else loaded in the background follows
|
|
95
|
+
the boot-shell ladder: [boot-shell.md](./boot-shell.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junoput01/junoui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "junoui — a token-driven design system. Color carries semantic meaning, never decoration: every hue has one assigned role (NOMINAL / ACTIVE / TARGET / CAUTION / WARNING). Ships multi-platform tokens (CSS, SCSS, JS/TS, JSON, Android, iOS, Flutter) plus a framework-agnostic CSS component layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"module": "./dist/js/tokens.js",
|
|
30
30
|
"types": "./dist/js/tokens.d.ts",
|
|
31
31
|
"sideEffects": [
|
|
32
|
-
"**/*.css"
|
|
32
|
+
"**/*.css",
|
|
33
|
+
"./dist/icons/inline.js"
|
|
33
34
|
],
|
|
34
35
|
"exports": {
|
|
35
36
|
".": {
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
},
|
|
40
41
|
"./css": "./dist/css/juno.css",
|
|
41
42
|
"./css/tokens": "./dist/css/juno-tokens.css",
|
|
43
|
+
"./css/custom-media": "./dist/css/juno-custom-media.css",
|
|
42
44
|
"./fonts.css": "./dist/css/juno-fonts.css",
|
|
43
45
|
"./scss": "./dist/scss/_juno-tokens.scss",
|
|
44
46
|
"./tokens.json": "./dist/json/tokens.json",
|
|
@@ -47,6 +49,7 @@
|
|
|
47
49
|
"./ios": "./dist/ios/JunoTokens.swift",
|
|
48
50
|
"./flutter": "./dist/flutter/juno_tokens.dart",
|
|
49
51
|
"./icons": "./dist/icons/juno-icons.svg",
|
|
52
|
+
"./icons/inline": "./dist/icons/inline.js",
|
|
50
53
|
"./package.json": "./package.json"
|
|
51
54
|
},
|
|
52
55
|
"files": [
|
package/src/css/base.css
CHANGED
|
@@ -13,7 +13,37 @@
|
|
|
13
13
|
/* System color scheme. Without data-juno-mode the theme follows the OS
|
|
14
14
|
(`prefers-color-scheme`; see juno-tokens.css) — `dark light` here lets the
|
|
15
15
|
UA pick matching scrollbars/form chrome. An explicit mode pins both. */
|
|
16
|
-
:root {
|
|
16
|
+
:root {
|
|
17
|
+
color-scheme: dark light;
|
|
18
|
+
|
|
19
|
+
/* Floating-nav scroll clearance — a page/scroller that a fixed dock or
|
|
20
|
+
pillbar floats over reserves this much room at its foot so the last row
|
|
21
|
+
clears the overlay. Consumers write `padding-block-end:
|
|
22
|
+
var(--juno-dock-clearance)` and stay correct when the dock geometry
|
|
23
|
+
changes — no hand-rolled calc() per app. Web-only (they carry env()), so
|
|
24
|
+
they live in the CSS layer, not the cross-platform token set.
|
|
25
|
+
--juno-dock-clearance ≈ pill height + bottom margin + breathing room;
|
|
26
|
+
--juno-pillbar-clearance suits the shorter centered pill. */
|
|
27
|
+
--juno-dock-clearance: calc(var(--juno-space-72) + var(--juno-space-20) + env(safe-area-inset-bottom, 0px));
|
|
28
|
+
--juno-pillbar-clearance: calc(var(--juno-space-72) + env(safe-area-inset-bottom, 0px));
|
|
29
|
+
|
|
30
|
+
/* JS-readable motion contract. CSS-only `prefers-reduced-motion` never reaches
|
|
31
|
+
imperative JS (scrollTo/scrollIntoView smooth behavior, rAF-driven transforms,
|
|
32
|
+
an "animate or not" branch) — so the preference is also exposed as a custom
|
|
33
|
+
property script can read with one getComputedStyle call, no matchMedia
|
|
34
|
+
listener required for a one-shot check:
|
|
35
|
+
getComputedStyle(document.documentElement)
|
|
36
|
+
.getPropertyValue('--juno-motion').trim() !== 'none'
|
|
37
|
+
A component MAY author its durations through the scale rather than a raw
|
|
38
|
+
duration token — `calc(var(--juno-motion-duration-base) * var(--juno-motion-scale))`
|
|
39
|
+
— so the reduced-motion override below collapses them to 0 without its own
|
|
40
|
+
per-component media query. Web-only DOM contract, so it lives
|
|
41
|
+
here rather than in the cross-platform token set (see --juno-dock-clearance
|
|
42
|
+
above for the same reasoning). */
|
|
43
|
+
--juno-motion: auto;
|
|
44
|
+
--juno-motion-scale: 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
17
47
|
:root[data-juno-mode='dark'] { color-scheme: dark; }
|
|
18
48
|
|
|
19
49
|
/* Control surfaces. On dark panels the hairline border (~23% L) disappears, so
|
|
@@ -90,17 +120,102 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
90
120
|
@media (pointer: coarse) {
|
|
91
121
|
:root { --juno-size-tap-min: var(--juno-size-tap-comfortable); }
|
|
92
122
|
|
|
93
|
-
/* iOS Safari
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
123
|
+
/* iOS Safari is widely observed to zoom the page onto a focused text field
|
|
124
|
+
whose font-size is under 16px. NOTE: no primary WebKit/Apple source states
|
|
125
|
+
this — it is empirical behavior, not published spec, so don't cite it as
|
|
126
|
+
one (see docs/ios-conformance.md and ticket 20260803-032). The mitigation
|
|
127
|
+
is harmless either way: hold text-entry controls (.juno-input covers input,
|
|
128
|
+
textarea and the select's inner control) at a 16px floor on touch. max()
|
|
129
|
+
keeps the floor even under a scaled-down --juno-font-scale, and still grows
|
|
130
|
+
when scaled up. */
|
|
98
131
|
.juno-input { font-size: max(16px, var(--juno-font-size-16)); }
|
|
132
|
+
|
|
133
|
+
/* Kill the UA tap-highlight square on the interactive surfaces so it never
|
|
134
|
+
flashes past a rounded control on tap. Consumers were adding this by hand
|
|
135
|
+
per component; make it a first-class touch default. See 20260802-020. */
|
|
136
|
+
:where(
|
|
137
|
+
.juno-btn,
|
|
138
|
+
.juno-dock__item,
|
|
139
|
+
.juno-pillbar__item,
|
|
140
|
+
.juno-tabs__tab,
|
|
141
|
+
.juno-list__item,
|
|
142
|
+
.juno-menu__item
|
|
143
|
+
) {
|
|
144
|
+
-webkit-tap-highlight-color: transparent;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Tappable primitives opt out of double-tap-to-zoom. A browser that still
|
|
149
|
+
recognises that gesture has to WAIT after the first tap to see whether a
|
|
150
|
+
second one is coming, which reads as a late, mushy tap on exactly the
|
|
151
|
+
surfaces a phone UI is built from. `manipulation` keeps panning and
|
|
152
|
+
pinch-zoom (so the page stays zoomable — never `none` here, that would be an
|
|
153
|
+
a11y regression) and drops only the double-tap.
|
|
154
|
+
NOT inside the pointer:coarse block above: a hybrid device (touch laptop,
|
|
155
|
+
iPad with a trackpad) reports a fine primary pointer while still taking
|
|
156
|
+
touch input, and the property is inert on a mouse anyway.
|
|
157
|
+
Community convention — no primary Apple/WebKit source names it; see
|
|
158
|
+
docs/ios-conformance.md. Named components only, so a consumer's own elements
|
|
159
|
+
are untouched. See 20260803-038. */
|
|
160
|
+
:where(
|
|
161
|
+
.juno-btn,
|
|
162
|
+
.juno-dock__item,
|
|
163
|
+
.juno-pillbar__item,
|
|
164
|
+
.juno-pillbar__overflow,
|
|
165
|
+
.juno-tabs__tab,
|
|
166
|
+
.juno-list__item,
|
|
167
|
+
.juno-menu__item,
|
|
168
|
+
.juno-seg__option,
|
|
169
|
+
.juno-chip,
|
|
170
|
+
.juno-toggle-btn
|
|
171
|
+
) {
|
|
172
|
+
touch-action: manipulation;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* Gesture-owned surfaces — for an element whose pointer events are fully
|
|
176
|
+
driven by app JS (drag-pan, pinch-zoom, swipe classification: a state
|
|
177
|
+
machine, not a native scroller). The UA must get out of the way: no
|
|
178
|
+
scroll/zoom gesture recognition, no callout menu on long-press, no text
|
|
179
|
+
selection, no tap-highlight flash. Community convention — no primary
|
|
180
|
+
Apple/WebKit source names any of these four properties for iOS, see
|
|
181
|
+
docs/ios-conformance.md; do not attribute them to Apple. Opt-in utility
|
|
182
|
+
class (unlike the pointer:coarse reset above, which targets junoui's own
|
|
183
|
+
named components), so it isn't gated behind a pointer-type query — apply
|
|
184
|
+
it only to elements a gesture handler actually owns.
|
|
185
|
+
--juno-touch-action narrows the axis without overriding the rest of the
|
|
186
|
+
block; default `none` hands every axis to JS. */
|
|
187
|
+
.juno-gesture-surface {
|
|
188
|
+
touch-action: var(--juno-touch-action, none);
|
|
189
|
+
-webkit-touch-callout: none;
|
|
190
|
+
user-select: none;
|
|
191
|
+
-webkit-tap-highlight-color: transparent;
|
|
99
192
|
}
|
|
100
193
|
|
|
194
|
+
/* Single-axis escape hatches: the UA keeps native scrolling on one axis
|
|
195
|
+
while JS reads gestures on the other (e.g. a vertical list row that also
|
|
196
|
+
supports horizontal swipe-to-reveal). Equivalent to
|
|
197
|
+
`--juno-touch-action: pan-x|pan-y` on .juno-gesture-surface, offered as
|
|
198
|
+
standalone classes for elements that only need the axis lock and none of
|
|
199
|
+
the callout/selection/tap-highlight resets. */
|
|
200
|
+
.juno-pan-x { touch-action: pan-x; }
|
|
201
|
+
.juno-pan-y { touch-action: pan-y; }
|
|
202
|
+
|
|
101
203
|
@keyframes juno-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
|
102
204
|
|
|
205
|
+
/* Softer opacity pulse — a "working / refreshing" cadence that dims rather
|
|
206
|
+
than fully blinks out (used by the reload indicator and any live dot that
|
|
207
|
+
wants a gentler beat than juno-blink). */
|
|
208
|
+
@keyframes juno-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
|
|
209
|
+
|
|
103
210
|
@media (prefers-reduced-motion: reduce) {
|
|
211
|
+
/* Flip the JS-readable contract declared at the top of this file. Without
|
|
212
|
+
this the property would always report `auto` and script could never see
|
|
213
|
+
the preference — the whole point of exposing it. */
|
|
214
|
+
:root {
|
|
215
|
+
--juno-motion: none;
|
|
216
|
+
--juno-motion-scale: 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
104
219
|
*, *::before, *::after {
|
|
105
220
|
animation-duration: 0.01ms !important;
|
|
106
221
|
animation-iteration-count: 1 !important;
|
|
@@ -125,3 +240,51 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
125
240
|
.juno-readout { border: 1px solid CanvasText; }
|
|
126
241
|
.juno-badge { forced-color-adjust: none; } /* keep status fill meaningful */
|
|
127
242
|
}
|
|
243
|
+
|
|
244
|
+
/* ── iOS standalone letterbox unlock ─────────────────────────────────────────
|
|
245
|
+
iOS WebKit (observed through 26.6, iPhone 16 Pro, iOS 18.7) sizes a
|
|
246
|
+
Home-Screen standalone window whose DOCUMENT cannot scroll as if a
|
|
247
|
+
retractable browser toolbar existed: `screen.height − status bar`, pinned to
|
|
248
|
+
the top. The reserved strip — exactly env(safe-area-inset-top) — surfaces at
|
|
249
|
+
the BOTTOM of the glass, outside the window, painting black on every screen.
|
|
250
|
+
The same install with a scrollable document gets the whole screen.
|
|
251
|
+
|
|
252
|
+
Established by measurement, not inference (nexora 20260812-006): 201 device
|
|
253
|
+
readings; a four-structure A/B on one install where doc-scroll = full screen
|
|
254
|
+
and fixed-shell = letterboxed on every cold launch; and this exact mechanism
|
|
255
|
+
— the shell kept, the document left scrollable behind it by an invisible
|
|
256
|
+
in-flow spacer — verified full-screen on the device before landing here.
|
|
257
|
+
Transient scrollability is NOT enough (seven timed interventions, five
|
|
258
|
+
controlled runs, all negative): iOS samples the resting structure.
|
|
259
|
+
|
|
260
|
+
So: keep the document scrollable behind the app. The spacer is body::after
|
|
261
|
+
(a consumer that needs body::after for itself overrides this at the same
|
|
262
|
+
gate — none of ours do), invisible, 1px wide, and taller than the LARGE
|
|
263
|
+
viewport so the document always overflows whatever window iOS grants.
|
|
264
|
+
`overscroll-behavior: none` keeps the ghost scroller from rubber-banding;
|
|
265
|
+
apps put `overscroll-behavior: contain` on their real scrollers so an
|
|
266
|
+
inner fling never chains into it.
|
|
267
|
+
|
|
268
|
+
Gate, all three required:
|
|
269
|
+
display-mode: standalone only installed apps are letterboxed
|
|
270
|
+
pointer: coarse touch devices — keeps macOS Dock apps out
|
|
271
|
+
-webkit-touch-callout support iOS/iPadOS WebKit only
|
|
272
|
+
Selectors carry `html:root` (0,1,2) on purpose: app resets commonly declare
|
|
273
|
+
`body { overflow: hidden }` at (0,0,1) AFTER this sheet, and the unlock must
|
|
274
|
+
win the cascade without !important. */
|
|
275
|
+
@media (display-mode: standalone) and (pointer: coarse) {
|
|
276
|
+
@supports (-webkit-touch-callout: none) {
|
|
277
|
+
html:root,
|
|
278
|
+
html:root body { overflow: visible; }
|
|
279
|
+
html:root body { overscroll-behavior: none; }
|
|
280
|
+
|
|
281
|
+
html:root body::after {
|
|
282
|
+
content: '';
|
|
283
|
+
display: block;
|
|
284
|
+
width: 1px;
|
|
285
|
+
height: calc(100lvh + 80px);
|
|
286
|
+
visibility: hidden;
|
|
287
|
+
pointer-events: none;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|