Skip to content

Commit 6acf59f

Browse files
committed
work
1 parent 36555f1 commit 6acf59f

23 files changed

Lines changed: 1389 additions & 80 deletions

.agents/skills/new-component/SKILL.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ request (see the constraints in
3030
- `useActionBoundToFormParams` for form integration
3131
- Validation via `@jsenv/validation`
3232

33+
### Controls that can be grouped
34+
35+
A control declares the radius of its frame on its own root element, and the
36+
inner element that actually paints the frame takes `border-radius: inherit`.
37+
That is what lets `<Group>` join controls into a single frame — it squares the
38+
corners of its direct children and knows nothing of their internals. A radius
39+
declared on an inner element instead leaves round corners in the middle of a
40+
group, and no amount of CSS in `group.jsx` can fix it from outside.
41+
42+
Assembling controls into one framed row/column is `<Group>`'s job too — never
43+
hand-write negative margins or per-member radius resets. See
44+
[packages/frontend/navi/docs/control_group.md](../../../packages/frontend/navi/docs/control_group.md).
45+
3346
### Navigation Components
3447

3548
- Enhanced `<a>` tags with action execution for links

packages/frontend/navi/dist/jsenv_navi.js

Lines changed: 795 additions & 31 deletions
Large diffs are not rendered by default.

packages/frontend/navi/dist/jsenv_navi.js.map

Lines changed: 21 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/frontend/navi/dist/jsenv_navi_side_effects.js

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/frontend/navi/dist/jsenv_navi_side_effects.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/frontend/navi/docs/AI_INSTRUCTIONS.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ consistency across the app, not from any single call site.
4444
`header`/`body`/`footer` on, `FixedBar` space, `List`'s `scroller`, and
4545
scroll inside a `Dialog`/`Popover`. Read it before writing CSS to make
4646
something scroll — navi almost certainly already has the prop.
47-
- `docs/z_index.md` — stacking: why DOM order is the first tool, what a
47+
- `docs/control_group.md``<Group>`: several controls reading as one framed
48+
object (one border per seam, radius on the outer corners only). Read it
49+
before placing bordered controls against each other, and before writing
50+
negative margins or `border-radius: 0` by hand to make them join.
51+
- `docs/z_index.md` (and its tokens in `src/navi_z_indexes.js`) — stacking: why DOM order is the first tool, what a
4852
`z-index` without `isolation: isolate` actually competes against, and the
4953
values navi's own popups/bars/tables use. Read it before writing a `z-index`.
5054
- `docs/i18n.md` — where the texts an app displays live: `interpolateText` /
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Grouping controls (`<Group>`)
2+
3+
What we want: **several controls reading as one object.** A search input and
4+
its button, a row of segmented buttons, a column of setting rows — when they
5+
belong together, the eye should see one frame with divisions inside it, not
6+
three boxes that happen to touch. Concretely that means: one border along each
7+
seam instead of two side by side, and a radius only on the four outer corners.
8+
9+
`<Group>` is what produces that. Reach for it whenever you place controls
10+
against each other and any of them has a border or a radius — never hand-write
11+
negative margins, `border-radius: 0` overrides, or a `borderRadius` prop set to
12+
`0` on the middle members. Those spellings look right on the case you are
13+
looking at and break on the next one: a member added at the end, a member
14+
hidden by a condition (the "first" is then the second child), a switch from row
15+
to column.
16+
17+
```jsx
18+
<Group>
19+
<Input name="search" placeholder="Search…" />
20+
<Button>Go</Button>
21+
</Group>
22+
```
23+
24+
Stacked, one setting per row:
25+
26+
```jsx
27+
<Group row>
28+
<Picker name="side" ui={…} expandX />
29+
<Picker name="level" ui={…} expandX />
30+
<Picker name="city" ui={…} expandX />
31+
</Group>
32+
```
33+
34+
`row` (a `Box` prop — its children are laid out as rows, so they stack
35+
vertically) is the only prop that changes the arrangement; everything else is
36+
read off the members themselves.
37+
38+
Live examples: `src/control/demos/15_group_demo.html`.
39+
40+
## What Group does to its members
41+
42+
- **Seams**: each member after the first is pulled back by one border width, so
43+
the two borders along a seam become one line. The width comes from the
44+
member's own `--border-width` when it declares one, else from the group's
45+
`--group-border-width` (default: `--navi-control-border-width`).
46+
- **Corners**: the first member loses the radius on the side that joins, the
47+
last one loses it on the other side, and any member in between loses all
48+
four. A single member keeps its own radius — a group of one looks like the
49+
control alone.
50+
- **Overlap order**: the member under the pointer, and the member holding
51+
focus, paints above its neighbours (`position: relative; z-index: 1`).
52+
Without it the border color change and the focus ring of the active member
53+
would be sliced by whichever neighbour is painted after it. The value is `1`
54+
and there is deliberately no `isolation: isolate` — see
55+
[z_index.md](./z_index.md).
56+
57+
Nothing else: a group does not restyle its members, does not impose a size,
58+
and takes any `Box` prop for its own layout.
59+
60+
## Writing a control that belongs in a group
61+
62+
A group squares the corners of its **direct children** — it addresses the
63+
control root, never the elements inside it. So a control declares the radius
64+
of its frame on its own root element, and whatever inner element actually
65+
paints the frame takes `border-radius: inherit`:
66+
67+
```css
68+
.navi_thing {
69+
/* The radius is declared here even though the frame below draws it */
70+
border-radius: var(--thing-border-radius);
71+
72+
.navi_thing_box {
73+
border: ...;
74+
border-radius: inherit;
75+
}
76+
}
77+
```
78+
79+
A control that declares its radius on an inner element instead is invisible to
80+
`Group`: it keeps round corners in the middle of the row, and no rule written
81+
in `group.jsx` can reach it without naming that private class — which is how a
82+
layout component ends up knowing the internals of every control. When you meet
83+
that symptom, fix the control, not the group.
84+
85+
The controls shipped by navi all follow this: `Button`, `Input`, `Select`,
86+
`Picker`, `Picker.Spin`, checkbox, range.
87+
88+
## When it is not a Group
89+
90+
- Controls separated by space, each with its own frame — that is a `Box` with
91+
`spacing`, they were never one object.
92+
- A label and its control — that is `Field`.
93+
- Radio buttons or checkboxes sharing a name and a validation — that is
94+
`RadioGroup` / `CheckboxGroup`, which is about the value, not the frame. They
95+
can be put inside a `Group` if you also want them to share a frame.

packages/frontend/navi/docs/interactions.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,37 @@ it:
249249
```
250250

251251
Reusing the item's own class is the point: the copy is that item, so it is styled
252-
as that item plus whatever being carried changes.
252+
as that item plus whatever being carried changes. The copy is a real element in the
253+
page, in the top layer, and everything about its look is reachable from CSS —
254+
including these, read off the dragged element so a whole list or a single item can
255+
answer:
256+
257+
| Variable | What it changes |
258+
| --------------------- | ------------------------------------------------------------- |
259+
| `--drag-clone-shadow` | what being lifted casts; `none` for something that flies flat |
260+
| `--drag-clone-scale` | how much bigger it gets once picked up |
261+
262+
**What stays behind is the source, not a hole.** The original is never taken out of
263+
the page — it keeps its place in the layout and wears `navi-drag-clone-source`,
264+
which only makes it `visibility: hidden`. So a mark left where the thing was — an
265+
imprint, a dashed outline, the shape a note was pinned on — is drawn ON that
266+
element and not next to it, and its parts have to say `visibility: visible` to come
267+
back from the hidden source:
268+
269+
```css
270+
.paper[navi-drag-clone-source]::after {
271+
position: absolute;
272+
inset: 0;
273+
border: 1px dashed currentColor;
274+
opacity: 0.35;
275+
visibility: visible;
276+
content: "";
277+
}
278+
```
279+
280+
It stays until the answer settles, which is what makes it say where the thing left
281+
from for as long as the question is open — and if the answer refuses, the copy comes
282+
back to it.
253283

254284
`data-drag-axis` says which axes the drag walks, and its default is not the same
255285
for every outcome: `reorder` alone walks the list (`y`, or `x` for a list that runs

packages/frontend/navi/docs/z_index.md

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,22 +69,46 @@ context is how a value ends up tuned to a symptom.
6969

7070
## 5. The values navi plays with
7171

72-
| What | Value | Notes |
73-
| ----------------------------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
74-
| Top layer (`Dialog`/`Popover` with `layer="top"`) | above everything | Browser top layer — no `z-index` involved, nothing in the page can beat it |
75-
| `Dialog`/`Popover` with `layer="local"`, and their backdrop | `--navi-popup-z-index` (1000) `+ stack order` | The stack order increments per open, so the last opened wins |
76-
| Callout (validation messages) | `--callout-z-index` (1000) | |
77-
| `FixedBar` | 1 | `position: fixed` — it opens its own stacking context, but competes in the root one at 1, which is exactly why a stray `z-index: 2` anywhere on the page lands in front of it |
78-
| `List` sticky group labels, `List` footer | 1 | Local to the list |
79-
| `Table` (sticky cells, drag, resize) | 1–7, see `src/control/table/z_indexes.js` | Derived from each other, never literals |
80-
81-
Two things to read from this table:
82-
83-
- navi itself keeps its values low and relative, except for popups, which sit
84-
at 1000 precisely so nothing has to guess;
85-
- an app that writes a number above 1 is already competing with `FixedBar`.
86-
Write `isolation: isolate` on the parent instead, and the number stops
87-
meaning anything outside it.
72+
They all live in `src/navi_z_indexes.js`, as tokens, in one ordered list — the
73+
file is the overview, this table is its summary. Bands are a decade apart so
74+
one can grow without reaching the next, and so a value seen in devtools says
75+
which band it came from.
76+
77+
| Band | Token | Value |
78+
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------- |
79+
| Top layer (`Dialog`/`Popover` with `layer="top"`) || above everything |
80+
| `Dialog`/`Popover` with `layer="local"`, their backdrop, callouts | `--navi-z-index-popup`, `--navi-z-index-callout` | 1000 `+ stack order` |
81+
| `FixedBar` | `--navi-z-index-bar` | 100 |
82+
| Sticky while something scrolls under: `List` header/footer/group labels, `SidePanel` head/foot | `--navi-z-index-sticky` | 10 |
83+
| A `Group` member under the pointer, then the one holding focus | `--navi-z-index-control-hovered`, `--navi-z-index-control-focused` | 1, 2 |
84+
| `Table` sticky cells, drag, resize | `src/control/table/z_indexes.js` | 1–7, derived from each other |
85+
86+
What to read from it:
87+
88+
- **The order matters more than the numbers.** A bar is above anything the page
89+
scrolls, a popup above the bar, and a control raising itself above its
90+
neighbour is at the bottom — a hovered control crossing the top bar is the
91+
bug the gaps exist to make impossible.
92+
- **A z-index that only orders a component's own parts stays a literal** next to
93+
the rule that needs it. Tokens are for what is decided against another
94+
component; putting "above my own sibling" in the global list would only
95+
dilute it.
96+
- **An app writing its own number is competing with this scale.** Write
97+
`isolation: isolate` on the parent instead, and the number stops meaning
98+
anything outside it.
99+
100+
### Why a `Group` member is not isolated
101+
102+
`Group` overlaps its members by one border width, so the one the user is on has
103+
to paint over its neighbour — otherwise its focus ring is sliced in half by the
104+
member that comes after it in the DOM. DOM order cannot express "whichever one
105+
is hovered", so this is a legitimate `z-index`.
106+
107+
`isolation: isolate` on the group would contain those two values, but it would
108+
also contain the popup of a `Picker` held in the group: its 1000 would become
109+
local, and the popup would be capped inside the group instead of covering the
110+
page. So the group is deliberately not isolated, and what keeps its 1 and 2
111+
harmless is the scale above them.
88112

89113
## A card that stacks three layers with no `z-index`
90114

packages/frontend/navi/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import "./src/navi_css_vars.js";
2+
import "./src/navi_z_indexes.js";
23
// The element the URL designates — the one whose id is the hash — shows itself
34
// when it renders, which in an app is long after the browser stopped caring.
45
// Imported for its effect: neither the element nor the page has to opt in.

0 commit comments

Comments
 (0)