actual-css 0.12.0 → 0.13.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 (78) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +7 -8
  3. package/components.json +8 -2
  4. package/dist/actual.full.css +505 -233
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/design-notes/architecture.md +67 -0
  8. package/docs/design-notes/steps.md +45 -28
  9. package/docs/pages/components/avatar.md +17 -11
  10. package/docs/pages/components/breadcrumb.md +16 -2
  11. package/docs/pages/components/list.md +9 -13
  12. package/docs/pages/components/navbar.md +42 -22
  13. package/docs/pages/components/pagination.md +19 -0
  14. package/docs/pages/components/steps.md +24 -20
  15. package/docs/pages/enhancements/scrollspy.md +6 -6
  16. package/docs/pages/enhancements/tabs.md +74 -3
  17. package/docs/pages/forms/floating-field.md +21 -7
  18. package/docs/pages/forms/form-actions.md +3 -6
  19. package/docs/pages/forms/states-and-sizes.md +15 -15
  20. package/docs/pages/foundations/philosophy.md +97 -1
  21. package/docs/pages/foundations/tokens.md +40 -4
  22. package/docs/pages/guides/design-tools.md +1 -1
  23. package/docs/pages/guides/integrating-actual-css.md +74 -8
  24. package/docs/pages/guides/introduction.md +3 -2
  25. package/docs/pages/guides/modular-import.md +1 -1
  26. package/docs/pages/guides/progressive-enhancement.md +25 -7
  27. package/docs/pages/guides/upgrading.md +86 -9
  28. package/docs/pages/layout/center.md +2 -1
  29. package/docs/pages/layout/cluster.md +11 -4
  30. package/docs/pages/layout/grid.md +2 -0
  31. package/docs/pages/layout/sidebar-layout.md +3 -0
  32. package/docs/pages/layout/switcher.md +2 -0
  33. package/docs/pages/layout/topbar.md +6 -4
  34. package/docs/pages/patterns/icon-slot.md +17 -1
  35. package/docs/pages/utilities/flex.md +1 -1
  36. package/docs/pages/utilities/sizing-wrapping.md +51 -6
  37. package/package.json +5 -5
  38. package/src/cli/actual-css.js +1 -1
  39. package/src/css/components/alert.css +34 -34
  40. package/src/css/components/avatar.css +2 -18
  41. package/src/css/components/badge.css +0 -1
  42. package/src/css/components/breadcrumb.css +74 -8
  43. package/src/css/components/drawer.css +23 -42
  44. package/src/css/components/flyout.css +0 -1
  45. package/src/css/components/join.css +2 -2
  46. package/src/css/components/list.css +10 -10
  47. package/src/css/components/modal.css +17 -10
  48. package/src/css/components/navbar.css +10 -8
  49. package/src/css/components/pagination.css +5 -0
  50. package/src/css/components/scrollspy.css +1 -1
  51. package/src/css/components/steps.css +66 -12
  52. package/src/css/components/tab.css +134 -4
  53. package/src/css/core/focus.css +8 -6
  54. package/src/css/core/theme.css +6 -1
  55. package/src/css/core/tokens.css +4 -2
  56. package/src/css/forms/choice-card.css +1 -1
  57. package/src/css/forms/choice.css +1 -1
  58. package/src/css/forms/control.css +1 -1
  59. package/src/css/forms/floating-field.css +20 -18
  60. package/src/css/forms/form-actions.css +5 -8
  61. package/src/css/forms/native.css +2 -2
  62. package/src/css/forms/otp.css +4 -5
  63. package/src/css/forms/switch.css +1 -1
  64. package/src/css/forms/validation.css +23 -30
  65. package/src/css/layout/center.css +2 -0
  66. package/src/css/layout/cluster.css +14 -0
  67. package/src/css/layout/grid.css +7 -0
  68. package/src/css/layout/scroller.css +19 -3
  69. package/src/css/layout/sidebar-layout.css +3 -0
  70. package/src/css/layout/switcher.css +3 -0
  71. package/src/css/utilities/extra.css +3 -3
  72. package/src/js/full.js +4 -0
  73. package/src/js/inline-visible.js +54 -0
  74. package/src/js/parsed.js +21 -0
  75. package/src/js/tab.js +33 -7
  76. package/src/tooling/design-export.js +1 -1
  77. package/src/tooling/design-tokens.js +44 -33
  78. package/src/tooling/theme-contrast.js +32 -24
