@junoput01/junoui 0.7.0 → 0.8.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 +105 -0
- package/dist/android/dimens.xml +2 -0
- package/dist/classes.json +161 -1
- package/dist/css/juno-tokens.css +10 -0
- package/dist/css/juno.css +861 -39
- package/dist/flutter/juno_tokens.dart +10 -0
- package/dist/ios/JunoTokens.swift +10 -0
- package/dist/js/tokens.js +16 -0
- package/dist/json/tokens.json +51 -0
- package/dist/rust/juno_tokens.rs +383 -0
- package/dist/scss/_juno-tokens.scss +10 -0
- package/docs/components/canvas-ink.md +71 -0
- package/docs/components/gizmo.md +114 -0
- package/docs/components/swatch.md +95 -0
- package/docs/components/tree.md +112 -0
- package/docs/conformance-kit.md +26 -14
- package/docs/native.md +38 -1
- package/docs/tokens-reference.md +15 -0
- package/package.json +4 -1
- package/src/css/base.css +10 -39
- package/src/css/components/canvas-ink.css +97 -0
- package/src/css/components/gizmo.css +238 -0
- package/src/css/components/swatch.css +187 -0
- package/src/css/components/tree.css +259 -0
- package/src/css/touch-surfaces.mjs +95 -0
- package/tools/gizmo.mjs +144 -0
- package/tools/tree.mjs +178 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Viewport orientation gizmo
|
|
2
|
+
|
|
3
|
+
The orientation widget any 3D or map viewport ships: a compass ring with
|
|
4
|
+
clickable snap targets, a secondary arc for a second angle (pitch, tilt,
|
|
5
|
+
elevation) inside a clamped range, and a centre target that resets the view.
|
|
6
|
+
|
|
7
|
+
CAD and BIM viewers, product configurators, virtual tours, model previews, floor
|
|
8
|
+
plans, any map with a tilt.
|
|
9
|
+
|
|
10
|
+
## A ring, not a cube
|
|
11
|
+
|
|
12
|
+
An Autodesk-style ViewCube is the wrong shape for anything with a **privileged
|
|
13
|
+
up-vector** — a map, a terrain, a site plan — because there is no meaningful
|
|
14
|
+
front, right or bottom face to click. A ring degrades to that case and
|
|
15
|
+
generalises to free orbit; a cube does not go the other way.
|
|
16
|
+
|
|
17
|
+
## Web
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<div
|
|
21
|
+
class="juno-gizmo"
|
|
22
|
+
role="group"
|
|
23
|
+
aria-label="View orientation"
|
|
24
|
+
style="--juno-gizmo-heading:45deg; --juno-gizmo-pitch:35deg;"
|
|
25
|
+
>
|
|
26
|
+
<p class="juno-gizmo__readout" aria-live="polite">
|
|
27
|
+
Facing north-east, 45 degrees. Tilted 35 degrees.
|
|
28
|
+
</p>
|
|
29
|
+
<div class="juno-gizmo__ring">
|
|
30
|
+
<span class="juno-gizmo__needle" aria-hidden="true"></span>
|
|
31
|
+
<button class="juno-gizmo__mark" style="--juno-gizmo-at:0deg" aria-label="Face north">N</button>
|
|
32
|
+
<!-- …seven more, every 45° -->
|
|
33
|
+
<button class="juno-gizmo__center" aria-label="Reset view to north, level">⌖</button>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="juno-gizmo__arc"><span class="juno-gizmo__arc-hand" aria-hidden="true"></span></div>
|
|
36
|
+
</div>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Class / prop | Effect |
|
|
40
|
+
| --------------------------------- | -------------------------------------------- |
|
|
41
|
+
| `.juno-gizmo` | Root. Holds the angles and the derived size. |
|
|
42
|
+
| `.juno-gizmo__readout` | The spoken state. `aria-live="polite"`. |
|
|
43
|
+
| `.juno-gizmo__ring` | The compass ring. |
|
|
44
|
+
| `.juno-gizmo__needle` | Points at `--juno-gizmo-heading`. |
|
|
45
|
+
| `.juno-gizmo__mark` | A snap target. A real `<button>`. |
|
|
46
|
+
| `.juno-gizmo__center` | Reset target. |
|
|
47
|
+
| `.juno-gizmo__arc` / `__arc-hand` | The second angle, clamped. |
|
|
48
|
+
| `--juno-gizmo-heading` / `-pitch` | **The app writes these.** |
|
|
49
|
+
| `--juno-gizmo-at` | One mark's own bearing. |
|
|
50
|
+
| `--juno-gizmo-pitch-min` / `-max` | The clamp (default `0deg`–`85deg`). |
|
|
51
|
+
| `--juno-gizmo-size` | Ring diameter. Derived — see below. |
|
|
52
|
+
|
|
53
|
+
**The app owns the camera.** junoui rotates the needle and the hand; it never
|
|
54
|
+
stores or changes an angle.
|
|
55
|
+
|
|
56
|
+
## The accessibility contract
|
|
57
|
+
|
|
58
|
+
This is why the component is upstream rather than in each app.
|
|
59
|
+
|
|
60
|
+
- **`role="group"`** with an accessible name on the root.
|
|
61
|
+
- **Snap targets are real `<button>`s.** Not a canvas hit test, not a `div` with
|
|
62
|
+
a click handler. A button is focusable, activates on Enter _and_ Space, is
|
|
63
|
+
announced as a control, and works in a screen reader's forms mode.
|
|
64
|
+
- **Every mark carries an accessible name**, because `"N"` is a letter, not a
|
|
65
|
+
name. `aria-label="Face north"`.
|
|
66
|
+
- **One focus stop.** Tab reaches the gizmo once; arrow keys move between marks
|
|
67
|
+
and **wrap**, Enter activates. Eight tab stops for eight compass points is what
|
|
68
|
+
apps ship and what makes the widget unusable by keyboard.
|
|
69
|
+
- **The bearing is announced in words.** A rotating needle announces nothing, and
|
|
70
|
+
`"37deg"` is a number the listener has to convert. The readout is a live region
|
|
71
|
+
saying _"Facing north-east, 37 degrees. Tilted 45 degrees."_
|
|
72
|
+
- **`aria-current="true"`** on the mark the camera is nearest — not a class, since
|
|
73
|
+
the app must say it for the screen reader anyway.
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { enhanceGizmo, orientationLabel, bearingLabel } from 'junoui/gizmo';
|
|
77
|
+
|
|
78
|
+
enhanceGizmo(el); // one focus stop, wrapping arrows
|
|
79
|
+
readout.textContent = orientationLabel(yaw, pitch);
|
|
80
|
+
bearingLabel(37); // → "north-east"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Stateless, like `junoui/tree`: it moves focus and lets the marks' own click
|
|
84
|
+
handlers fire. It never writes an angle.
|
|
85
|
+
|
|
86
|
+
## The diameter is derived, not chosen
|
|
87
|
+
|
|
88
|
+
`N` marks sit evenly around the rim, and each is inset from it by half a target,
|
|
89
|
+
so their centres lie on a circle of radius `d/2 − tap/2`. The straight-line
|
|
90
|
+
distance between two adjacent centres — the **chord**, not the arc — must be at
|
|
91
|
+
least one tap target:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
d ≥ tap · (1 / sin(π / N) + 1)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
which is what `--juno-gizmo-size` computes: `158.98px` at `N = 8` and a 44px
|
|
98
|
+
target.
|
|
99
|
+
|
|
100
|
+
**This derivation was wrong twice, and both errors were invisible in the
|
|
101
|
+
source.** Sizing off the _arc_ between centres (`N · tap / π`) gives 112.05px,
|
|
102
|
+
whose chord is 42.9px — every neighbouring pair overlapping by 1.1px. Correcting
|
|
103
|
+
to the chord but forgetting the inset gives 114.98px and measures 27.16px between
|
|
104
|
+
centres. Both were caught by measuring a laid-out ring, which is why
|
|
105
|
+
`test/visual/gizmo.spec.mjs` asserts the distance between every pair of marks
|
|
106
|
+
rather than trusting the formula. The tap floor **moves** — 24px on a
|
|
107
|
+
fine pointer, 44px on a coarse one — so a ring with a hard-coded diameter has
|
|
108
|
+
eight overlapping targets on a phone. Setting `--juno-gizmo-size` larger is fine;
|
|
109
|
+
`max()` stops it going below what the marks need.
|
|
110
|
+
|
|
111
|
+
## Motion
|
|
112
|
+
|
|
113
|
+
Snap transitions run on `--juno-motion-scale`, so `prefers-reduced-motion`
|
|
114
|
+
collapses them through the base layer without a component-local media query.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Colour swatch & palette
|
|
2
|
+
|
|
3
|
+
Showing a user-chosen colour, and letting someone pick one. Diagrams, calendars,
|
|
4
|
+
tag and label systems, chart series colours, annotation tools, theming UIs,
|
|
5
|
+
kanban boards.
|
|
6
|
+
|
|
7
|
+
## The hard part is not the square
|
|
8
|
+
|
|
9
|
+
A swatch shows an **arbitrary** colour, so every piece of chrome on it — its
|
|
10
|
+
border, its focus ring, its checked indicator — has to stay visible against a
|
|
11
|
+
colour junoui has never seen. A single hairline fails at one end of the range: a
|
|
12
|
+
dark border vanishes on near-black, a light one vanishes on near-white, and the
|
|
13
|
+
swatch that loses its border is the one that has merged with the panel behind it.
|
|
14
|
+
|
|
15
|
+
So the border is a **pair** of hairlines, one dark (inset) and one light
|
|
16
|
+
(outset). Whatever the swatch, one of them contrasts; the other is the one you
|
|
17
|
+
do not notice. `test/swatch.test.mjs` sweeps the swatch colour and asserts one
|
|
18
|
+
ring always clears 3:1, rather than asserting a border value.
|
|
19
|
+
|
|
20
|
+
**Focus rings sit outside the swatch, with an offset**, so their contrast is
|
|
21
|
+
against the panel — a known surface — rather than against a hue junoui cannot
|
|
22
|
+
predict. A ring drawn _on_ the swatch has the same unsolvable problem, and a
|
|
23
|
+
thicker ring does not fix a hue collision.
|
|
24
|
+
|
|
25
|
+
## Web
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<span
|
|
29
|
+
class="juno-swatch"
|
|
30
|
+
style="--juno-swatch-color:#C41E3A"
|
|
31
|
+
role="img"
|
|
32
|
+
aria-label="Crimson"
|
|
33
|
+
></span>
|
|
34
|
+
|
|
35
|
+
<button
|
|
36
|
+
class="juno-swatch juno-swatch--button"
|
|
37
|
+
style="--juno-swatch-color:#1F6FEB"
|
|
38
|
+
aria-label="Annotation colour: Azure"
|
|
39
|
+
popovertarget="palette"
|
|
40
|
+
></button>
|
|
41
|
+
|
|
42
|
+
<div class="juno-popover" popover id="palette">
|
|
43
|
+
<div class="juno-palette" role="listbox" aria-label="Annotation colour">
|
|
44
|
+
<button
|
|
45
|
+
class="juno-palette__option"
|
|
46
|
+
role="option"
|
|
47
|
+
aria-selected="true"
|
|
48
|
+
style="--juno-swatch-color:#1F6FEB"
|
|
49
|
+
aria-label="Azure"
|
|
50
|
+
>
|
|
51
|
+
<svg class="juno-icon juno-palette__check" aria-hidden="true">
|
|
52
|
+
<use href="…#juno-i-check" />
|
|
53
|
+
</svg>
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Class / prop | Effect |
|
|
60
|
+
| ----------------------------- | ------------------------------------------------------- |
|
|
61
|
+
| `.juno-swatch` | The square. Two-tone ring, sized off the control scale. |
|
|
62
|
+
| `.juno-swatch--circle` | Round rather than square. |
|
|
63
|
+
| `.juno-swatch--sm` | `space.16` — inline with body text. |
|
|
64
|
+
| `.juno-swatch--lg` | `size.tap.comfortable` — a primary trigger. |
|
|
65
|
+
| `.juno-swatch--button` | Swatch used as a trigger. |
|
|
66
|
+
| `.juno-swatch--none` | "No colour" — a slash, not a grey. |
|
|
67
|
+
| `.juno-palette__option--none` | The same, as a choice in the grid. |
|
|
68
|
+
| `.juno-palette` | The grid inside a `.juno-popover`. |
|
|
69
|
+
| `.juno-palette__option` | One choice. Selected shows a glyph **and** a ring. |
|
|
70
|
+
| `--juno-swatch-color` | **The app writes this.** |
|
|
71
|
+
| `--juno-swatch-size` | Defaults to `size.tap.min`. |
|
|
72
|
+
| `--juno-palette-columns` | Grid width (default `6`). |
|
|
73
|
+
|
|
74
|
+
The app owns the colour list and which one is chosen.
|
|
75
|
+
|
|
76
|
+
## Colour is never the only signal
|
|
77
|
+
|
|
78
|
+
junoui's standing rule, and a bare swatch is exactly what violates it.
|
|
79
|
+
|
|
80
|
+
- **Every swatch carries an accessible name.** `aria-label="Crimson"`, not a bare
|
|
81
|
+
square. A decorative swatch beside its own visible label can be `aria-hidden`,
|
|
82
|
+
but a swatch that _is_ the information needs the name.
|
|
83
|
+
- **The checked state is a glyph**, not a hue. "The chosen one looks slightly
|
|
84
|
+
different" is invisible to anyone who cannot separate the two hues — and to
|
|
85
|
+
anyone reading a screenshot. The selected option also grows a ring in the
|
|
86
|
+
active role, so there are two non-colour cues.
|
|
87
|
+
- **The check itself sits on an arbitrary colour**, so it gets a light glyph with
|
|
88
|
+
a dark halo — the canvas-ink pair at glyph scale.
|
|
89
|
+
- **"No colour" is a slash, not a grey.** A consumer without this paints unset as
|
|
90
|
+
a mid grey and the user cannot tell _grey_ from _none_, which is a different
|
|
91
|
+
thing to know.
|
|
92
|
+
|
|
93
|
+
`role="listbox"` + `role="option"` + `aria-selected` is the contract for a
|
|
94
|
+
single-choice palette. The popover, its trigger and the open/close behaviour are
|
|
95
|
+
`.juno-popover`'s — this is the grid that goes inside it.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Tree / outliner
|
|
2
|
+
|
|
3
|
+
Nested rows at arbitrary depth with disclosure, selection and a reorder handle.
|
|
4
|
+
Layer stacks, file browsers, settings trees, org charts, comment threads, nested
|
|
5
|
+
navigation. `.juno-list` is flat and `.juno-accordion` is single-level; this
|
|
6
|
+
nests.
|
|
7
|
+
|
|
8
|
+
## Web
|
|
9
|
+
|
|
10
|
+
```html
|
|
11
|
+
<ul class="juno-tree" role="tree" aria-label="Scene">
|
|
12
|
+
<li
|
|
13
|
+
class="juno-tree__item"
|
|
14
|
+
role="treeitem"
|
|
15
|
+
aria-level="1"
|
|
16
|
+
aria-expanded="true"
|
|
17
|
+
aria-selected="false"
|
|
18
|
+
>
|
|
19
|
+
<div class="juno-tree__row">
|
|
20
|
+
<button class="juno-tree__caret" tabindex="-1" aria-hidden="true"></button>
|
|
21
|
+
<svg class="juno-icon juno-tree__icon" aria-hidden="true"><use href="…#juno-i-stack" /></svg>
|
|
22
|
+
<span class="juno-tree__label">Basemap</span>
|
|
23
|
+
<span class="juno-tree__count">3</span>
|
|
24
|
+
<span class="juno-tree__trail"><span class="juno-badge juno--nominal">ON</span></span>
|
|
25
|
+
<button class="juno-tree__handle" aria-label="Reorder Basemap"></button>
|
|
26
|
+
</div>
|
|
27
|
+
<ul class="juno-tree__group" role="group">
|
|
28
|
+
<li class="juno-tree__item" role="treeitem" aria-level="2" aria-selected="true">…</li>
|
|
29
|
+
</ul>
|
|
30
|
+
</li>
|
|
31
|
+
</ul>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Class / prop | Effect |
|
|
35
|
+
| -------------------- | ------------------------------------------------------------------------ |
|
|
36
|
+
| `.juno-tree` | The `role="tree"` root. Sets `--juno-tree-indent`. |
|
|
37
|
+
| `.juno-tree__item` | One `role="treeitem"`. Carries `aria-expanded` / `-level` / `-selected`. |
|
|
38
|
+
| `.juno-tree__group` | A nested `role="group"`. One indent step; depth is structural. |
|
|
39
|
+
| `.juno-tree__row` | The focusable line. Holds the tap floor. |
|
|
40
|
+
| `.juno-tree__caret` | Disclosure triangle; rotates on `aria-expanded="true"`. |
|
|
41
|
+
| `.juno-tree__count` | Count badge slot on a group row. |
|
|
42
|
+
| `.juno-tree__trail` | Trailing-control slot, as on `.juno-list__row`. |
|
|
43
|
+
| `.juno-tree__handle` | Reorder affordance and its hit area. |
|
|
44
|
+
| `--juno-tree-indent` | One indent step (default `space.16`). |
|
|
45
|
+
|
|
46
|
+
Expansion and collapse are `aria-expanded` on the item — the app owns it, as with
|
|
47
|
+
`aria-pressed` elsewhere in junoui. No JS is needed for the visuals.
|
|
48
|
+
|
|
49
|
+
## The ARIA contract
|
|
50
|
+
|
|
51
|
+
This is the half apps get wrong, so it is stated rather than implied.
|
|
52
|
+
|
|
53
|
+
- **`role="tree"`** on the root, with an accessible name (`aria-label` or
|
|
54
|
+
`aria-labelledby`). **`role="group"`** on every nested list, **`role="treeitem"`**
|
|
55
|
+
on every item.
|
|
56
|
+
- **`aria-level`** on every item, 1-based. Required: the nesting is visual, and
|
|
57
|
+
a screen reader does not infer depth from indentation.
|
|
58
|
+
- **`aria-expanded`** on branches only. **Its absence is what makes an item a
|
|
59
|
+
leaf** — do not put `aria-expanded="false"` on a childless row, or it is
|
|
60
|
+
announced as a collapsed branch that never opens.
|
|
61
|
+
- **`aria-selected`** for what the next action applies to. Distinct from
|
|
62
|
+
`aria-current` (which node you are _on_) and from `:hover`. A layer stack has
|
|
63
|
+
all three at once, which is why they paint differently.
|
|
64
|
+
- **Roving tabindex**: exactly one row has `tabindex="0"`, the rest `-1`. One Tab
|
|
65
|
+
stop for the whole tree, not one per node.
|
|
66
|
+
- **Multi-select** is `aria-multiselectable="true"` on the root plus
|
|
67
|
+
`aria-selected` on each item.
|
|
68
|
+
|
|
69
|
+
### Keyboard
|
|
70
|
+
|
|
71
|
+
| Key | Action |
|
|
72
|
+
| ------------ | --------------------------------------------------------------- |
|
|
73
|
+
| ↓ / ↑ | Next / previous **visible** item, across levels |
|
|
74
|
+
| → | Closed branch: expand. Open branch: first child. Leaf: nothing. |
|
|
75
|
+
| ← | Open branch: collapse. Otherwise: parent. |
|
|
76
|
+
| Home / End | First / last visible item |
|
|
77
|
+
| Enter, Space | Select |
|
|
78
|
+
| `*` | Expand every sibling at this level |
|
|
79
|
+
|
|
80
|
+
A tree without this is a list of buttons wearing tree roles. Because it is
|
|
81
|
+
behaviour, no stylesheet can ship it — so junoui ships a stateless enhancer:
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
import { enhanceTree } from 'junoui/tree';
|
|
85
|
+
const stop = enhanceTree(document.querySelector('.juno-tree'));
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
It stores nothing: expansion and selection live on the DOM and belong to you. It
|
|
89
|
+
moves focus and dispatches `juno-tree-toggle` / `juno-tree-select` (bubbling,
|
|
90
|
+
cancelable, `detail.item`) — it does **not** expand, collapse, select or reorder,
|
|
91
|
+
because in a real outliner expanding a node may need to load it.
|
|
92
|
+
|
|
93
|
+
## Touch
|
|
94
|
+
|
|
95
|
+
The row holds `--juno-size-tap-min`, which becomes the 44px comfortable target on
|
|
96
|
+
a coarse pointer. The caret and the handle **paint** small so a dense tree stays
|
|
97
|
+
dense, and grow only their **hit area** with a transparent overlay — a 44px
|
|
98
|
+
painted caret would swallow the row it sits in.
|
|
99
|
+
|
|
100
|
+
**The reorder handle is explicit, and that is not a style choice.** Long-press-drag
|
|
101
|
+
is the obvious gesture and the wrong one: a tree sitting on or beside a pan/zoom
|
|
102
|
+
surface has to let the pan win, and a gesture that means "reorder" here and "pan"
|
|
103
|
+
one pixel to the left is a coin flip. The handle carries `touch-action: none` on
|
|
104
|
+
itself alone, so dragging it never scrolls while the rest of the row still pans.
|
|
105
|
+
|
|
106
|
+
## What junoui does not do
|
|
107
|
+
|
|
108
|
+
Reorder **logic** — the drop calculation, the model mutation, autoscroll. junoui
|
|
109
|
+
ships the affordance, its hit area, and the drop-target styling
|
|
110
|
+
(`data-juno-drop="before|after|into"`, plus `data-juno-dragging`). A drop lands
|
|
111
|
+
_between_ rows by default: the edge line says "between", a filled highlight says
|
|
112
|
+
"into this one", and those are different operations in a tree.
|
package/docs/conformance-kit.md
CHANGED
|
@@ -121,9 +121,21 @@ file that deliberately names wrong classes, and 3 consumer-owned names.
|
|
|
121
121
|
Generated, the typo class of defect cannot exist and the divergence becomes
|
|
122
122
|
a visible decision.
|
|
123
123
|
|
|
124
|
-
**
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
**Shipped**, and the question is answered: **one set**, decided on the rules'
|
|
125
|
+
own rationales rather than merged for tidiness. The tap-highlight rule exists so
|
|
126
|
+
a UA square "never flashes past a rounded control on tap", and every name the
|
|
127
|
+
shorter list omitted is a rounded tappable — `.juno-chip` and
|
|
128
|
+
`.juno-pillbar__overflow` are 999px pills, `.juno-seg__opt` and
|
|
129
|
+
`.juno-toggle-btn` carry `radius-3`. The omission had no stated reason and the
|
|
130
|
+
rationale covers them, so it was an omission, not a decision.
|
|
131
|
+
|
|
132
|
+
`src/css/touch-surfaces.mjs` declares the set; `bundle-css.mjs` emits both
|
|
133
|
+
rules. `touch-action` stays out of the coarse block (a hybrid device reports a
|
|
134
|
+
fine primary pointer while still taking touch input); the highlight stays in it.
|
|
135
|
+
|
|
136
|
+
The guard checks the declared set against the **component sources**, not the
|
|
137
|
+
manifest — the manifest is built from the bundle, and the bundle now contains
|
|
138
|
+
these lists, so a misspelled member would vouch for itself there.
|
|
127
139
|
|
|
128
140
|
## C. Pointer-first responsiveness
|
|
129
141
|
|
|
@@ -225,17 +237,17 @@ the guard that would have failed did not exist on that branch (20260826-039).
|
|
|
225
237
|
|
|
226
238
|
## Sequencing
|
|
227
239
|
|
|
228
|
-
| Slice | Contents
|
|
229
|
-
| ----- |
|
|
230
|
-
| ~~1~~ | ~~A (manifest + helper)~~ — **shipped**
|
|
231
|
-
| 2
|
|
232
|
-
| 3 | C (one pointer-first mechanism)
|
|
233
|
-
| 4 | D (buckets + floating chrome owns its inset)
|
|
234
|
-
| 5 | E (pillbar budget)
|
|
235
|
-
| 6 | F (doctor)
|
|
236
|
-
| 7 | G (checklist, reduced to what F checks)
|
|
237
|
-
|
|
238
|
-
|
|
240
|
+
| Slice | Contents | Depends on |
|
|
241
|
+
| ----- | ---------------------------------------------------- | ---------- |
|
|
242
|
+
| ~~1~~ | ~~A (manifest + helper)~~ — **shipped** | — |
|
|
243
|
+
| ~~2~~ | ~~B2 (generate the `:where()` lists)~~ — **shipped** | 1 |
|
|
244
|
+
| 3 | C (one pointer-first mechanism) | — |
|
|
245
|
+
| 4 | D (buckets + floating chrome owns its inset) | 3 |
|
|
246
|
+
| 5 | E (pillbar budget) | — |
|
|
247
|
+
| 6 | F (doctor) | 1, 3 |
|
|
248
|
+
| 7 | G (checklist, reduced to what F checks) | 6 |
|
|
249
|
+
|
|
250
|
+
Slices 1 and 2, B1 and E-for-dock are done. 024–027 are absorbed and closed.
|
|
239
251
|
|
|
240
252
|
**Review asks:** the three open questions above (A's public subset, B's two
|
|
241
253
|
lists, C's condition), and whether slice 6 is worth its cost before slice 1
|
package/docs/native.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Native
|
|
1
|
+
# Native (Android / iOS / Rust)
|
|
2
2
|
|
|
3
3
|
Native platforms can't parse `oklch()`, so colors are pre-converted to sRGB hex
|
|
4
4
|
during the build. Values match the web rendering.
|
|
@@ -43,6 +43,43 @@ let body: CGFloat = JunoTokens.fontSize14
|
|
|
43
43
|
- Colors are `UIColor` constants named `<palette><Mode><Role>` (camelCase).
|
|
44
44
|
- Dimensions are `CGFloat` constants.
|
|
45
45
|
|
|
46
|
+
## Rust
|
|
47
|
+
|
|
48
|
+
Any native Rust stack — egui, iced, Slint, Bevy, Dioxus desktop, Tauri's Rust
|
|
49
|
+
side. Vendor `dist/rust/juno_tokens.rs` into your crate, or add junoui as a
|
|
50
|
+
build dependency and copy it in `build.rs`.
|
|
51
|
+
|
|
52
|
+
```rust
|
|
53
|
+
include!(concat!(env!("OUT_DIR"), "/juno_tokens.rs")); // or `mod juno_tokens;`
|
|
54
|
+
|
|
55
|
+
let bg = STANDARD_DARK.s0.to_f32_array(); // a whole theme, picked at runtime
|
|
56
|
+
let accent = STANDARD_DARK_ACTIVE; // or one role, flat
|
|
57
|
+
let gap = SPACE_16; // f32 px
|
|
58
|
+
let fade = MOTION_DURATION_BASE_MS; // f32 milliseconds
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Emitted as | From |
|
|
62
|
+
| -------------------------------------------------- | ---------------------------------------------------- |
|
|
63
|
+
| `Rgba` (`hex()`, `to_f32_array()`, `with_alpha()`) | every color token |
|
|
64
|
+
| `Palette` + one const per palette/mode | the same colors, grouped |
|
|
65
|
+
| `f32` | `px` lengths |
|
|
66
|
+
| `f32`, suffixed `_MS` | `ms` durations |
|
|
67
|
+
| `i32` | whole numbers (z-index, font weight) |
|
|
68
|
+
| `f32` | ratios (opacity, line height) |
|
|
69
|
+
| `&str` | CSS-authored values, verbatim (shadows, font stacks) |
|
|
70
|
+
|
|
71
|
+
**`to_f32_array()` is sRGB-encoded, not linear.** If your pipeline wants linear
|
|
72
|
+
(wgpu with a non-sRGB surface format), convert at your boundary — junoui cannot
|
|
73
|
+
know which surface you created.
|
|
74
|
+
|
|
75
|
+
**Shadows and font stacks are shipped unparsed.** A Rust renderer cannot consume
|
|
76
|
+
`0 4px 14px rgb(0 0 0 / 0.35)` directly; they are here so the values live in one
|
|
77
|
+
place, not as ready-made native input.
|
|
78
|
+
|
|
79
|
+
**Do not transcribe these values into your own crate.** That copy is exactly
|
|
80
|
+
what this target exists to stop — it goes stale on the first patch release and
|
|
81
|
+
no lint catches it.
|
|
82
|
+
|
|
46
83
|
## Keeping in sync
|
|
47
84
|
|
|
48
85
|
Re-run `npm run build` in junoui and re-copy the files (or script the copy in your
|
package/docs/tokens-reference.md
CHANGED
|
@@ -113,6 +113,21 @@ Color encodes status, never decoration. Each role has exactly one meaning.
|
|
|
113
113
|
| `shadow.2` | `0 4px 14px rgb(0 0 0 / 0.35)` | `var(--juno-shadow-2)` | `JunoTokens.shadow2` |
|
|
114
114
|
| `shadow.3` | `0 12px 32px rgb(0 0 0 / 0.50)` | `var(--juno-shadow-3)` | `JunoTokens.shadow3` |
|
|
115
115
|
|
|
116
|
+
### ink
|
|
117
|
+
|
|
118
|
+
| Token | Value | CSS variable | Flutter / iOS |
|
|
119
|
+
|---|---|---|---|
|
|
120
|
+
| `ink.canvas.ink` | `#FFFFFF` | `var(--juno-ink-canvas-ink)` | `JunoTokens.inkCanvasInk` |
|
|
121
|
+
| `ink.canvas.halo` | `#000000` | `var(--juno-ink-canvas-halo)` | `JunoTokens.inkCanvasHalo` |
|
|
122
|
+
| `ink.canvas.halo-width` | `2px` | `var(--juno-ink-canvas-halo-width)` | `JunoTokens.inkCanvasHalo-width` |
|
|
123
|
+
| `ink.canvas.halo-width-lg` | `3px` | `var(--juno-ink-canvas-halo-width-lg)` | `JunoTokens.inkCanvasHalo-width-lg` |
|
|
124
|
+
| `ink.canvas.scrim` | `0.28` | `var(--juno-ink-canvas-scrim)` | `JunoTokens.inkCanvasScrim` |
|
|
125
|
+
| `ink.vivid.nominal` | `oklch(76% 0.28 148)` | `var(--juno-ink-vivid-nominal)` | `JunoTokens.inkVividNominal` |
|
|
126
|
+
| `ink.vivid.active` | `oklch(76% 0.24 205)` | `var(--juno-ink-vivid-active)` | `JunoTokens.inkVividActive` |
|
|
127
|
+
| `ink.vivid.target` | `oklch(70% 0.30 328)` | `var(--juno-ink-vivid-target)` | `JunoTokens.inkVividTarget` |
|
|
128
|
+
| `ink.vivid.caution` | `oklch(82% 0.20 82)` | `var(--juno-ink-vivid-caution)` | `JunoTokens.inkVividCaution` |
|
|
129
|
+
| `ink.vivid.warning` | `oklch(68% 0.28 25)` | `var(--juno-ink-vivid-warning)` | `JunoTokens.inkVividWarning` |
|
|
130
|
+
|
|
116
131
|
### Motion
|
|
117
132
|
|
|
118
133
|
| Token | Value | CSS variable | Flutter / iOS |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junoput01/junoui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -48,8 +48,11 @@
|
|
|
48
48
|
"./android/dimens": "./dist/android/dimens.xml",
|
|
49
49
|
"./ios": "./dist/ios/JunoTokens.swift",
|
|
50
50
|
"./flutter": "./dist/flutter/juno_tokens.dart",
|
|
51
|
+
"./rust": "./dist/rust/juno_tokens.rs",
|
|
51
52
|
"./icons": "./dist/icons/juno-icons.svg",
|
|
52
53
|
"./subset": "./tools/subset-sprite.mjs",
|
|
54
|
+
"./tree": "./tools/tree.mjs",
|
|
55
|
+
"./gizmo": "./tools/gizmo.mjs",
|
|
53
56
|
"./testing": "./tools/testing.mjs",
|
|
54
57
|
"./classes.json": "./dist/classes.json",
|
|
55
58
|
"./icons/inline": "./dist/icons/inline.js",
|
package/src/css/base.css
CHANGED
|
@@ -189,47 +189,18 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
|
|
|
189
189
|
`.juno-input` font-size later in the bundle. It did, silently, until
|
|
190
190
|
20260815-006's coarse-pointer project measured it. */
|
|
191
191
|
|
|
192
|
-
/*
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
.juno-btn,
|
|
197
|
-
.juno-dock__item,
|
|
198
|
-
.juno-pillbar__item,
|
|
199
|
-
.juno-tabs__tab,
|
|
200
|
-
.juno-list__row,
|
|
201
|
-
.juno-menu__item
|
|
202
|
-
) {
|
|
203
|
-
-webkit-tap-highlight-color: transparent;
|
|
204
|
-
}
|
|
192
|
+
/* The tap-highlight default that used to live here is GENERATED now, from
|
|
193
|
+
src/css/touch-surfaces.mjs, together with the touch-action default that
|
|
194
|
+
shares its member list. Two hand-maintained lists had drifted from the
|
|
195
|
+
classes and from each other; see that file. */
|
|
205
196
|
}
|
|
206
197
|
|
|
207
|
-
/*
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
NOT inside the pointer:coarse block above: a hybrid device (touch laptop,
|
|
214
|
-
iPad with a trackpad) reports a fine primary pointer while still taking
|
|
215
|
-
touch input, and the property is inert on a mouse anyway.
|
|
216
|
-
Community convention — no primary Apple/WebKit source names it; see
|
|
217
|
-
docs/ios-conformance.md. Named components only, so a consumer's own elements
|
|
218
|
-
are untouched. See 20260803-038. */
|
|
219
|
-
:where(
|
|
220
|
-
.juno-btn,
|
|
221
|
-
.juno-dock__item,
|
|
222
|
-
.juno-pillbar__item,
|
|
223
|
-
.juno-pillbar__overflow,
|
|
224
|
-
.juno-tabs__tab,
|
|
225
|
-
.juno-list__row,
|
|
226
|
-
.juno-menu__item,
|
|
227
|
-
.juno-seg__opt,
|
|
228
|
-
.juno-chip,
|
|
229
|
-
.juno-toggle-btn
|
|
230
|
-
) {
|
|
231
|
-
touch-action: manipulation;
|
|
232
|
-
}
|
|
198
|
+
/* The touch defaults that used to be two hand-maintained `:where()` lists here
|
|
199
|
+
are GENERATED from src/css/touch-surfaces.mjs and emitted straight after this
|
|
200
|
+
file. Both lists had drifted — from the classes (`.juno-seg__option`,
|
|
201
|
+
`.juno-list__item`: neither exists, so `:where()` matched nothing and the
|
|
202
|
+
rule parsed anyway) and from each other. One declared set now feeds both.
|
|
203
|
+
See 20260826-024 and docs/conformance-kit.md. */
|
|
233
204
|
|
|
234
205
|
/* Gesture-owned surfaces — for an element whose pointer events are fully
|
|
235
206
|
driven by app JS (drag-pan, pinch-zoom, swipe classification: a state
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
2
|
+
* Component — Canvas ink (marks drawn over arbitrary imagery)
|
|
3
|
+
* For text and vector ink sitting on a photo, a map, a video frame or a
|
|
4
|
+
* camera feed: anywhere the background is CONTENT rather than one of
|
|
5
|
+
* junoui's surfaces.
|
|
6
|
+
*
|
|
7
|
+
* WHY IT IS NOT JUST A COLOUR. junoui's whole contrast story assumes a
|
|
8
|
+
* controlled surface, s0 through s3 — every role colour's ratio is
|
|
9
|
+
* computed against a known background. Over imagery there is no known
|
|
10
|
+
* background. In one orthophoto a black shadow and a snowfield are
|
|
11
|
+
* adjacent pixels, so no single ink colour is legible and no contrast
|
|
12
|
+
* ratio can be asserted about one. The answer is a PAIR that spans the
|
|
13
|
+
* luminance range: over a light backing the halo carries the contrast,
|
|
14
|
+
* over a dark one the ink does. Neither half works alone, which is why
|
|
15
|
+
* they are applied together by one class.
|
|
16
|
+
*
|
|
17
|
+
* NOT THEMED, on purpose. A satellite image does not get lighter because
|
|
18
|
+
* the user chose light mode. Theming this pair would make it track the
|
|
19
|
+
* app's surface, which is precisely the background it is NOT over.
|
|
20
|
+
* Usage:
|
|
21
|
+
* <figcaption class="juno-canvas-ink">Sector 7 · 1.2 km</figcaption>
|
|
22
|
+
* <svg class="juno-canvas-ink"><path class="juno-canvas-ink__stroke" …/></svg>
|
|
23
|
+
* <div class="juno-canvas-scrim">…chrome floating over the content…</div>
|
|
24
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
25
|
+
|
|
26
|
+
/* Text. Four offset shadows rather than one blur: a blurred shadow fades at
|
|
27
|
+
the glyph's corners, which is exactly where a thin stroke needs the most
|
|
28
|
+
help, and it costs the same. `paint-order` is for SVG text, where a real
|
|
29
|
+
stroke is available and better. */
|
|
30
|
+
.juno-canvas-ink {
|
|
31
|
+
color: var(--juno-ink-canvas-ink);
|
|
32
|
+
paint-order: stroke fill;
|
|
33
|
+
stroke: var(--juno-ink-canvas-halo);
|
|
34
|
+
stroke-width: var(--juno-ink-canvas-halo-width);
|
|
35
|
+
text-shadow:
|
|
36
|
+
var(--juno-ink-canvas-halo-width) 0 0 var(--juno-ink-canvas-halo),
|
|
37
|
+
calc(-1 * var(--juno-ink-canvas-halo-width)) 0 0 var(--juno-ink-canvas-halo),
|
|
38
|
+
0 var(--juno-ink-canvas-halo-width) 0 var(--juno-ink-canvas-halo),
|
|
39
|
+
0 calc(-1 * var(--juno-ink-canvas-halo-width)) 0 var(--juno-ink-canvas-halo);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Display sizes: a 2px halo reads as a hairline against a heavier stroke. */
|
|
43
|
+
.juno-canvas-ink--lg {
|
|
44
|
+
stroke-width: var(--juno-ink-canvas-halo-width-lg);
|
|
45
|
+
text-shadow:
|
|
46
|
+
var(--juno-ink-canvas-halo-width-lg) 0 0 var(--juno-ink-canvas-halo),
|
|
47
|
+
calc(-1 * var(--juno-ink-canvas-halo-width-lg)) 0 0 var(--juno-ink-canvas-halo),
|
|
48
|
+
0 var(--juno-ink-canvas-halo-width-lg) 0 var(--juno-ink-canvas-halo),
|
|
49
|
+
0 calc(-1 * var(--juno-ink-canvas-halo-width-lg)) 0 var(--juno-ink-canvas-halo);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Vector ink — measurement lines, selection outlines. Two passes: the halo
|
|
53
|
+
is the same path drawn wider underneath. In SVG give the halo element
|
|
54
|
+
this class and the mark element __stroke; in canvas, stroke twice. */
|
|
55
|
+
.juno-canvas-ink__halo {
|
|
56
|
+
fill: none;
|
|
57
|
+
stroke: var(--juno-ink-canvas-halo);
|
|
58
|
+
stroke-width: calc(var(--juno-ink-canvas-halo-width) * 2);
|
|
59
|
+
stroke-linecap: round;
|
|
60
|
+
stroke-linejoin: round;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.juno-canvas-ink__stroke {
|
|
64
|
+
fill: none;
|
|
65
|
+
stroke: var(--juno-ink-canvas-ink);
|
|
66
|
+
stroke-linecap: round;
|
|
67
|
+
stroke-linejoin: round;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Status over canvas. The themed role hues are chosen against s0–s3 and wash
|
|
71
|
+
out over a saturated backdrop, so these are the same hues at raised chroma.
|
|
72
|
+
Louder than the panel roles by design — they will look wrong on a panel. */
|
|
73
|
+
.juno-canvas-ink--nominal { color: var(--juno-ink-vivid-nominal); }
|
|
74
|
+
.juno-canvas-ink--active { color: var(--juno-ink-vivid-active); }
|
|
75
|
+
.juno-canvas-ink--target { color: var(--juno-ink-vivid-target); }
|
|
76
|
+
.juno-canvas-ink--caution { color: var(--juno-ink-vivid-caution); }
|
|
77
|
+
.juno-canvas-ink--warning { color: var(--juno-ink-vivid-warning); }
|
|
78
|
+
|
|
79
|
+
/* Chrome floating over live content. Deliberately NOT --juno-opacity-scrim
|
|
80
|
+
(0.62): that value suppresses a modal's background, and here the background
|
|
81
|
+
is the thing the chrome is annotating — greying it out defeats the purpose. */
|
|
82
|
+
.juno-canvas-scrim {
|
|
83
|
+
background: color-mix(in srgb, var(--juno-s0) calc(var(--juno-ink-canvas-scrim) * 100%), transparent);
|
|
84
|
+
-webkit-backdrop-filter: blur(8px);
|
|
85
|
+
backdrop-filter: blur(8px);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* A translucent bar over moving content is the case where reduced
|
|
89
|
+
transparency matters most — the motion behind the text is the problem, not
|
|
90
|
+
the blur. Fall back to the opaque surface. */
|
|
91
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
92
|
+
.juno-canvas-scrim {
|
|
93
|
+
background: var(--juno-s0);
|
|
94
|
+
-webkit-backdrop-filter: none;
|
|
95
|
+
backdrop-filter: none;
|
|
96
|
+
}
|
|
97
|
+
}
|