@jsenv/navi 0.29.417 → 0.29.419

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 (45) hide show
  1. package/README.md +1 -1
  2. package/dist/dev/jsenv_navi.js +347 -83
  3. package/dist/dev/jsenv_navi.js.map +12 -12
  4. package/dist/jsenv_navi.js +345 -81
  5. package/dist/jsenv_navi.js.map +12 -12
  6. package/docs/AI_INSTRUCTIONS.md +177 -258
  7. package/docs/actions.md +164 -251
  8. package/docs/autofocus.md +66 -83
  9. package/docs/control_object.md +117 -214
  10. package/docs/control_value.md +121 -185
  11. package/docs/create_and_edit.md +159 -254
  12. package/docs/css_architecture.md +196 -260
  13. package/docs/data_states.md +41 -44
  14. package/docs/dialog_shape.md +91 -134
  15. package/docs/drag_interactions.md +291 -454
  16. package/docs/drag_to_travel.md +152 -748
  17. package/docs/dynamic_import.md +119 -136
  18. package/docs/error_handling.md +75 -127
  19. package/docs/field_validation.md +49 -89
  20. package/docs/form_changed.md +61 -78
  21. package/docs/group.md +141 -0
  22. package/docs/i18n.md +31 -76
  23. package/docs/interactions.md +170 -326
  24. package/docs/list_action.md +36 -51
  25. package/docs/list_refresh.md +73 -115
  26. package/docs/mobile_touch.md +56 -62
  27. package/docs/navigation.md +377 -723
  28. package/docs/network_policy.md +61 -75
  29. package/docs/pan_zoom.md +62 -103
  30. package/docs/popup_backdrop.md +128 -168
  31. package/docs/popup_lift.md +179 -268
  32. package/docs/popup_open.md +380 -651
  33. package/docs/resource.md +228 -408
  34. package/docs/route_transitions.md +333 -473
  35. package/docs/safe_area.md +102 -89
  36. package/docs/scroll.md +175 -317
  37. package/docs/state_binding.md +40 -84
  38. package/docs/testid.md +18 -23
  39. package/docs/typography.md +111 -141
  40. package/docs/view_transitions.md +106 -104
  41. package/docs/z_index.md +61 -109
  42. package/package.json +2 -2
  43. package/docs/badge_list.md +0 -83
  44. package/docs/control_group.md +0 -168
  45. package/docs/mobile_layout_pitfalls.md +0 -145