@@ -0,0 +1,67 @@
1
+ # Architecture
2
+
3
+ How to decide what belongs in Actual, and where. The rules an application
4
+ author follows are in [Philosophy](../pages/foundations/philosophy.md) and
5
+ [Building with Actual CSS](../pages/guides/integrating-actual-css.md); naming is
6
+ in [Naming conventions](naming.md), behavior in
7
+ [Enhancement contract](enhancement-contract.md).
8
+
9
+ The test for where a rule goes: if it changes how someone writes their
10
+ application CSS, it belongs in the guide; if it only decides how Actual is
11
+ built, it belongs here.
12
+
13
+ ## Ownership
14
+
15
+ - **A component owns its intrinsic anatomy, not page composition.** Padding,
16
+ surface, slots and states are the component's; outer margins and sibling
17
+ spacing are not.
18
+ - **A layout primitive on a component replaces its default layout without
19
+ specificity tricks.** The component's own box layout sits in `:where()`
20
+ (`.card`, its header and footer slots). `components/` loads after `layout/`,
21
+ so a plain class declaration would win on source order and flatten
22
+ `class="card media"` into a column.
23
+ - **Structural anatomy never lends a class's weight to a bare element.**
24
+ `:where(dialog).modal > :where(form, .stack)`, not `> :is(form, .stack)`,
25
+ which would give `<form>` the specificity of `.stack`.
26
+
27
+ ## Utilities
28
+
29
+ A utility makes one reusable adjustment. A class that sets a surface, a border,
30
+ padding and type together has become a component, whatever file it lives in.
31
+
32
+ ## Abstraction
33
+
34
+ - **Duplication beats an abstraction that couples unrelated components.** Every
35
+ file under `components/` is a public export (`actual-css/css/components/*`).
36
+ A shared file imported only by the index would break a single-component
37
+ import, so `.modal` and `.drawer` each keep the declarations they share.
38
+ - **No generic primitive because two examples look alike.** A primitive names a
39
+ relationship that recurs across unrelated applications. When the existing
40
+ answer was hard to find, improve the docs instead.
41
+ - **Bytes do not justify specificity.** A selector is not merged or raised to
42
+ save a few compressed bytes when it changes who can override it.
43
+
44
+ ## Tokens
45
+
46
+ A value derived from an overridable token is computed on the element that uses
47
+ it, never as an alias on `:root`: an inherited custom property arrives already
48
+ computed, so an override on a theme island or a region would not reach it. The
49
+ generic focus line (`focus.css`) and the dialog and drawer focus reserves
50
+ follow this rule.
51
+
52
+ ## Decoration
53
+
54
+ A decorative cue must not dictate geometry, focus or scroll behavior. Scrolling
55
+ strips have no shared edge fade: its mask set their scroll-padding and focus
56
+ reserve, and repeated what a clipped item and the scrollbar already show. The
57
+ strips reserve only their focus line. `.tabs` is the exception: it hides its
58
+ scrollbar, so its fade is the cue, and the fade spans the strip's existing
59
+ scroll-padding instead of setting it.
60
+
61
+ Starting a strip on its current item is a progressive enhancement, not a
62
+ contract: `scroll-initial-target` on the item, no runtime. One declaration
63
+ replaces a module, its opt-in token and its wait for parsing. Engines without
64
+ the property start at the first item, which the fade or the scrollbar already
65
+ says is not all; do not reintroduce a script fallback for them. `.pagination` does not scroll at all: it wraps, because a
66
+ click on a page navigates instead of moving the strip, so a scrolling row hid
67
+ pages the mouse could not reach.
@@ -82,7 +82,7 @@ and upcoming is the absence of both, because the platform already spells those.
82
82
 
83
83
  Out of contract — a step name belongs in a `.step-label` — and it degrades in
84
84
  the right direction. `:empty` reads an item's own content, so `<li>Account</li>`
85
- is not empty: it takes the reading width a wrapped label would, and only the
85
+ is not empty: it takes the same authored item floor as a wrapped label, and only the
86
86
  placing and aligning that `.step-label` rules provide are missing. It lays out
87
87
  and does not fall apart, which is all an out-of-contract markup is owed.
88
88
 
@@ -90,11 +90,8 @@ This is the case that makes bare text expensive to support as a first-class
90
90
  form, and it is worth stating precisely, because a component that sizes steps
91
91
  *per label* pays for it twice.
92
92
 
93
- In behaviour: a row mixing wrapped and unwrapped steps compacts only the
94
- wrapped ones. Measured at five steps in a 559px region, the unwrapped step
95
- holds 201px while its four peers shrink to 89px. That skew is what
96
- `> li:has(.step-label)` produces — bare text has no label, so it takes a
97
- marker-wide floor while its wrapped peers keep `--step-min`.
93
+ In behaviour: sizing by `> li:has(.step-label)` would give bare text and
94
+ wrapped labels different floors whenever an author sets `--step-min`.
98
95
 
