@junoput01/junoui 0.2.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 +251 -0
- package/README.md +23 -19
- package/dist/css/juno-custom-media.css +32 -0
- package/dist/css/juno.css +1355 -40
- package/dist/icons/inline.js +22 -0
- package/docs/accessibility.md +188 -0
- package/docs/boot-shell.md +295 -0
- package/docs/components/README.md +71 -0
- package/docs/components/accordion.md +49 -0
- package/docs/components/alert.md +46 -0
- package/docs/components/avatar.md +45 -0
- package/docs/components/badge.md +35 -0
- package/docs/components/breadcrumb.md +31 -0
- package/docs/components/button.md +32 -0
- package/docs/components/card.md +41 -0
- package/docs/components/checkbox.md +41 -0
- package/docs/components/chip.md +44 -0
- package/docs/components/divider.md +32 -0
- package/docs/components/dock.md +215 -0
- package/docs/components/drawer.md +100 -0
- package/docs/components/field.md +44 -0
- package/docs/components/fold-slot.md +31 -0
- package/docs/components/gauge.md +48 -0
- package/docs/components/icon-loader.md +122 -0
- package/docs/components/icon.md +70 -0
- package/docs/components/input.md +45 -0
- package/docs/components/list.md +76 -0
- package/docs/components/load-state.md +131 -0
- package/docs/components/loader.md +88 -0
- package/docs/components/menu.md +54 -0
- package/docs/components/modal.md +52 -0
- package/docs/components/navbar.md +53 -0
- package/docs/components/pagination.md +42 -0
- package/docs/components/pillbar.md +211 -0
- package/docs/components/popover.md +43 -0
- package/docs/components/rail.md +54 -0
- package/docs/components/readout.md +39 -0
- package/docs/components/reload.md +41 -0
- package/docs/components/segmented.md +46 -0
- package/docs/components/select.md +33 -0
- package/docs/components/skeleton.md +45 -0
- package/docs/components/slider.md +48 -0
- package/docs/components/spark.md +51 -0
- package/docs/components/status.md +30 -0
- package/docs/components/stepper.md +46 -0
- package/docs/components/switch.md +38 -0
- package/docs/components/table.md +126 -0
- package/docs/components/tabs.md +61 -0
- package/docs/components/thumb.md +81 -0
- package/docs/components/toast.md +48 -0
- package/docs/components/toggle-button.md +40 -0
- package/docs/components/tooltip.md +63 -0
- package/docs/design-guidelines.md +173 -0
- package/docs/flutter.md +42 -0
- package/docs/getting-started.md +93 -0
- package/docs/integration.md +95 -0
- package/docs/ios-conformance.md +224 -0
- package/docs/layout.md +271 -0
- package/docs/native.md +52 -0
- package/docs/roadmap.md +106 -0
- package/docs/tokens-reference.md +223 -0
- package/docs/web.md +95 -0
- package/package.json +7 -3
- package/src/css/base.css +171 -1
- package/src/css/components/dock.css +335 -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 +67 -0
- 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 +222 -7
- package/src/css/components/rail.css +8 -0
- 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 +95 -5
- package/src/css/utilities.css +4 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Design guidelines
|
|
2
|
+
|
|
3
|
+
Buildable in any tool — these are rules, not code. Pair with exact values in
|
|
4
|
+
[tokens-reference.md](./tokens-reference.md).
|
|
5
|
+
|
|
6
|
+
## First principle: color encodes status, never decoration
|
|
7
|
+
|
|
8
|
+
Every hue has one assigned role. Never reuse a status color as styling.
|
|
9
|
+
|
|
10
|
+
| Role | Meaning | When |
|
|
11
|
+
| --------- | ----------------- | ----------------------------------------------------------------------- |
|
|
12
|
+
| `nominal` | Normal / OK | Nominal readings, confirmed-good, success. |
|
|
13
|
+
| `active` | Selected / active | Current selection, armed state, focus rings, live status. |
|
|
14
|
+
| `target` | Managed / goal | Computed set-points, automated targets. Use sparingly. |
|
|
15
|
+
| `caution` | Non-normal, watch | Approaching limits, degraded — no immediate action. |
|
|
16
|
+
| `warning` | Critical | Limit exceeded, act now. Reserve strictly — overuse destroys authority. |
|
|
17
|
+
|
|
18
|
+
Neutral / structural roles carry no status meaning:
|
|
19
|
+
|
|
20
|
+
| Role | Use |
|
|
21
|
+
| --------------- | ------------------------------------------------------------------ |
|
|
22
|
+
| `data` | Primary numeric readouts and live values — the brightest text. |
|
|
23
|
+
| `data-dim` | Faint metadata — timestamps, tick labels. Decorative; below muted. |
|
|
24
|
+
| `label` | Labels, units, helper and secondary text. Never for values. |
|
|
25
|
+
| `muted` | Disabled, placeholder, decorative separators. |
|
|
26
|
+
| `border` | Hairline borders and dividers. |
|
|
27
|
+
| `border-strong` | Divider / stronger rule — heavier than the hairline border. |
|
|
28
|
+
| `s0`–`s3` | Surface depth: base → panel → elevated card → selected/overlay. |
|
|
29
|
+
|
|
30
|
+
## More principles
|
|
31
|
+
|
|
32
|
+
- **Hierarchy via contrast, not scale.** Brightness/weight separate levels before size does.
|
|
33
|
+
- **Density is intentional.** Choose spacing deliberately; don't pad by accident.
|
|
34
|
+
Switch the whole UI between comfortable and compact with one attribute (below).
|
|
35
|
+
- **Designed for long sessions.** The `soft` palette and dark mode reduce fatigue.
|
|
36
|
+
|
|
37
|
+
## Typography
|
|
38
|
+
|
|
39
|
+
- **B612 Mono** — every number, timestamp, identifier, code. Fixed-width digits
|
|
40
|
+
stop value-change jitter. Use `tabular-nums`.
|
|
41
|
+
- **B612** — all non-numeric UI: headings, labels, navigation, buttons.
|
|
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
|
+
```
|
|
52
|
+
|
|
53
|
+
## Date & time
|
|
54
|
+
|
|
55
|
+
One fixed house format everywhere — no locale drift between screens. This is a
|
|
56
|
+
**content convention**, not code: apps do the formatting; junoui specifies what it
|
|
57
|
+
looks like.
|
|
58
|
+
|
|
59
|
+
| What | Format | Example |
|
|
60
|
+
| -------- | ------------------------- | -------------------- |
|
|
61
|
+
| Date | `dd.mm.yyyy`, zero-padded | `05.07.2026` |
|
|
62
|
+
| Time | 24-hour `HH:MM`(`:SS`) | `14:32` · `14:32:07` |
|
|
63
|
+
| Combined | date first, interpunct | `05.07.2026 · 14:32` |
|
|
64
|
+
| Range | en-dash, no spaces | `14:00–15:30` |
|
|
65
|
+
| Open | trailing en-dash | `14:00–` |
|
|
66
|
+
|
|
67
|
+
Rendering rules:
|
|
68
|
+
|
|
69
|
+
- Always mono + `tabular-nums` (the [Typography](#typography) value rule) — use
|
|
70
|
+
`.juno-value` or `.juno-mono`, wrapped in `<time>` with the machine-readable ISO
|
|
71
|
+
value: `<time class="juno-mono" datetime="2026-07-05T14:32">05.07.2026 · 14:32</time>`.
|
|
72
|
+
- Never AM/PM, never month names in data UI (prose may spell them out).
|
|
73
|
+
- Relative time ("2h ago") is app policy; when used, pair it with the absolute
|
|
74
|
+
timestamp in a `title` or adjacent text.
|
|
75
|
+
|
|
76
|
+
App-side recipe (the `de-DE` locale happens to produce exactly this format):
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
const fmt = new Intl.DateTimeFormat('de-DE', {
|
|
80
|
+
day: '2-digit',
|
|
81
|
+
month: '2-digit',
|
|
82
|
+
year: 'numeric',
|
|
83
|
+
hour: '2-digit',
|
|
84
|
+
minute: '2-digit',
|
|
85
|
+
hour12: false,
|
|
86
|
+
});
|
|
87
|
+
fmt.format(date).replace(', ', ' · '); // "05.07.2026 · 14:32"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This is the junoui house format. Locale-sensitive consumer products may override it
|
|
91
|
+
with `Intl` defaults — deviating is a per-app decision; log it like any other
|
|
92
|
+
extension (see [integration.md](./integration.md)).
|
|
93
|
+
|
|
94
|
+
## Foundation tokens (motion, layering, depth, opacity)
|
|
95
|
+
|
|
96
|
+
These scales exist so transitions, overlays, and surfaces stay consistent. junoui
|
|
97
|
+
ships the values; stateful behavior (open/close, positioning, focus traps) lives in
|
|
98
|
+
apps. Exact values: [tokens-reference.md](./tokens-reference.md).
|
|
99
|
+
|
|
100
|
+
- **Motion.** Durations `--juno-motion-duration-{instant,quick,base,deliberate}`
|
|
101
|
+
(80·140·200·300ms) and easings `--juno-motion-ease-{decel,accel,standard,spring}`.
|
|
102
|
+
`instant` for state feedback, `quick` for tooltip/menu, `base` for popover/drawer,
|
|
103
|
+
`deliberate` for modal/scrim. Enter with `decel`, exit with `accel`, reposition with
|
|
104
|
+
`standard`, toggle with `spring`. Always inside a `transition`/`animation`;
|
|
105
|
+
`prefers-reduced-motion` already collapses these in `base.css`.
|
|
106
|
+
- **Z-index.** `--juno-z-*` is the single layering source of truth — never invent raw
|
|
107
|
+
z-index. Layer stack: `surface` (0) < `raised` (100) < `anchored` (2000, menu ·
|
|
108
|
+
popover · tooltip) < `overlay` (4000, modal · drawer + scrim) < `alert` (5000, toast).
|
|
109
|
+
Large gaps leave room for app layers.
|
|
110
|
+
- **Elevation.** Depth is **border-first** by design; elevation = z-index + shadow,
|
|
111
|
+
paired. Reach for `--juno-shadow-{1,2,3}` only when a surface lifts off the glass —
|
|
112
|
+
`1` raised card, `2` anchored (menu/popover/tooltip), `3` overlay (modal/drawer).
|
|
113
|
+
- **Opacity.** `--juno-opacity-disabled` for inert controls, `--juno-opacity-muted`
|
|
114
|
+
for de-emphasised content, `--juno-opacity-scrim` for a modal/drawer backdrop.
|
|
115
|
+
|
|
116
|
+
## Density
|
|
117
|
+
|
|
118
|
+
One attribute on any ancestor swaps the internal padding of every component
|
|
119
|
+
underneath it — no per-component class:
|
|
120
|
+
|
|
121
|
+
```html
|
|
122
|
+
<body data-juno-density="compact">
|
|
123
|
+
<!-- comfortable (default) needs no attribute -->
|
|
124
|
+
</body>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Components read **semantic padding aliases** (`--juno-pad-control-*`,
|
|
128
|
+
`--juno-pad-surface-*`, `--juno-gap-control`) instead of raw `--juno-space-*`;
|
|
129
|
+
`data-juno-density` redefines that set. Density is deliberately **non-linear** —
|
|
130
|
+
compact removes more block (vertical) than inline padding, so text never crowds its
|
|
131
|
+
edges. Interactive controls keep their `min-height` (WCAG tap target); only padding
|
|
132
|
+
shrinks. New components should use the aliases for internal padding to inherit
|
|
133
|
+
density for free; add a new archetype only when one is genuinely needed.
|
|
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
|
+
|
|
152
|
+
## Accessibility
|
|
153
|
+
|
|
154
|
+
Accessibility is a core design goal, held to published standards — **WCAG 2.2** and
|
|
155
|
+
**WAI-ARIA** — with the exact success criteria cited in
|
|
156
|
+
[accessibility.md](./accessibility.md).
|
|
157
|
+
|
|
158
|
+
- The `colorblind` palette is the IBM Carbon universal set — distinguishable across
|
|
159
|
+
deuteranopia, protanopia, tritanopia, and AAA-contrast (≥ 7:1) on dark surfaces
|
|
160
|
+
(WCAG 1.4.6).
|
|
161
|
+
- **Never make color the sole signal** (WCAG 1.4.1). Always pair it with a text label,
|
|
162
|
+
icon, or shape.
|
|
163
|
+
- Interactive targets: `size.tap.min` = 24px (WCAG 2.5.8, AA); `size.tap.comfortable`
|
|
164
|
+
= 44px (WCAG 2.5.5, AAA) recommended for primary mobile actions.
|
|
165
|
+
- Focus is always visible — the `active` role, 2px outline (WCAG 2.4.7 / 2.4.11).
|
|
166
|
+
|
|
167
|
+
## Choosing a palette
|
|
168
|
+
|
|
169
|
+
| Palette | Use |
|
|
170
|
+
| ------------ | -------------------------------------------------- |
|
|
171
|
+
| `standard` | Default. Vivid, high-contrast OKLCH. |
|
|
172
|
+
| `colorblind` | Accessibility-critical or universal audiences. |
|
|
173
|
+
| `soft` | Long viewing sessions; lower chroma, less fatigue. |
|
package/docs/flutter.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Flutter
|
|
2
|
+
|
|
3
|
+
Colors are pre-converted to sRGB hex at build time (Dart has no `oklch`).
|
|
4
|
+
|
|
5
|
+
## Add the file
|
|
6
|
+
|
|
7
|
+
Copy the generated file into your project:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
dist/flutter/juno_tokens.dart → lib/theme/juno_tokens.dart
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Use it
|
|
14
|
+
|
|
15
|
+
```dart
|
|
16
|
+
import 'theme/juno_tokens.dart';
|
|
17
|
+
|
|
18
|
+
Container(
|
|
19
|
+
color: JunoTokens.standardDarkS1,
|
|
20
|
+
padding: const EdgeInsets.all(JunoTokens.space16),
|
|
21
|
+
child: Text(
|
|
22
|
+
'89.3',
|
|
23
|
+
style: TextStyle(
|
|
24
|
+
color: JunoTokens.standardDarkNominal,
|
|
25
|
+
fontSize: JunoTokens.fontSize38,
|
|
26
|
+
fontFamily: 'B612 Mono',
|
|
27
|
+
),
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- Colors are `Color` constants named `<palette><Mode><Role>` (camelCase),
|
|
33
|
+
fully opaque (`0xFF…`).
|
|
34
|
+
- Dimensions are `double` constants (`space16`, `fontSize14`, `radius8`, …).
|
|
35
|
+
|
|
36
|
+
## Theming
|
|
37
|
+
|
|
38
|
+
Map the active palette/mode to a helper that returns the right constant, or build a
|
|
39
|
+
`ThemeExtension` keyed on palette+mode. All constants derive from the same source,
|
|
40
|
+
so they stay in lockstep with web and native.
|
|
41
|
+
|
|
42
|
+
Exact values for every token: [tokens-reference.md](./tokens-reference.md).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
junoui is a design system for **modern, futuristic, highly readable and accessible**
|
|
4
|
+
UIs — usable for critical systems and everyday apps alike. Mechanically it is the
|
|
5
|
+
single graphics source for your UIs: colors, spacing, typography, radii, sizing. You
|
|
6
|
+
build the UI skeleton; junoui defines how it looks. Tokens are authored once in
|
|
7
|
+
`tokens/` and compiled to every platform.
|
|
8
|
+
|
|
9
|
+
Accessibility is a first-class goal, held to the international standards (WCAG 2.2 +
|
|
10
|
+
WAI-ARIA) with the exact criteria cited per component — see
|
|
11
|
+
[accessibility.md](./accessibility.md).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install @junoput01/junoui
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or as a git submodule (then build once):
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
git submodule add git@github.com:junoput/junoui.git vendor/junoui
|
|
23
|
+
cd vendor/junoui && npm install # `prepare` builds dist/
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`npm install` runs the `prepare` script, so `dist/` is always built for you.
|
|
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
|
+
|
|
51
|
+
## The model
|
|
52
|
+
|
|
53
|
+
- **Palette** — `standard` · `colorblind` · `soft`
|
|
54
|
+
- **Mode** — `dark` · `light`
|
|
55
|
+
- **Role** — `nominal` `active` `target` `caution` `warning` (semantic) and
|
|
56
|
+
`data` `label` `muted` `border` `s0`–`s3` (neutral/surface)
|
|
57
|
+
|
|
58
|
+
A theme is one palette × one mode. Color always encodes status — see
|
|
59
|
+
[design-guidelines.md](./design-guidelines.md).
|
|
60
|
+
|
|
61
|
+
## Pick your platform
|
|
62
|
+
|
|
63
|
+
| You're building in… | Read |
|
|
64
|
+
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
65
|
+
| Web (CSS, SCSS, JS/TS) | [web.md](./web.md) |
|
|
66
|
+
| Android / iOS | [native.md](./native.md) |
|
|
67
|
+
| Flutter | [flutter.md](./flutter.md) |
|
|
68
|
+
| Figma / Sketch / any tool without code | [tokens-reference.md](./tokens-reference.md) (exact values) + [design-guidelines.md](./design-guidelines.md) |
|
|
69
|
+
|
|
70
|
+
Responsive behavior (how blocks adapt on resize) is its own layer:
|
|
71
|
+
[layout.md](./layout.md).
|
|
72
|
+
|
|
73
|
+
## What ships
|
|
74
|
+
|
|
75
|
+
`npm install @junoput01/junoui` delivers `dist/` (all platform outputs), `src/css/` (authored
|
|
76
|
+
stylesheet sources), `src/icons/` (SVG icon sources + license), and `src/fonts/`
|
|
77
|
+
(self-hosted B612 woff2 + OFL license). The interactive demo in `showcase/` is **not**
|
|
78
|
+
shipped — it lives in the repo for development only.
|
|
79
|
+
|
|
80
|
+
## Build outputs
|
|
81
|
+
|
|
82
|
+
| Path | Platform |
|
|
83
|
+
| ----------------------------------------- | ------------------------------------------------------------- |
|
|
84
|
+
| `dist/css/juno.css` | Web — tokens + base + utilities + components |
|
|
85
|
+
| `dist/css/juno-tokens.css` | Web — CSS variables only |
|
|
86
|
+
| `dist/css/juno-fonts.css` + `dist/fonts/` | Web — opt-in self-hosted B612 (`@junoput01/junoui/fonts.css`) |
|
|
87
|
+
| `dist/icons/juno-icons.svg` | Web — icon sprite (`@junoput01/junoui/icons`) |
|
|
88
|
+
| `dist/scss/_juno-tokens.scss` | Web — SCSS variables |
|
|
89
|
+
| `dist/js/tokens.js` + `.d.ts` | JS / TS — `TOKENS`, `CORE`, `getTokens()` |
|
|
90
|
+
| `dist/json/tokens.json` | W3C DTCG — any tool |
|
|
91
|
+
| `dist/android/colors.xml` · `dimens.xml` | Android |
|
|
92
|
+
| `dist/ios/JunoTokens.swift` | iOS |
|
|
93
|
+
| `dist/flutter/juno_tokens.dart` | Flutter |
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Integrating junoui into an app
|
|
2
|
+
|
|
3
|
+
junoui is **presentational**: tokens + a CSS component layer, zero JS. It dresses a UI
|
|
4
|
+
your app builds. This is the recipe for consuming it cleanly — and the rules for
|
|
5
|
+
extending it without breaking the "one design across all apps" goal.
|
|
6
|
+
|
|
7
|
+
## 1. Import order (app shell)
|
|
8
|
+
|
|
9
|
+
`@junoput01/junoui/css` sets a small reset + document defaults on `:root`/`body`. Load it **first**,
|
|
10
|
+
your app styles **after**, so your shell wins where it must:
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import '@junoput01/junoui/css'; // tokens + base + components — FIRST
|
|
14
|
+
import '@junoput01/junoui/fonts.css'; // optional self-hosted B612 (see §2)
|
|
15
|
+
import './app.css'; // your shell + app-specific styles — AFTER
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
What junoui's base layer touches: `box-sizing`, margin/padding reset, `body`
|
|
19
|
+
font/color/background, link color, focus-visible ring, and `prefers-reduced-motion` /
|
|
20
|
+
`forced-colors` handling. It does **not** position or lay out your shell — that's yours.
|
|
21
|
+
If a body default fights your shell, override it in your app CSS (loaded after).
|
|
22
|
+
|
|
23
|
+
## 2. Fonts (opt-in, CSP-safe)
|
|
24
|
+
|
|
25
|
+
junoui never fetches fonts. Either bring your own B612, or opt into the self-hosted set:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import '@junoput01/junoui/fonts.css'; // local @font-face → woff2 in the package, no network
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Safe under a strict CSP (`font-src 'self'`). Skip it and the UI falls back to system
|
|
32
|
+
sans/mono until you provide the family.
|
|
33
|
+
|
|
34
|
+
## 3. Theming — two attributes
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<html data-juno-palette="standard" data-juno-mode="dark" data-juno-density="comfortable"></html>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `data-juno-palette`: `standard` · `colorblind` · `soft`
|
|
41
|
+
- `data-juno-mode`: `dark` · `light`
|
|
42
|
+
- `data-juno-density`: `comfortable` · `compact`
|
|
43
|
+
- `data-juno-text`: `base` · `large` · `xl` (type scale)
|
|
44
|
+
|
|
45
|
+
Flip these at runtime and the whole UI re-themes. Persist them if you want (localStorage);
|
|
46
|
+
the value is yours to manage.
|
|
47
|
+
|
|
48
|
+
## 4. Token bridge (aliasing app variables)
|
|
49
|
+
|
|
50
|
+
If your app already has design aliases, redefine them in terms of `--juno-*` so one system
|
|
51
|
+
drives everything:
|
|
52
|
+
|
|
53
|
+
```css
|
|
54
|
+
:root {
|
|
55
|
+
--bg: var(--juno-s0);
|
|
56
|
+
--ink: var(--juno-data);
|
|
57
|
+
--ink-dim: var(--juno-data-dim); /* timestamps, metadata */
|
|
58
|
+
--line: var(--juno-border);
|
|
59
|
+
--line-strong: var(--juno-border-strong); /* dividers */
|
|
60
|
+
--accent: var(--juno-active); /* see §5 — accent is semantic */
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Prefer a shipped token over `color-mix()`. junoui exposes the full neutral ramp
|
|
65
|
+
(`data` · `data-dim` · `label` · `muted`) and two border tones (`border` ·
|
|
66
|
+
`border-strong`), so most dense-UI needs don't require mixing.
|
|
67
|
+
|
|
68
|
+
## 5. Accent is semantic — not a brand hue
|
|
69
|
+
|
|
70
|
+
junoui has **no decorative brand-accent knob**, on purpose. Color encodes _status_, never
|
|
71
|
+
decoration: every hue is a role (`nominal` · `active` · `target` · `caution` · `warning`).
|
|
72
|
+
The "accent" is the `active` role, and it changes with the palette.
|
|
73
|
+
|
|
74
|
+
If you want a different accent, **switch palette** (`soft` reads calmer) or add your own
|
|
75
|
+
brand color as an app-local variable used only for non-semantic chrome (a logo, a splash).
|
|
76
|
+
Do not remap junoui's roles to a brand hue — that erases the shared meaning across apps.
|
|
77
|
+
|
|
78
|
+
## 6. Extending — where new things go
|
|
79
|
+
|
|
80
|
+
| Need | Where |
|
|
81
|
+
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
82
|
+
| New **status meaning**, reusable across apps | Propose a new role in junoui core (semver: additive = minor). Rare. |
|
|
83
|
+
| A full **alternate theme** (same role names, new values) | A new **palette** (like `standard`/`colorblind`/`soft`) — the sanctioned expansion slot. |
|
|
84
|
+
| App-specific **decorative** color | App-local, **namespaced** (`--myapp-*`), layered on top; alias to `--juno-*` where you can. Never redefine junoui roles. |
|
|
85
|
+
| A **component** junoui lacks (e.g. chat bubble) | Build it locally against the token contract until junoui (or a `junoui-<domain>` layer) ships it. |
|
|
86
|
+
|
|
87
|
+
Rule of thumb: extend **additively and namespaced**. If every app injects its own palette
|
|
88
|
+
into junoui, the single-design guarantee dies.
|
|
89
|
+
|
|
90
|
+
## 7. Stateful behavior stays in your app
|
|
91
|
+
|
|
92
|
+
junoui ships no JS. Focus traps, list virtualization, popover positioning, data — all
|
|
93
|
+
yours (or a future sibling `junoui-<framework>` package). junoui gives you the stable,
|
|
94
|
+
semantic DOM (real elements, BEM classes, ARIA hooks) to wire behavior + analytics onto;
|
|
95
|
+
see [accessibility.md](./accessibility.md) for the per-component ARIA contract.
|
|
@@ -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/).
|