@@ -1,168 +0,0 @@
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` (or `vertical`, the same prop — each member is a row, so they stack
35
- vertically and meet along horizontal seams) is the only prop that changes the
36
- arrangement; everything else is read off the members themselves. Say it rather
37
- than `flex="y"`: the seams follow `row`, not the layout.
38
-
39
- Live examples: `src/control/demos/15_group_demo.html`.
40
-
41
- ## What Group does to its members
42
-
43
- - **Seams**: a member with a member before it is pulled back by one border
44
- width, so the two borders along a seam become one line. The width comes from
45
- the member's own `--border-width` when it declares one, else from the group's
46
- `--group-border-width` (default: `--navi-control-border-width`).
47
- - **Corners**: a member loses the radius on each side where it faces another
48
- member — the first one on the side that joins, the last one on the other
49
- side, one in between on all four. A single member keeps its own radius — a
50
- group of one looks like the control alone. The ask is made twice, in two
51
- forms — see below.
52
- - **Members only**: a child out of the group's flow is none of the group's
53
- business, and there are more of those than it looks. A popup renders inside
54
- its opener's own subtree, so a `Dialog` or `Popover` written next to the
55
- button that opens it is a child of the group itself, and brings its backdrop
56
- along; a callout anchored on a `<button>` is mounted in that button's parent.
57
- They mark themselves `navi-out-of-flow`, and the group reads that: none of
58
- them takes a corner of its own, none moves anyone else's (a row of one button
59
- and the dialog it opens is still a group of one), none is pulled back by a
60
- border width it does not share.
61
- - **Overlap order**: the member under the pointer, the member showing a focus
62
- ring, and the member holding something open (`aria-expanded="true"`) paint
63
- above their neighbours (`position: relative` plus
64
- `--navi-z-index-control-hovered` / `-focused` / `-expanded`). Without it the
65
- border color change and the focus ring of the active member would be sliced
66
- by whichever neighbour is painted after it. The focused member is matched
67
- whether it wears `data-focus-visible` itself or merely contains it — a
68
- control that wraps a real input (`Picker`, `Spin`) draws the ring on its own
69
- frame while the keyboard is held inside; the expanded member is matched the
70
- same way, since it can be wrapped in an enrobage. Expanded ranks highest: a
71
- `Picker` opened by a click shows no focus ring, hands the focus to its popup
72
- and lets the pointer travel to a neighbour, yet its border keeps saying it is
73
- the one open. There is deliberately no `isolation: isolate` — see
74
- [z_index.md](./z_index.md).
75
-
76
- Nothing else: a group does not restyle its members, does not impose a size,
77
- and takes any `Box` prop for its own layout.
78
-
79
- ## Writing a control that belongs in a group
80
-
81
- A group never writes a selector that reaches inside a member — a member's
82
- subtree holds more than the member (a `Picker` renders its popup inside
83
- itself, not in a portal; a control carries buttons of its own, like the clear
84
- cross in a slot), and a rule matching "some descendant" finds all of them. It
85
- asks for a square corner in two forms instead, and a control answers with
86
- whichever fits.
87
-
88
- **The property, on the member itself.** A control declares the radius of its
89
- frame on its own root, and whatever inner element paints that frame takes
90
- `border-radius: inherit`. The group sets the property on the root; the frame
91
- follows.
92
-
93
- ```css
94
- .navi_thing {
95
- /* Declared here even though the box below is what draws it */
96
- border-radius: var(--thing-border-radius);
97
-
98
- .navi_thing_box {
99
- border: ...;
100
- border-radius: inherit;
101
- }
102
- }
103
- ```
104
-
105
- **The custom property, which travels.** A member is not always the control
106
- that carries the frame: a button can arrive wrapped in a tooltip or a link, at
107
- any depth. So the group also sets `--x-corner-top-left-radius` and its three
108
- siblings on the member, and a control that can arrive wrapped reads them as an
109
- override of its own radius. The `--x-` prefix says what they are: navi's
110
- internal wiring between a group and its members, not a surface an app writes
111
- to (an app changes a radius with the `borderRadius` prop, which lands in the
112
- fallback below and is what the corner keeps everywhere the group has no claim):
113
-
114
- ```css
115
- .navi_thing {
116
- border-top-left-radius: var(
117
- --x-corner-top-left-radius,
118
- var(--thing-border-radius)
119
- );
120
- /* …and the three others */
121
- }
122
- ```
123
-
124
- **Whoever answers the ask also stops it.** Custom properties inherit all the
125
- way down, so the control that consumed a corner sets the four back to
126
- `initial` on the first element inside it — otherwise a button in a slot, or the
127
- Save button of a form in an open popup, reads a corner meant for the row that
128
- opened it. `Popover` and `Dialog` stop it too, at their own root: nothing a
129
- popup holds is at a seam.
130
-
131
- Reference: `.navi_button_content` in `button_ui.jsx` (a button reads then
132
- stops), `.navi_picker_box`, `.navi_input_slot`, `.navi_popover`,
133
- `.navi_dialog`.
134
-
135
- **A member drawing the frame itself stops it too**, without being a control. A
136
- row of settings — a `Field`, a `Label`, a `Box` — that paints a background or a
137
- border, or that insets what it holds with padding, is what the outer corner
138
- belongs to; the switch at the right end of that row is not on it. `Box` marks
139
- itself with `navi-box-frame` for exactly those props and stops the claims for
140
- everything inside. A wrapper adding neither is only an enrobage — a tooltip, a
141
- link, a bare `<Box>` — and lets them through to the control that really carries
142
- the frame.
143
-
144
- A control that declares its radius on an inner element instead is invisible to
145
- `Group`: it keeps round corners in the middle of the row, and no rule written
146
- in `group.jsx` can reach it without naming that private class — which is how a
147
- layout component ends up knowing the internals of every control. When you meet
148
- that symptom, fix the control, not the group.
149
-
150
- The controls shipped by navi all follow this: `Button`, `Input`, `Select`,
151
- `Picker`, `Spin`, checkbox, range.
152
-
153
- ## When it is not a Group
154
-
155
- - Controls separated by space, each with its own frame — that is a `Box` with
156
- `spacing`, they were never one object.
157
- - A label and its control — that is `Field`.
158
- - Several controls making ONE value between them (an address out of three
159
- fields, a day and two wheels out of one moment) — that is `ControlGroup`,
160
- which is about the value and draws nothing. Same word, other subject: see
161
- [control_object.md](./control_object.md). Both at once is fine — a `Group`
162
- around the members of a `ControlGroup`.
163
- - Radio buttons or checkboxes sharing a name and a validation — that is
164
- `RadioGroup` / `CheckboxGroup`, which is about the value, not the frame. They
165
- can be put inside a `Group` if you also want them to share a frame.
166
- - Controls that are ONE value between them, with a word written inside the frame
167
- (the hours and the minutes of "07h30") — that is `SpinGroup`, which drops the
168
- members' frames rather than joining them.
@@ -1,145 +0,0 @@
1
- # Mobile layout pitfalls
2
-
3
- ## Horizontal overflow on mobile breaks `<dialog>` positioning
4
-
5
- ### What happens
6
-
7
- On Chrome Android, if **any element causes the document to overflow horizontally**,
8
- the browser inflates the layout viewport (`window.innerWidth/innerHeight`) to match
9
- the full content dimensions instead of the visible screen dimensions.
10
-
11
- Example on a 412×915px phone with a 2000px-wide grid:
12
-
13
- - `window.innerWidth` → `1648` (inflated, ~4× the real width)
14
- - `window.innerHeight` → `3660` (inflated, ~4× the real height)
15
- - `window.visualViewport.width` → `412` (the actual screen)
16
-
17
- **This creates a large invisible empty area below the real content** — the browser
18
- has "expanded" the document to a size that doesn't correspond to anything visible.
19
-
20
- ### Why `<dialog>` breaks
21
-
22
- `position: fixed; margin: auto` is supposed to center an element in the viewport.
23
- But for obscure reasons, Chrome Android centers it relative to the inflated **layout
24
- viewport** instead of the visual viewport.
25
-
26
- With `innerHeight: 3660`, `margin: auto` places the dialog at ~1830px from the top —
27
- the user scrolls down and finds the dialog sitting far below the visible area, miscentered
28
- or completely invisible.
29
-
30
- **To make things worse**, `showModal()` internally calls `.focus()` on the dialog element,
31
- which triggers the browser's native `scrollIntoView`. So after opening, the page automatically
32
- scrolls to bring the dialog into view — scrolling the user away from where they were.
33
-
34
- ### The fix: clip the horizontal overflow in a wrapper
35
-
36
- The root cause is the document overflowing horizontally. The fix is to **never let the
37
- document itself overflow in X** — instead, contain the overflow inside a child wrapper.
38
-
39
- ```html
40
- <body>
41
- <!-- This wrapper absorbs the horizontal overflow of the whole app -->
42
- <div style="overflow-x: clip;">
43
- <!-- all app content goes here -->
44
- </div>
45
-
46
- <!-- Dialog is outside the wrapper, directly in body, placed first -->
47
- <dialog>...</dialog>
48
- </body>
49
- ```
50
-
51
- With this structure:
52
-
53
- - The document never overflows horizontally → layout viewport = visual viewport
54
- - `position: fixed; margin: auto` centers the dialog correctly
55
- - No ghost empty space at the bottom
56
-
57
- As a safety net, add `overflow-x: clip` on `html` and `body` to prevent any content
58
- that forgets to use a wrapper from inflating the layout viewport:
59
-
60
- ```css
61
- html,
62
- body {
63
- overflow-x: clip;
64
- }
65
- ```
66
-
67
- What it costs: content wider than the screen is cut off instead of reachable by dragging.
68
- When some element genuinely needs to be scrolled horizontally (a wide table, a carousel),
69
- give **that element** its own `overflow-x: auto` — the wrapper stays `clip`.
70
-
71
- ### Why `clip` and not `auto` or `hidden`
72
-
73
- `clip` is the only value that clips without turning the box into a **scroll container**.
74
- `auto`, `scroll` and `hidden` all create one, and that has two consequences that show up
75
- far from the wrapper:
76
-
77
- - **Every `position: sticky` in the app sticks to that wrapper**, because sticky resolves
78
- against the nearest scroll container in the DOM — not against the box the app considers
79
- its scroller. The wrapper grows with its content and never scrolls, so nothing sticks
80
- anymore: sticky headers and `<List groupBy>` group labels just scroll away with the
81
- content.
82
- - **Worse than not sticking: the sticky element is offset downwards.** The rectangle a
83
- sticky element sticks within is the scroll container's box shrunk by its
84
- `scroll-padding` (CSS Position L3), and Chromium applies that for an element scroll
85
- container. A wrapper carrying `data-navi-safe-area` has
86
- `scroll-padding-top: var(--navi-safe-area-inset-top)`, so labels come to rest at
87
- `scroll-padding-top + top` — a group label floating a hundred pixels below the bar,
88
- covering the content above it.
89
-
90
- `overflow-x: clip` also lets `overflow-y` stay `visible`, where `hidden`/`auto` force the
91
- other axis to `auto`.
92
-
93
- Note: `overflow: hidden` on `<html>` would additionally create a new containing block,
94
- which could break `position: fixed` in edge cases. A dialog in the top layer is out of
95
- reach of that: the top layer takes it out of every ancestor's containing block, which is
96
- why navi's `Dialog` stays where it is written, in its opener's subtree.
97
-
98
- ### The wrapper is a net, not a fix: find what overflows
99
-
100
- Clipping makes the symptom disappear, and with it the signal. Something wider than
101
- the screen is a layout bug wherever it happens — a width in px, a `min-width`, a grid
102
- of fixed columns, an unbreakable string coming from the data. The wrapper only keeps
103
- that bug from taking the whole mobile viewport down with it.
104
-
105
- So in dev, ask who overflows:
106
-
107
- ```js
108
- import { detectHorizontalOverflow } from "@jsenv/navi";
109
-
110
- if (import.meta.dev) {
111
- detectHorizontalOverflow({ root: document.querySelector("#main") });
112
- }
113
- ```
114
-
115
- It outlines the culprits in red and names them in the console, at load and whenever
116
- the layout changes. It reports the **outermost** box that sticks out (its children
117
- stick out because it does), and stays quiet about what cannot reach the document:
118
- anything inside a box that scrolls or clips on its own — a wide table in its own
119
- `overflow-x: auto` container is doing the right thing — and anything `position: fixed`
120
- or in the top layer.
121
-
122
- Measuring against the wrapper matters here: once it is in `clip` there is no scrollable
123
- overflow left to read, so `scrollWidth > clientWidth` reports nothing. The rectangles of
124
- the descendants are what tells.
125
-
126
- ### Also: place `<dialog>` before content in the DOM
127
-
128
- Even with the viewport fix, `showModal()` triggers `scrollIntoView` on the dialog element.
129
- If the `<dialog>` is placed at the end of the DOM (after all the app content), the page
130
- scrolls down to it when it opens.
131
-
132
- Fix: **place `<dialog>` as the first child of `<body>`**, before any scrollable content.
133
- `scrollIntoView` on an element already at the top of the document has no effect.
134
-
135
- This is advice for a `<dialog>` written by hand. navi's `Dialog` is not moved: it
136
- renders in its opener's subtree (see
137
- [css_architecture.md](./css_architecture.md#counting-children-what-navi-puts-in-your-tree)).
138
-
139
- ### Summary
140
-
141
- | Cause | Symptom | Fix |
142
- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
143
- | Document overflows horizontally | Layout viewport inflated → ghost space below → dialog miscentered | Wrap app content in `overflow-x: clip` container |
144
- | `<dialog>` at end of DOM | Page scrolls to dialog on `showModal()` | Place `<dialog>` first in `<body>` |
145
- | Wrapper uses `auto`/`hidden` | It becomes a scroll container: sticky headers and group labels stick to it (and get offset by its `scroll-padding`) | Use `clip`; put `overflow-x: auto` on the wide element itself |