99
96
  In bytes: the only valid way to express "every step is wrapped" is to enumerate
100
97
  positions (see "`:has()` cannot be nested"), which grows the selector for each
@@ -105,7 +102,7 @@ supported count.
105
102
  a link and bare text.
106
103
 
107
104
  The row-level alternative, `.steps-horizontal:not(:has(.step-label)) > li`,
108
- avoids the skew differently: it makes every step reading-width, so an empty
105
+ avoids the skew differently: it gives every step the authored floor, so an empty
109
106
  step in a mixed row becomes a wide gap rather than a marker. It also puts the
110
107
  compact form itself behind `:has()`, above the Degraded tier, for a shape that
111
108
  needs no modern selector at all.
@@ -156,6 +153,32 @@ stay in the stacked layout.
156
153
  property, so overriding `--step-min` moves the scroll budget but not this
157
154
  query. Same trade as `--grid-min` in `grid.css`.
158
155
 
156
+ ## Even marker pitch and wrapping labels
157
+
158
+ Stacked labelled steps keep an even marker pitch because each outgoing
159
+ connector must reach the next marker's edge. Intrinsic per-label widths would
160
+ move the markers unevenly and make that length depend on a neighbour's width.
161
+
162
+ The first and last markers align with the row's edges at every width. Each
163
+ end cell takes half a normal share plus half a marker; simply moving the marker
164
+ inside a full cell would stretch the outer connectors. Labels follow the same
165
+ start/end alignment, while the middle steps remain centred. The first connector
166
+ crosses two end-cell widths minus both markers, plus the intervening gap.
167
+
168
+ It is not behind a viewport query. Edge alignment is the row's geometry, the
169
+ one the compact and inline forms already have, and a breakpoint on the viewport
170
+ would lay out the same 600px row differently on a phone and in a desktop card.
171
+
172
+ The marker sets the default item floor. A `minmax(0, 1fr)` grid column keeps a
173
+ long label from widening its step; `hyphens: auto` breaks a long word where the
174
+ page's `lang` allows, and `overflow-wrap: anywhere` still lets an unbroken name
175
+ fit when it does not. `--step-gap` separates both the marker from its label and
176
+ one step from the next, so wrapped names cannot run together.
177
+
178
+ Authors can raise `--step-min` when wrapping costs too much height. The row
179
+ then scrolls once those floors and the intervening gaps outgrow its region.
180
+ This budget changes neither orientation nor the wide inline threshold.
181
+
159
182
  ## The compact form
160
183
 
161
184
  The whole unlabelled contract, in both orientations:
@@ -235,8 +258,7 @@ having no share to fill — is only as wide as its own marker.
235
258
 
236
259
  The result is a track flush with both ends of the row, which is what lets it
237
260
  line up with the heading or paragraph that names the step. Keeping the centred
238
- geometry leaves half-cell margins at both ends: measured on five empty steps in
239
- a 736px row, 60px of dead space on each side, with the first marker floating
261
+ geometry leaves half-cell margins at both ends, with the first marker floating
240
262
  inward of a left-aligned heading.
241
263
 
242
264
  It also matches the inline representation, whose `:last-child { flex: 0 0 auto }`
@@ -244,7 +266,7 @@ is the same declaration for the same reason.
244
266
 
245
267
  The connector follows from the alignment rather than being tuned to it. The
246
268
  base geometry starts a connector at `50% + --step-size / 2` — the right edge of
247
- a centred marker — and runs it `100% - --step-size`, to the next marker's left
269
+ a centred marker — and runs it `100% - --step-size + --step-gap`, to the next marker's left
248
270
  edge. Move the marker to the start of the share and that origin becomes
249
271
  `--step-size`; the width is unchanged, and the connector still lands exactly on
250
272
  the next marker. Measured at 736px and at 200px: zero gap either side.
@@ -265,7 +287,7 @@ leave in a component whose whole claim is that sizing follows content.
265
287
  ### Why `:empty` rather than `:has()`
266
288
 
267
289
  It says what the markup says, and it reads the item's own content, which is
268
- what keeps bare text off a marker-wide floor (see "Bare text in an `<li>`").
290
+ what keeps bare text on the same authored floor as labelled peers (see "Bare text in an `<li>`").
269
291
 
270
292
  It also predates `:has()` by a decade, so the compact form reaches the Degraded
271
293
  tier. `:has(.step-label)` appears only in the wide container query, where
@@ -412,7 +434,7 @@ The contract is one sentence. A step label may be a link, a marker never is, and
412
434
  anything else focusable in a step is out of contract.
