@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +251 -0
  2. package/README.md +23 -19
  3. package/dist/css/juno-custom-media.css +32 -0
  4. package/dist/css/juno.css +1355 -40
  5. package/dist/icons/inline.js +22 -0
  6. package/docs/accessibility.md +188 -0
  7. package/docs/boot-shell.md +295 -0
  8. package/docs/components/README.md +71 -0
  9. package/docs/components/accordion.md +49 -0
  10. package/docs/components/alert.md +46 -0
  11. package/docs/components/avatar.md +45 -0
  12. package/docs/components/badge.md +35 -0
  13. package/docs/components/breadcrumb.md +31 -0
  14. package/docs/components/button.md +32 -0
  15. package/docs/components/card.md +41 -0
  16. package/docs/components/checkbox.md +41 -0
  17. package/docs/components/chip.md +44 -0
  18. package/docs/components/divider.md +32 -0
  19. package/docs/components/dock.md +215 -0
  20. package/docs/components/drawer.md +100 -0
  21. package/docs/components/field.md +44 -0
  22. package/docs/components/fold-slot.md +31 -0
  23. package/docs/components/gauge.md +48 -0
  24. package/docs/components/icon-loader.md +122 -0
  25. package/docs/components/icon.md +70 -0
  26. package/docs/components/input.md +45 -0
  27. package/docs/components/list.md +76 -0
  28. package/docs/components/load-state.md +131 -0
  29. package/docs/components/loader.md +88 -0
  30. package/docs/components/menu.md +54 -0
  31. package/docs/components/modal.md +52 -0
  32. package/docs/components/navbar.md +53 -0
  33. package/docs/components/pagination.md +42 -0
  34. package/docs/components/pillbar.md +211 -0
  35. package/docs/components/popover.md +43 -0
  36. package/docs/components/rail.md +54 -0
  37. package/docs/components/readout.md +39 -0
  38. package/docs/components/reload.md +41 -0
  39. package/docs/components/segmented.md +46 -0
  40. package/docs/components/select.md +33 -0
  41. package/docs/components/skeleton.md +45 -0
  42. package/docs/components/slider.md +48 -0
  43. package/docs/components/spark.md +51 -0
  44. package/docs/components/status.md +30 -0
  45. package/docs/components/stepper.md +46 -0
  46. package/docs/components/switch.md +38 -0
  47. package/docs/components/table.md +126 -0
  48. package/docs/components/tabs.md +61 -0
  49. package/docs/components/thumb.md +81 -0
  50. package/docs/components/toast.md +48 -0
  51. package/docs/components/toggle-button.md +40 -0
  52. package/docs/components/tooltip.md +63 -0
  53. package/docs/design-guidelines.md +173 -0
  54. package/docs/flutter.md +42 -0
  55. package/docs/getting-started.md +93 -0
  56. package/docs/integration.md +95 -0
  57. package/docs/ios-conformance.md +224 -0
  58. package/docs/layout.md +271 -0
  59. package/docs/native.md +52 -0
  60. package/docs/roadmap.md +106 -0
  61. package/docs/tokens-reference.md +223 -0
  62. package/docs/web.md +95 -0
  63. package/package.json +7 -3
  64. package/src/css/base.css +171 -1
  65. package/src/css/components/dock.css +335 -0
  66. package/src/css/components/drawer.css +40 -3
  67. package/src/css/components/fold-slot.css +44 -0
  68. package/src/css/components/icon-loader.css +67 -0
  69. package/src/css/components/icon.css +6 -4
  70. package/src/css/components/load-state.css +136 -0
  71. package/src/css/components/loader.css +6 -0
  72. package/src/css/components/menu.css +4 -0
  73. package/src/css/components/modal.css +24 -3
  74. package/src/css/components/navbar.css +5 -1
  75. package/src/css/components/pillbar.css +222 -7
  76. package/src/css/components/rail.css +8 -0
  77. package/src/css/components/reload.css +48 -0
  78. package/src/css/components/skeleton.css +41 -13
  79. package/src/css/components/tabs.css +5 -0
  80. package/src/css/components/thumb.css +63 -1
  81. package/src/css/components/toast.css +5 -1
  82. package/src/css/density.css +22 -0
  83. package/src/css/layout.css +95 -5
  84. 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. |
@@ -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/).