413
435
 
414
436
  The one `:has(a[href])` in the file is unrelated: a navigable row reserves
415
- `padding-block` for its focus ring, because the scroll container clips at the
437
+ padding on both axes for its focus ring, because the scroll container clips at the
416
438
  padding edge. See "Scrolling on one axis".
417
439
 
418
440
  A link needs no styling rule of its own to go with it. `color: inherit` sits in
@@ -442,13 +464,9 @@ with `.sr-only` on `.step-label`, so the text stays in the accessibility tree
442
464
  while its box disappears. Or the same rules behind a public class, so the author
443
465
  picks the representation instead of a width picking it.
444
466
 
445
- The threshold is not the weak part. Items hold at `flex: 1 0 var(--step-min)`,
446
- so a row's intrinsic width is `count × --step-min` — 35rem for five steps — and
447
- the query fires exactly where a five-step stacked row starts to scroll. Four
448
- steps share that threshold rather than getting their own at 28rem, so between
449
- 448px and 560px they compact although stacked would still fit; splitting the
450
- difference at 32rem is worse, handing five-step rows a scrolling band instead
451
- (40px of overflow at 520px, 20px at 540px).
467
+ A fixed hiding threshold cannot reflect the authored item floor or the actual
468
+ length of a label. Wrapping uses the available share directly, without removing
469
+ content or introducing a per-count breakpoint.
452
470
 
453
471
  ### Why the premise fails
454
472
 
@@ -469,7 +487,7 @@ So the question is not "how narrow before labels must go?" but "does this
469
487
  sequence carry names at all?", and that one is the author's to answer in
470
488
  markup. An unlabelled sequence is compact because it has nothing to show; a
471
489
  labelled one keeps everything it was given. When a labelled row does not fit,
472
- the honest answers are scrolling, more space, or `.steps-vertical`.
490
+ the available answers are wrapping, scrolling, more space, or `.steps-vertical`.
473
491
 
474
492
  ### What the rule is worth, concretely
475
493
 
@@ -611,7 +629,7 @@ retuning surface is `--scroller-thumb` and `--scroller-track`, which belong to
611
629
  `.scroller`. Copying those into Steps duplicates the primitive; omitting them
612
630
  leaves a scrollbar no documented hook reaches.
613
631
 
614
- Measured on a five-step row in a 380px region, the pairing takes the scrollbar
632
+ Measured on a five-step row with an authored `--step-min: 7rem` in a 380px region, the pairing takes the scrollbar
615
633
  from 15px to 10px and the row from 69px to 64px, with the thumb on
616
634
  `--text-muted` instead of the OS grey.
617
635
 
@@ -620,19 +638,18 @@ Its `.stable-gutter` variant, on the other hand, is a trap worth naming.
620
638
  scrollbar — and this row sets `overflow-y: hidden`, so there is never one to
621
639
  reserve for. It does not steady the row's height across the appearance of the
622
640
  horizontal scrollbar, which is what an author would reach for it to do:
623
- measured, the row is 54px when it fits and 64px when it scrolls either way,
624
- while the reserved inline gutter takes 10px off the row's width and pushes
625
- overflow from 36px to 46px. Strictly worse in both directions.
641
+ the reserved inline gutter narrows the row without reserving space for a
642
+ horizontal scrollbar, making horizontal overflow more likely.
626
643
 
627
644
  What makes the row scroll rather than squash is the item floor:
628
645
  `min-inline-size: max(var(--step-size), var(--step-min))`. The `max()` is there
629
- so an author who lowers `--step-min` below `--step-size` narrows the scroll
630
- budget without collapsing items behind their own markers.
646
+ so an author who lowers `--step-min` below `--step-size` cannot collapse items
647
+ behind their own markers.
631
648
 
632
649
  The cost of scrolling is that clipping happens at the padding edge, and a focus
633
- ring is drawn outside its element's border box — `--focus-outline` is
650
+ ring is drawn outside its element's border box — the generic outline is
634
651
  `--focus-ring-width` wide at `--focus-outline-offset`, so 4px past a label that sits
635
- flush with the bottom of a stacked row. A navigable row therefore reserves
652
+ flush with the edge of a stacked row. A navigable row therefore reserves
636
653
  exactly that much `padding-block`, gated on `.steps-horizontal:has(a[href])`. An
637
654
  informational stepper — the common case, with no ring to protect — keeps its
638
655
  height, and so does a navigable column: the padding is compensation for the
@@ -12,12 +12,12 @@
12
12
 
13
13
  ## Class reference
14
14
 
15
- | Class | Kind | Description |
16
- | --------------- | ----------- | -------------------------------------------- |
17
- | `.avatar` | Component | Initials or image in a circular box. |
18
- | `.avatar-stack` | Composition | Overlapping avatar group. |
19
- | `.badge:empty` | Composition | Status dot, at the bottom-end corner. |
20
- | `.sm` / `.lg` | Size | Optical size; also rescales `.avatar-stack`. |
15
+ | Class | Kind | Description |
16
+ | --------------- | ----------- | -------------------------------------- |
17
+ | `.avatar` | Component | Initials or image in a circular box. |
18
+ | `.avatar-stack` | Composition | Overlapping avatar group. |
19
+ | `.badge:empty` | Composition | Status dot, at the bottom-end corner. |
20
+ | `.sm` / `.lg` | Size | Optical size, on an avatar or a stack. |
21
21
 
22
22
  `.avatar` works on `div`, `a` and `button`. The status dot is an empty
23
23
  `.badge` inside an `.avatar`, and needs an `aria-label` of its own.
@@ -79,6 +79,9 @@ is suppressed inside the avatar.
79
79
 
80
80
  ## Avatar stack
81
81
 
82
+ Set `.sm` or `.lg` on `.avatar-stack` to size the group; its avatars inherit
83
+ that size without their own size classes. Child sizes do not resize the stack.
84
+
82
85
  ```html demo
83
86
  <div class="avatar-stack" role="group" aria-label="Team members">
84
87
  <div class="avatar" role="img" aria-label="John Doe" style="--avatar-bg:#E5EEE4">
@@ -93,16 +96,19 @@ is suppressed inside the avatar.
93
96
  </div>
94
97
  ```
95
98
 
96
- ## CSS hooks
99
+ ### Hooks
97
100
 
98
101
  - `--avatar-size` — inline and block size.
99
102
  - `--avatar-radius` — corner radius; set a smaller value for squared avatars.
103
+ - `--avatar-font-size` — initials size.
100
104
  - `--avatar-bg` — background; `data-tone` adjusts the derived foreground.
101
105
  - `--avatar-stack-size` — stack-scoped size that each avatar falls back to.
106
+ - `--avatar-stack-font-size` — stack-scoped initials size, paired with `--avatar-stack-size`.
102
107
  - `--avatar-stack-overlap` — how far stacked avatars overlap.
103
108
  - `--avatar-stack-ring` — separating ring width inside `.avatar-stack`.
104
109
 
105
- Prefer intents for avatar colors. Inside `.avatar-stack`, `--avatar-size` falls
106
- back to a stack-scoped value so the whole group resizes together with `.sm`/`.lg`;
107
- set `--avatar-size` on an individual avatar to opt one out.
108
- `--avatar-stack-overlap` is derived from `--avatar-stack-size`, not `--avatar-size`.
110
+ Prefer intents for avatar colors. Inside `.avatar-stack`, `--avatar-size` and
111
+ `--avatar-font-size` fall back to the stack-scoped pair, which `.sm`/`.lg` on
112
+ the stack set, so the whole group resizes together; set `--avatar-size` on an
113
+ individual avatar to opt one out. `--avatar-stack-overlap` is derived from
114
+ `--avatar-stack-size`, not `--avatar-size`.
@@ -4,8 +4,11 @@
4
4
 
5
5
  - Use a semantic `<nav>` landmark.
6
6
  - Put `.breadcrumb` on the ordered list.
7
- - Use `aria-current="page"` for the current page.
8
- - Separators are generated with CSS (`li + li::before`).
7
+ - Use `aria-current="page"` for the current page, on its link or on its `<li>`,
8
+ and keep it last.
9
+ - Separators are generated with CSS (`> li + li::before`).
10
+ - Put each ancestor label in an `<a>` or a `<span>`: that box carries the
11
+ ellipsis. The current page may be bare text in an `<li aria-current="page">`.
9
12
 
10
13
  ```html demo
11
14
  <nav aria-label="Breadcrumb">
@@ -17,3 +20,14 @@
17
20
  </ol>
18
21
  </nav>
19
22
  ```
23
+
24
+ ## Narrow space
25
+
26
+ A trail stays on one line and never scrolls. When it does not fit, labels
27
+ truncate with an ellipsis: ancestors first, down to a couple of characters
28
+ each, then the current page. A link keeps its full text as its accessible
29
+ name; add a `title` if pointer users should see it on hover.
30
+
31
+ Truncation does not remove levels. For a deep hierarchy on a phone, collapse
32
+ it in markup — keep the root and the current page, and replace the middle
33
+ levels with a `…` item, or render only a link to the parent page.
@@ -39,14 +39,10 @@ gap: `.list-item-content` always takes the row's free space and
39
39
  leading region or not. A settings row — a label, a description, and one
40
40
  control on the end edge — is the two-region form.
41
41
 
42
- Regions align to the row's first line: the leading check or avatar, the title,
43
- and the trailing metadata or control sit on one horizontal line while
44
- supporting text wraps beneath — a structured row, not a centered button. For a
45
- shared two-line arrangement where leading and trailing truly need to center,
46
- the app composes the alignment itself rather than switching this default.
47
- A pure traversal affordance such as a trailing caret can opt out of the
48
- first-line rule on its own item with `align-self: center`, keeping metadata
49
- rows unaffected.
42
+ Regions center on the row. When supporting text runs to three lines or more,
43
+ add `.items-start` to the row, or to the `.list` for every row, so the leading
44
+ avatar and the trailing control stay on the title line instead of floating
45
+ mid-paragraph (top-align, align to first line).
50
46
 
51
47
  ```html demo
52
48
  <ul class="list">
@@ -83,7 +79,7 @@ For a navigable row, keep the list semantics and put `.list-item` on the link:
83
79
  <strong class="list-item-title">Profile</strong>
84
80
  <span class="list-item-text">Name, photo, and contact details</span>
85
81
  </span>
86
- <i class="ti ti-chevron-right list-item-trailing" style="align-self: center" aria-hidden="true"></i>
82
+ <i class="ti ti-chevron-right list-item-trailing" aria-hidden="true"></i>
87
83
  </a>
88
84
  </li>
89
85
  </ul>
@@ -92,10 +88,10 @@ For a navigable row, keep the list semantics and put `.list-item` on the link:
92
88
  Do not attach click JavaScript to the entire row. Use a link for navigation and
93
89
  a button or native control for actions.
94
90
 
95
- In a master/detail navigation, mark the current row with `aria-current` on its
96
- link, using the value that matches the semantics (`page`, `location`, …).
97
- Presence is the contract: set the attribute on the current row and remove it
98
- elsewhere, never `aria-current="false"`. The [Workspaces example](../examples/overview.md)
91
+ Mark the current row on its link with `aria-current="page"` when the row opens
92
+ a page, or `aria-current="true"` when it is the current item of a master/detail
93
+ view (a conversation, a thread, a record) that is not a page of its own. Any
94
+ other value is inert. The [Workspaces example](../examples/overview.md)
99
95
  shows a current row with trailing actions and a flyout. Navigable rows usually
100
96
  want a non-zero `--list-item-pad-inline`: without that gutter, the current
101
97
  indicator touches the leading region and the trailing touches the row edge.
@@ -6,10 +6,9 @@
6
6
  - `.navbar-brand` is the brand link at the inline start.
7
7
  - Horizontal links live in a `.cluster`; its items are `.nav-link`.
8
8
  - `.nav-list` is the shared vertical navigation list, reused by `.drawer` for stacked links.
9
- - Mark the current page with `aria-current="page"` on the active `.nav-link`.
10
- - Presence is the contract: set `aria-current` on the current link and remove
11
- it elsewhere, never `aria-current="false"`. Scrollspy follows the same rule
12
- with `aria-current="location"`.
9
+ - Mark the current page with `aria-current="page"` on the active `.nav-link`,
10
+ or the current section of the page with `aria-current="location"` (what
11
+ scrollspy sets). Any other value is inert.
13
12
 
14
13
  For a public or normally scrolling page, `.navbar` belongs inside the semantic
15
14
  site header. Do not use `.topbar` or `.app-layout` unless the page actually has
@@ -59,31 +58,36 @@ section titles, a pinned account footer, and the native current state.
59
58
  There is no collapse/toggler mechanism. For a responsive navigation, keep the
60
59
  horizontal `.navbar` for the desktop bar and compose the mobile experience from
61
60
  a `.drawer` with a vertical `.nav-list`, opened by a `command="show-modal"` /
62
- `commandfor` trigger. Keep that trigger outside the `<nav>` subtree you hide at
63
- narrow widths, or hiding the nav hides the control that opens it.
61
+ `commandfor` trigger. Keep that trigger in the bar but outside the `<nav>`
62
+ subtree you hide at narrow widths, or hiding the nav hides the control that
63
+ opens it: the bar is then the `<header>`, and the links get their own `<nav>`.
64
64
 
65
65
  When the compact layout drops labels, keep that text in the DOM and hide it
66
66
  visually instead of replacing it with `aria-label` strings: one source for the
67
67
  accessible name.
68
68
 
69
69
  ```html demo
70
- <nav class="navbar" aria-label="Main">
70
+ <header class="navbar">
71
71
  <a class="navbar-brand" href="/">Actual CSS</a>
72
- <ul class="cluster">
73
- <li><a class="nav-link" href="/" aria-current="page">Home</a></li>
74
- <li><a class="nav-link" href="/docs">Docs</a></li>
75
- </ul>
76
- </nav>
77
-
78
- <button class="btn ghost"
79
- type="button"
80
- command="show-modal"
81
- commandfor="site-nav"
82
- aria-haspopup="dialog"
83
- aria-controls="site-nav"
84
- aria-label="Open menu">
85
- <i class="ti ti-menu-2" aria-hidden="true"></i>
86
- </button>
72
+ <div class="cluster">
73
+ <nav class="site-links" aria-label="Main">
74
+ <ul class="cluster">
75
+ <li><a class="nav-link" href="/" aria-current="page">Home</a></li>
76
+ <li><a class="nav-link" href="/docs">Docs</a></li>
77
+ </ul>
78
+ </nav>
79
+
80
+ <button class="btn ghost site-menu"
81
+ type="button"
82
+ command="show-modal"
83
+ commandfor="site-nav"
84
+ aria-haspopup="dialog"
85
+ aria-controls="site-nav"
86
+ aria-label="Open menu">
87
+ <i class="ti ti-menu-2" aria-hidden="true"></i>
88
+ </button>
89
+ </div>
90
+ </header>
87
91
 
88
92
  <dialog class="drawer"
89
93
  id="site-nav"
@@ -107,6 +111,22 @@ accessible name.
107
111
  </dialog>
108
112
  ```
109
113
 
114
+ The demo shows both controls; the application picks one per width:
115
+
116
+ ```css
117
+ @media (width < 40rem) {
118
+ .site-links {
119
+ display: none;
120
+ }
121
+ }
122
+
123
+ @media (width >= 40rem) {
124
+ .site-menu {
125
+ display: none;
126
+ }
127
+ }
128
+ ```
129
+
110
130
  The drawer can be hidden until opened: give the `dialog` the `hidden` attribute
111
131
  in the HTML and let the command runtime manage it, or rely on the drawer being
112
132
  rendered only on the mobile layout. `command` and `commandfor` work without
@@ -54,3 +54,22 @@ buttons — they are decorative, not actionable.
54
54
  </ol>
55
55
  </nav>
56
56
  ```
57
+
58
+ ## Narrow screens
59
+
60
+ When the items do not fit, the row wraps: every page stays visible and
61
+ clickable. That is the fallback, not the mobile design. For a phone or a
62
+ narrow column (responsive, mobile, small screen), render a shorter range — the
63
+ server knows which pages matter around the current one:
64
+
65
+ ```html demo
66
+ <nav aria-label="Pagination">
67
+ <ol class="pagination sm">
68
+ <li><a href="?page=7" class="btn ghost" rel="prev"><i class="ti ti-chevron-left" aria-hidden="true"></i> Previous</a></li>
69
+ <li><a href="?page=7" class="btn ghost" aria-label="Page 7">7</a></li>
70
+ <li><a href="?page=8" class="btn ghost" aria-current="page" aria-label="Page 8">8</a></li>
71
+ <li><a href="?page=9" class="btn ghost" aria-label="Page 9">9</a></li>
72
+ <li><a href="?page=9" class="btn ghost" rel="next">Next <i class="ti ti-chevron-right" aria-hidden="true"></i></a></li>
73
+ </ol>
74
+ </nav>
75
+ ```
@@ -145,23 +145,28 @@ names, or when a step should be navigable:
145
145
  ```
146
146
 
147
147
  **Actual CSS never hides an authored step label.** Labels stay visible at every
148
- width. If a labelled row does not fit its space, it scrolls — and the honest
149
- answers to that are more room, fewer stages, or `.steps-vertical`.
148
+ width. Stacked steps keep their markers evenly spaced, and long names wrap
149
+ inside their share, hyphenated when the page declares a `lang`. The first and
150
+ last steps align with the row's edges, labels included, so the track lines up
151
+ with the heading above it. Short labels fit a phone with no width budget.
150
152
 
151
- Since scrolling is the fallback, a labelled row is worth pairing with
152
- `.scroller`, which gives the native scrollbar the theme's density and colour
153
- instead of the OS default:
153
+ Set `--step-min` when names need a wider minimum share. If the markers or that
154
+ minimum do not fit, the row scrolls; use `.steps-vertical` when every name must
155
+ remain visible without horizontal scrolling.
156
+
157
+ In supporting browsers, a row that scrolls starts on its `aria-current="step"`
158
+ instead of step 1. A row
159
+ that raises `--step-min` can scroll, so it is worth pairing with `.scroller`,
160
+ which gives the native scrollbar the theme's density and colour instead of the
161
+ OS default:
154
162
 
155
163
  ```html
156
164
  <ol class="steps steps-horizontal scroller">
157
165
  ```
158
166
 
159
- Steps does not apply it for you, because scrollbar treatment is an app-wide
160
- decision rather than a per-component one. Every other scroll container —
161
- `.table`, a `.flyout`, a scrolling `dialog` — leaves the scrollbar alone too,
162
- so a page that thinned only its stepper would show two kinds of scrollbar. Put
163
- `.scroller` on whatever scrolls, once. See the sizing and wrapping page for its
164
- hooks.
167
+ Steps does not apply it for you; see
168
+ [one-row strips](../utilities/sizing-wrapping.md#one-row-strips) for the
169
+ page-wide rule and the hooks.
165
170
 
166
171
  Do not add `.stable-gutter` here. It reserves gutter on the inline axis, for a
167
172
  vertical scrollbar this row never draws, so it narrows the row instead of
@@ -361,8 +366,9 @@ On `.steps`, so both orientations read them:
361
366
 
362
367
  `.steps-horizontal` only:
363
368
 
364
- - `--step-min` — minimum item width in the stacked horizontal layout
365
- - `--step-gap` — marker-to-label space in the stacked horizontal layout
369
+ - `--step-min` — minimum width budget for stacked steps; the first and last
370
+ cells use half that budget plus half the marker width
371
+ - `--step-gap` — marker-to-label and between-step space in the stacked layout
366
372
  - `--step-connector` — default horizontal connector background
367
373
  - `--step-inline-connector` — optional connector background for wide horizontal steps
368
374
 
@@ -372,12 +378,11 @@ On `.steps`, so both orientations read them:
372
378
 
373
379
  ## Notes
374
380
 
375
- Do not mix labelled and unlabelled steps in one sequence. Each step is sized on
376
- its own content, so a mixed sequence renders a narrow marker between reading-
377
- width steps — it lays out, but it reads as a gap rather than as progress.
381
+ Do not mix labelled and unlabelled steps in one sequence. Empty steps align
382
+ their markers to the start; labelled steps centre them over their names.
378
383
 
379
384
  A step name belongs in a `.step-label`. Bare text in an `<li>` is outside the
380
- contract: it is content, so it gets a reading width like a label, but no rule
385
+ contract: it takes the same item floor as a label, but no rule
381
386
  places or aligns it.
382
387
 
383
388
  The **2 to 5** range applies to the horizontal orientation, and the inline
@@ -399,9 +404,8 @@ to stay distinguishable there. Steps deliberately ships no forced-colors
399
404
  override of its own: painting the marker `Highlight` makes the number or glyph
400
405
  inside it unreadable, which is worse than the ambiguity it removes.
401
406
 
402
- The inline threshold is calibrated to the default `--step-min`. Container
403
- queries cannot read custom properties, so changing `--step-min` moves the
404
- scroll budget but not the threshold.
407
+ The inline threshold reserves connector space for five named stages. Changing
408
+ `--step-min` moves the stacked scroll budget without moving that threshold.
405
409
 
406
410
  See [Steps representations and thresholds](https://github.com/lekoala/actual-css/blob/master/docs/design-notes/steps.md)
407
411
  for why the contract is shaped this way, and which alternatives were measured
@@ -4,15 +4,15 @@
4
4
 
5
5
  Scrollspy is a behavior hook, not a visual component. It marks a navigation
6
6
  region for the JavaScript enhancement. Pair it with `.nav-list` and `.nav-link`,
7
- or style `[aria-current]` yourself.
7
+ or style `[aria-current="location"]` yourself.
8
8
 
9
9
  ## Class reference
10
10
 
11
- | Class | Kind | Description |
12
- | ------------ | ----------- | --------------------------------------------------------------- |
13
- | `.scrollspy` | Component | Marks the nav region for the enhancer; adds no visual styles. |
14
- | `.nav-list` | Composition | Vertical list of navigation links. |
15
- | `.nav-link` | Component | Navigation link whose active state comes from `[aria-current]`. |
11
+ | Class | Kind | Description |
12
+ | ------------ | ----------- | ------------------------------------------------------------- |
13
+ | `.scrollspy` | Component | Marks the nav region for the enhancer; adds no visual styles. |
14
+ | `.nav-list` | Composition | Vertical list of navigation links. |
15
+ | `.nav-link` | Component | Navigation link, current on `aria-current="location"`. |
16
16
 
17
17
  ## Usage
18
18