@uncinq/css-components 1.16.2 → 1.16.3

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 (50) hide show
  1. package/README.md +10 -10
  2. package/css/components/alert.css +5 -0
  3. package/css/components/panel.css +8 -6
  4. package/docs/_index.md +13 -11
  5. package/docs/buttons/_index.md +75 -0
  6. package/docs/buttons/btn-close.md +58 -0
  7. package/docs/buttons/btn-filter.md +36 -0
  8. package/docs/buttons/btn-menu.md +39 -0
  9. package/docs/buttons/btn-search.md +40 -0
  10. package/docs/buttons/btn-share.md +33 -0
  11. package/docs/buttons/btn-toc.md +23 -0
  12. package/docs/buttons/btn-toggle-video.md +58 -0
  13. package/docs/buttons/btn.md +95 -0
  14. package/docs/cascade-layers.md +8 -6
  15. package/docs/content/_index.md +39 -0
  16. package/docs/content/alert.md +97 -0
  17. package/docs/content/badge.md +50 -0
  18. package/docs/content/banner.md +43 -0
  19. package/docs/content/card.md +93 -0
  20. package/docs/content/items.md +76 -0
  21. package/docs/content/list.md +41 -0
  22. package/docs/content/media.md +88 -0
  23. package/docs/content/surtitle.md +43 -0
  24. package/docs/embeds/_index.md +25 -0
  25. package/docs/embeds/embed.md +53 -0
  26. package/docs/embeds/map.md +57 -0
  27. package/docs/embeds/video.md +53 -0
  28. package/docs/forms/_index.md +31 -0
  29. package/docs/forms/form-check.md +77 -0
  30. package/docs/forms/form.md +67 -0
  31. package/docs/navigation/_index.md +29 -0
  32. package/docs/navigation/breadcrumb.md +59 -0
  33. package/docs/navigation/nav-accessibility.md +67 -0
  34. package/docs/navigation/nav-title.md +43 -0
  35. package/docs/navigation/nav.md +65 -0
  36. package/docs/navigation/pagination.md +77 -0
  37. package/docs/overlays/_index.md +34 -0
  38. package/docs/overlays/drawer.md +58 -0
  39. package/docs/overlays/dropdown.md +87 -0
  40. package/docs/overlays/modal.md +75 -0
  41. package/docs/overlays/panel.md +159 -0
  42. package/docs/utilities/_index.md +21 -0
  43. package/docs/{utilities.md → utilities/scrollsnap.md} +26 -9
  44. package/package.json +1 -1
  45. package/docs/buttons.md +0 -103
  46. package/docs/content.md +0 -145
  47. package/docs/embeds.md +0 -66
  48. package/docs/forms.md +0 -80
  49. package/docs/navigation.md +0 -93
  50. package/docs/overlays.md +0 -133
@@ -0,0 +1,57 @@
1
+ ---
2
+ isIndex: false
3
+ title: Map
4
+ description: A Leaflet map container, and where Leaflet's own stylesheet belongs in the layer order.
5
+ weight: 3
6
+ icon: map
7
+ ---
8
+
9
+ A map container, driven by Leaflet through a `js-map` hook.
10
+
11
+ ```html
12
+ <div class="map js-map"
13
+ data-markers="[...]"
14
+ data-zoom="12"
15
+ data-tile="3"
16
+ data-marker-hidden="false"></div>
17
+ ```
18
+
19
+ | Attribute | Role |
20
+ | --- | --- |
21
+ | `data-markers` | The markers to plot |
22
+ | `data-zoom` | Initial zoom level |
23
+ | `data-tile` | Tile layer selection |
24
+ | `data-marker-hidden` | Whether markers start hidden |
25
+
26
+ This package provides the container only: ratio, background, radius and margins. **Leaflet itself, its stylesheet and the script reading those attributes are yours to supply.**
27
+
28
+ ## It breaks out on mobile
29
+
30
+ ```css
31
+ @media not (--sm) {
32
+ --map-ratio: var(--map-ratio-mobile);
33
+
34
+ .container & { margin-inline: calc(-1 * var(--gutter)); }
35
+ .row { display: block; }
36
+ }
37
+ ```
38
+
39
+ Three changes below 768px. The ratio switches to `--map-ratio-mobile`, usually taller, because a wide map on a narrow screen shows almost nothing useful. Inside a `.container` the map pulls out to the screen edges by one gutter. And a `.row` inside it stops laying out in columns.
40
+
41
+ The bleed is deliberately scoped to `.container &`, so a map in a card or a sidebar stays inside its parent instead of overflowing something with no room to give.
42
+
43
+ ## Where Leaflet's CSS goes
44
+
45
+ This is the clearest case in the package for why the layer order matters. Leaflet ships its own stylesheet, and a map is often loaded lazily.
46
+
47
+ ```css
48
+ @layer reset, tokens, libs, base, vendors, layouts, components, pages, utilities;
49
+ ```
50
+
51
+ Import Leaflet's stylesheet into `@layer libs`, and any adjustments you write into `@layer vendors`. A layer's position is fixed the first time its name appears, so a stylesheet injected at runtime lands in the slot its layer already occupies rather than at the end of the cascade. Without that, a lazily loaded Leaflet outranks your own rules purely on source order, and the map's marker styling starts winning arguments it should lose.
52
+
53
+ See [Cascade layers](../../cascade-layers/).
54
+
55
+ ## Accessibility
56
+
57
+ A map with no text alternative is unusable without sight. Whatever the map shows, addresses, opening hours, a route, should also exist as text near it. The container has no role and announces nothing on its own.
@@ -0,0 +1,53 @@
1
+ ---
2
+ isIndex: false
3
+ title: Video
4
+ description: A wrapper for a native video, positioning the optional pause control an autoplaying video needs.
5
+ weight: 2
6
+ icon: camera-video
7
+ ---
8
+
9
+ A wrapper for a **native** `<video>`, as opposed to an embedded player.
10
+
11
+ ```html
12
+ <div class="video">
13
+ <video autoplay muted loop playsinline></video>
14
+ <button class="btn btn-toggle-video is-playing" aria-label="Pause">
15
+ <span class="icon-pause"></span>
16
+ <span class="icon-play"></span>
17
+ </button>
18
+ </div>
19
+ ```
20
+
21
+ ## What it does
22
+
23
+ Three things:
24
+
25
+ ```css
26
+ .video {
27
+ position: relative;
28
+
29
+ video { aspect-ratio: var(--video-ratio, var(--ratio-video)); width: 100%; }
30
+
31
+ .btn-toggle-video { position: absolute; inset-block-end: 0; inset-inline-end: 0; z-index: 10; }
32
+ }
33
+ ```
34
+
35
+ It establishes the positioning context, sizes the video to its container at a known ratio, and pins the control to the bottom inline-end corner. The ratio falls back to `--ratio-video` from the design tokens, so `--video-ratio` only needs setting for a portrait or square clip.
36
+
37
+ ## The control is not decoration
38
+
39
+ A video that autoplays without native controls must be pausable to meet WCAG, which is exactly what [`.btn-toggle-video`](../../buttons/btn-toggle-video/) provides. If your video carries `controls`, you do not need it.
40
+
41
+ The button is the only child the wrapper positions. Anything else you put inside sits in the normal flow.
42
+
43
+ ## Inside a media block
44
+
45
+ ```css
46
+ .media .video { height: 100%; }
47
+ ```
48
+
49
+ A [`.media`](../../content/media/) stretches the wrapper to fill it, so a video and an image are interchangeable within a [card](../../content/card/) without changing the card's structure. The media block's own `--media-ratio` then governs the box, and the video is cropped to it by `object-fit: cover`.
50
+
51
+ ## Autoplay checklist
52
+
53
+ `muted` is required for autoplay to be allowed at all, and `playsinline` keeps iOS from taking the video fullscreen. `loop` is what makes a background video a background video. All three are attributes on the element, not something this component can supply.
@@ -0,0 +1,31 @@
1
+ ---
2
+ isIndex: false
3
+ title: Forms
4
+ description: The .form grid, the .form-check row, and where the actual control styling lives.
5
+ weight: 7
6
+ icon: ui-checks
7
+ ---
8
+
9
+ Form **controls** are styled by [@uncinq/css-base](../../css-base/base/#forms), on the native elements themselves. There is no `.input` or `.select` class to apply.
10
+
11
+ This package adds only what css-base cannot: the layout around those controls.
12
+
13
+ | Page | Class | Covers |
14
+ | --- | --- | --- |
15
+ | [Form](form/) | `.form` | The two-column grid and its layout helpers |
16
+ | [Check row](form-check/) | `.form-check` | A checkbox or radio laid out beside its label |
17
+
18
+ Both live in `components/form.css`.
19
+
20
+ ## Accessibility
21
+
22
+ Nothing here supplies an accessible name. Every control needs a `<label for>` pointing at its `id`, or a `.visually-hidden` label when the design has no room for a visible one.
23
+
24
+ Tie `.help` text to its field with `aria-describedby`, otherwise it is visible but never announced:
25
+
26
+ ```html
27
+ <input id="message" aria-describedby="message-help">
28
+ <p class="help" id="message-help">Markdown is supported.</p>
29
+ ```
30
+
31
+ For error states, use `aria-invalid="true"` on the control and point `aria-describedby` at the error message.
@@ -0,0 +1,77 @@
1
+ ---
2
+ isIndex: false
3
+ title: Check row
4
+ description: A checkbox or radio laid out beside its label, and why the class is only one declaration.
5
+ weight: 2
6
+ icon: check-square
7
+ ---
8
+
9
+ A checkbox or radio laid out inline with its label.
10
+
11
+ ```html
12
+ <div class="form-check">
13
+ <input id="terms" type="checkbox">
14
+ <label for="terms">I accept the terms</label>
15
+ </div>
16
+ ```
17
+
18
+ ## The whole component
19
+
20
+ ```css
21
+ .form-check { display: flex; }
22
+ ```
23
+
24
+ That is the entire rule, and it is deliberate. The control's appearance, its size, and the gap between it and the adjacent label all come from [css-base](../../../css-base/base/#forms), which styles `[type='checkbox'] + label` directly on the native elements.
25
+
26
+ So the only thing missing from a bare input and label is that they sit on separate lines by default. This class fixes that, and stays out of everything else.
27
+
28
+ The practical consequence: if the spacing between a checkbox and its label is wrong, the fix is in css-base or in the tokens, not here. Adding a `gap` to `.form-check` would apply on top of the one css-base already sets, and the two would drift apart.
29
+
30
+ ## The label must come after the control
31
+
32
+ `[type='checkbox'] + label` is an adjacent sibling selector. Wrapping the input inside the label, or putting the label first, means the control gets no spacing and no styling from css-base.
33
+
34
+ ```html
35
+ <!-- works -->
36
+ <div class="form-check">
37
+ <input id="terms" type="checkbox">
38
+ <label for="terms">I accept the terms</label>
39
+ </div>
40
+
41
+ <!-- does not -->
42
+ <div class="form-check">
43
+ <label for="terms">I accept the terms</label>
44
+ <input id="terms" type="checkbox">
45
+ </div>
46
+ ```
47
+
48
+ ## Inside a form grid
49
+
50
+ A check row is a direct child of [`.form`](../form/) like any other field, so it takes a grid track. Give it `.full` when the label is long enough to wrap in half the width:
51
+
52
+ ```html
53
+ <div class="form-check full">
54
+ <input id="newsletter" type="checkbox">
55
+ <label for="newsletter">Send me the monthly newsletter</label>
56
+ </div>
57
+ ```
58
+
59
+ ## Groups of radios
60
+
61
+ There is no class for the group. Use a `<fieldset>` with a `<legend>`, and one `.form-check` per option:
62
+
63
+ ```html
64
+ <fieldset class="full">
65
+ <legend>Preferred contact</legend>
66
+ <div class="form-check">
67
+ <input id="by-email" type="radio" name="contact" value="email">
68
+ <label for="by-email">Email</label>
69
+ </div>
70
+ <div class="form-check">
71
+ <input id="by-phone" type="radio" name="contact" value="phone">
72
+ <label for="by-phone">Phone</label>
73
+ </div>
74
+ </fieldset>
75
+ ```
76
+
77
+ The legend is what names the group in the accessibility tree. A set of radios with individual labels and no legend is announced one option at a time, with nothing saying what is being chosen.
@@ -0,0 +1,67 @@
1
+ ---
2
+ isIndex: false
3
+ title: Form
4
+ description: A responsive grid that is one column or two, never three, plus its layout helpers.
5
+ weight: 1
6
+ icon: columns-gap
7
+ ---
8
+
9
+ A responsive two-column grid that collapses to one column when there is not enough room.
10
+
11
+ ```html
12
+ <form class="form">
13
+ <div>
14
+ <label for="first">First name</label>
15
+ <input id="first" type="text">
16
+ </div>
17
+ <div>
18
+ <label for="last">Last name</label>
19
+ <input id="last" type="text">
20
+ </div>
21
+ <div class="full">
22
+ <label for="message">Message</label>
23
+ <textarea id="message"></textarea>
24
+ <p class="help" id="message-help">Markdown is supported.</p>
25
+ </div>
26
+ <div class="actions">
27
+ <button class="btn" type="submit">Send</button>
28
+ </div>
29
+ </form>
30
+ ```
31
+
32
+ Each field is a plain `div`. There is no `.form-group` or `.field` class: the grid places its direct children, whatever they are.
33
+
34
+ ## One column or two, never three
35
+
36
+ ```css
37
+ grid-template-columns: repeat(
38
+ auto-fill,
39
+ minmax(max(var(--form-col-min-width, 25ch), calc(50% - var(--gap) / 2)), 1fr)
40
+ );
41
+ ```
42
+
43
+ The `max()` is doing two jobs at once. It sets a **floor** of `25ch`, below which a field is too narrow to use, and it sets each track to at least half the width minus half the gap, which **caps** the result at two columns however wide the container gets.
44
+
45
+ A three-column form is almost never what you want, because it breaks the vertical reading order of a sequence of fields: the eye has to move right twice before dropping a line, and nothing in the layout says which order the fields belong in.
46
+
47
+ Change the floor with `--form-col-min-width`. Raising it past 50% forces a single column, which is a reasonable way to write a narrow form without a variant class.
48
+
49
+ ## Layout helpers
50
+
51
+ | Class | Effect |
52
+ | --- | --- |
53
+ | `.full` | Spans both columns, `grid-column: 1 / -1` |
54
+ | `.actions` | Spans both columns and stacks its children vertically |
55
+ | `.help` | Help text under a field |
56
+
57
+ `.actions` is a column flex container aligned to the start, with `--gap` between its children and their block margins flattened. So a submit button, a consent line and a legal note stack in the order written without fighting each other's margins.
58
+
59
+ `.help` reads `--help-*` for colour, size, weight, margin and alignment. Pair it with `aria-describedby`, or it is seen and never announced.
60
+
61
+ ## Field spacing
62
+
63
+ Both axes use `--gap`, the same token the grid uses for its column calculation. That is why the two-column cap subtracts `var(--gap) / 2`: the track width and the gutter come from one value, so changing it cannot desynchronise them.
64
+
65
+ ## Controls are not styled here
66
+
67
+ Inputs, selects, textareas and their focus and invalid states come from [css-base](../../../css-base/base/#forms), applied to the native elements. This file adds layout and nothing else, which is why a form built without `.form` still looks right, it is simply stacked.
@@ -0,0 +1,29 @@
1
+ ---
2
+ isIndex: false
3
+ title: Navigation
4
+ description: Navigation lists, navigation titles, skip links, breadcrumbs and pagination.
5
+ weight: 5
6
+ icon: signpost
7
+ ---
8
+
9
+ Five components for moving around a site.
10
+
11
+ | Page | Class | Purpose |
12
+ | --- | --- | --- |
13
+ | [Nav](nav/) | `.nav` | The base navigation list |
14
+ | [Nav title](nav-title/) | `.nav-title` | A label above a group of links |
15
+ | [Skip links](nav-accessibility/) | `.nav-accessibility` | Skip navigation for keyboard and assistive technology |
16
+ | [Breadcrumb](breadcrumb/) | `.breadcrumb-wrapper` | The breadcrumb trail |
17
+ | [Pagination](pagination/) | `.pagination` | Page navigation |
18
+
19
+ ## Two of them take the class on the wrapper
20
+
21
+ `.breadcrumb-wrapper` goes on the `<nav>`, not on the `<ol>` inside it. `.nav-accessibility` goes on the list itself, but only alongside `.nav`. Neither is guessable from the name, so check the page before writing the markup.
22
+
23
+ ## None of them supplies the landmark
24
+
25
+ `<nav>`, `aria-label` and `aria-current` are yours to write. A page with three navigation regions and no labels on them is harder to use with a screen reader than a page with one, which is the usual reason to add them.
26
+
27
+ ## `.item` means two different things
28
+
29
+ In [pagination](pagination/), `.item` is a page number. It is unrelated to the `.item` card unit mentioned throughout the [Content](../content/) section, which is not defined in this package at all.
@@ -0,0 +1,59 @@
1
+ ---
2
+ isIndex: false
3
+ title: Breadcrumb
4
+ description: The breadcrumb trail, with the class on the wrapper and the separator drawn from a token.
5
+ weight: 4
6
+ icon: chevron-double-right
7
+ ---
8
+
9
+ The breadcrumb trail. **The class goes on the wrapper, not on the list.**
10
+
11
+ ```html
12
+ <nav class="breadcrumb-wrapper" aria-label="Breadcrumb">
13
+ <ol>
14
+ <li><a href="/">Home</a></li>
15
+ <li><a href="/docs">Docs</a></li>
16
+ <li aria-current="page">Navigation</li>
17
+ </ol>
18
+ </nav>
19
+ ```
20
+
21
+ `.breadcrumb-wrapper` styles the `ol` and the `li` inside it by element, so there is no class to put on either. The name says wrapper for that reason.
22
+
23
+ ## The separator
24
+
25
+ Drawn on `li::before` from a token, with the first item's suppressed:
26
+
27
+ ```css
28
+ li::before { content: var(--breadcrumb-separator); color: var(--breadcrumb-color-separator); }
29
+ li:first-child::before { display: none; }
30
+ ```
31
+
32
+ `--breadcrumb-separator` defaults to `"/"` and takes any `content` value, so a chevron or an arrow is a token change. It needs the quotes, since it is dropped into `content` as is.
33
+
34
+ Because the separator is generated content rather than text, it is not selected when a reader copies the trail, and screen readers skip it.
35
+
36
+ ## Marking the current page
37
+
38
+ Use `aria-current="page"`, which is both the accessible marker and the styling hook:
39
+
40
+ ```css
41
+ li[aria-current="page"] { color: var(--breadcrumb-color-text-active); }
42
+ ```
43
+
44
+ There is no `.active` class here, deliberately. Relying on the attribute means the visual state and the announced state cannot disagree, and it rules out the other common approach, styling `:last-child`, which breaks the moment a trail ends on something other than the current page.
45
+
46
+ ## Overflow on narrow screens
47
+
48
+ ```css
49
+ overflow-x: auto;
50
+ li { flex-shrink: 0; white-space: nowrap; }
51
+ ```
52
+
53
+ A long trail scrolls sideways rather than wrapping into several lines or truncating. Items keep their full width, so the deepest entries stay readable and the reader scrolls to reach the shallow ones.
54
+
55
+ The wrapper also carries a block border and its own background, which is what lets a breadcrumb sit as a full-width band under the header.
56
+
57
+ ## Accessibility
58
+
59
+ `aria-label` on the `<nav>` is what distinguishes this landmark from the other navigation regions on the page. Use an `<ol>`, not a `<ul>`: the order of a breadcrumb is its meaning.
@@ -0,0 +1,67 @@
1
+ ---
2
+ isIndex: false
3
+ title: Skip links
4
+ description: Skip navigation for keyboard and assistive technology users, hidden off-screen rather than hidden.
5
+ weight: 3
6
+ icon: universal-access
7
+ ---
8
+
9
+ Skip links, for keyboard and assistive technology users.
10
+
11
+ ```html
12
+ <ul class="nav nav-accessibility">
13
+ <li><a href="#main">Skip to content</a></li>
14
+ <li><a href="#navigation">Skip to navigation</a></li>
15
+ <li><a href="#footer">Skip to footer</a></li>
16
+ </ul>
17
+ ```
18
+
19
+ Three things matter here, and getting any of them wrong defeats the purpose.
20
+
21
+ ## It must be the first focusable element in `<body>`
22
+
23
+ A skip link that comes after the navigation it skips is useless. It goes before the header, before any cookie banner, before anything else that can take focus.
24
+
25
+ ## It is hidden off-screen, not hidden
26
+
27
+ ```css
28
+ .nav-accessibility {
29
+ position: absolute;
30
+ transform: translateY(-100%);
31
+
32
+ &:focus-within { transform: translateY(0); }
33
+ }
34
+ ```
35
+
36
+ `display: none` or `visibility: hidden` would remove it from the tab order entirely, which is the one thing a skip link cannot afford. It is pushed out of view with a transform and slides back on `:focus-within`, so the whole group appears as soon as any link inside it is reached.
37
+
38
+ The slide is behind `prefers-reduced-motion`; with motion disabled the group simply appears.
39
+
40
+ ## It requires the `.nav` base class
41
+
42
+ `.nav-accessibility` sets `flex-direction: row` and a small font size, but the flex layout, the list reset and the link padding all come from [`.nav`](../nav/). On its own it is an unstyled list positioned off-screen.
43
+
44
+ ## The focus treatment is deliberate
45
+
46
+ ```css
47
+ a {
48
+ background: var(--color-background);
49
+ &:focus {
50
+ background: var(--color-text);
51
+ color: var(--color-background);
52
+ outline: none;
53
+ }
54
+ }
55
+ ```
56
+
57
+ The resting background is what keeps the links readable once the group slides over the page content. On focus the colours invert, which is why the usual focus ring is dropped: the inversion is the focus indicator, and it meets the contrast requirement on its own.
58
+
59
+ Note this uses `:focus`, not `:focus-visible`, so the indicator appears however focus arrived.
60
+
61
+ ## The targets have to be focusable
62
+
63
+ `z-index: 300` puts the group above sticky headers, but a working skip link also needs somewhere to land. An `id` on a non-interactive element needs `tabindex="-1"`, otherwise the jump scrolls the page without moving focus, and the next Tab press continues from the top of the document:
64
+
65
+ ```html
66
+ <main id="main" tabindex="-1">
67
+ ```
@@ -0,0 +1,43 @@
1
+ ---
2
+ isIndex: false
3
+ title: Nav title
4
+ description: A label above a group of navigation links.
5
+ weight: 2
6
+ icon: type
7
+ ---
8
+
9
+ A title within a navigation block, for labelling a group of links.
10
+
11
+ ```html
12
+ <nav aria-labelledby="docs-nav">
13
+ <p class="nav-title" id="docs-nav">Documentation</p>
14
+ <ul class="nav">
15
+ <li><a href="/docs/install">Installation</a></li>
16
+ <li><a href="/docs/usage">Usage</a></li>
17
+ </ul>
18
+ </nav>
19
+ ```
20
+
21
+ It is a leaf component with no children and no variants:
22
+
23
+ ```css
24
+ .nav-title {
25
+ font-size: var(--nav-title-font-size);
26
+ font-weight: var(--nav-title-font-weight);
27
+ margin-block: var(--nav-title-margin-block, 0);
28
+ padding-block: var(--nav-title-padding-block);
29
+ padding-inline: var(--nav-title-padding-inline);
30
+ }
31
+ ```
32
+
33
+ ## It is a sibling of the list, not an item in it
34
+
35
+ Putting it inside the `<ul>` as an `<li>` would make it a list item that screen readers count and announce as one, and [`.nav`](../nav/) would style it as a navigation entry. Keep it outside.
36
+
37
+ ## Why it carries inline padding
38
+
39
+ `--nav-title-padding-inline` exists so the label lines up with the link text below it, which is itself inset by `--nav-link-padding-inline`. Keep the two in step when you change either, or the title sits proud of the list it labels.
40
+
41
+ ## Use it as the accessible name
42
+
43
+ A group of links deserves a label in the accessibility tree as well as on screen. Point the surrounding `<nav>` at it with `aria-labelledby` rather than repeating the text in an `aria-label`, so the two cannot drift apart.
@@ -0,0 +1,65 @@
1
+ ---
2
+ isIndex: false
3
+ title: Nav
4
+ description: The base navigation list, vertical by default, whose direction is set by context rather than by a modifier class.
5
+ weight: 1
6
+ icon: compass
7
+ ---
8
+
9
+ The base navigation list, vertical by default.
10
+
11
+ ```html
12
+ <ul class="nav">
13
+ <li><a href="/">Home</a></li>
14
+ <li><a href="/about">About</a></li>
15
+ </ul>
16
+ ```
17
+
18
+ Works on `ul` or `ol`.
19
+
20
+ ## Direction is set by context, not by a modifier
21
+
22
+ There is no `.nav-horizontal`. The direction comes from a token:
23
+
24
+ ```css
25
+ flex-direction: var(--nav-direction, column);
26
+ ```
27
+
28
+ so a header lays its own navigation out horizontally by setting `--nav-direction: row` on it, or `flex-direction` directly. That keeps the component free of layout variants that would only ever be used in one place, and it means the same list is a column in a drawer and a row in a header without a class change.
29
+
30
+ ## Nested lists inherit the layout
31
+
32
+ ```css
33
+ &,
34
+ :is(ul, ol) { display: flex; flex-direction: var(--nav-direction, column); ... }
35
+ ```
36
+
37
+ The rule matches the nav **and** every list inside it, so a submenu needs no second `.nav` class. Note that a nested list inherits `--nav-direction` too, so a horizontal header menu gets horizontal submenus unless the submenu resets it.
38
+
39
+ ## Links
40
+
41
+ `a:where(:not(.btn))` and `li > span` are styled together, so a non-link entry, the current page rendered as text, lines up with the links around it. `.btn` is excluded through `:where()`, which adds no specificity, so a call to action inside a navigation keeps its own styling.
42
+
43
+ The current entry is marked with a class on the link:
44
+
45
+ ```html
46
+ <li><a href="/about" class="active" aria-current="page">About</a></li>
47
+ ```
48
+
49
+ ```css
50
+ &.active { --nav-color-text: var(--nav-color-text-active); }
51
+ ```
52
+
53
+ The class drives the colour; `aria-current` is what a screen reader announces. Write both.
54
+
55
+ ## Highlighted items
56
+
57
+ `.nav-item-highlighted` tightens the line height and pulls the item out to the nav's own inline padding, for an entry rendered as a block rather than as a line of text.
58
+
59
+ ## Dropdowns inside a nav
60
+
61
+ `.dropdown-toggle` is restyled to take `--nav-color-text`, so a [dropdown](../../overlays/dropdown/) in a header menu matches the links beside it. Nothing else about the dropdown changes.
62
+
63
+ ## Inside a panel
64
+
65
+ A `.modal` or `.drawer` publishes `--nav-color-text` from its own text colour, so a navigation inside a panel follows the panel's palette. Going inline with [`.panel-inline-*`](../../overlays/panel/#inline-variants) hands that token back to the page, which is why a drawer menu that becomes a sidebar column changes colour at the breakpoint.
@@ -0,0 +1,77 @@
1
+ ---
2
+ isIndex: false
3
+ title: Pagination
4
+ description: Page navigation, with masked control glyphs and a built-in responsive collapse.
5
+ weight: 5
6
+ icon: three-dots
7
+ ---
8
+
9
+ A page navigation list.
10
+
11
+ ```html
12
+ <nav aria-label="Pagination">
13
+ <ul class="pagination">
14
+ <li class="disabled"><a class="first">First</a></li>
15
+ <li><a class="previous" href="?page=1">Previous</a></li>
16
+ <li class="item active"><a href="?page=2">2</a></li>
17
+ <li class="item item-adjacent"><a href="?page=3">3</a></li>
18
+ <li><a class="next" href="?page=3">Next</a></li>
19
+ <li><a class="last" href="?page=9">Last</a></li>
20
+ </ul>
21
+ </nav>
22
+ ```
23
+
24
+ | Class | On | Role |
25
+ | --- | --- | --- |
26
+ | `.first`, `.previous`, `.next`, `.last` | the `a` | Navigation controls |
27
+ | `.item` | the `li` | A page number |
28
+ | `.item-adjacent` | the `li` | A page number next to the current one |
29
+ | `.active` | the `li` | The current page |
30
+ | `.disabled` | the `li` | An unavailable control |
31
+
32
+ `.active` and `.disabled` are both read as ancestors of the link (`.active a`, `.disabled a`), so putting either on the `li` is what the rules expect.
33
+
34
+ ## The glyphs are shipped
35
+
36
+ Each control draws a mask on `::after` from its own token:
37
+
38
+ ```css
39
+ .next::after { mask: var(--pagination-icon-next) center / var(--pagination-icon-size) no-repeat; }
40
+ ```
41
+
42
+ Four tokens, `--pagination-icon-first`, `-previous`, `-next` and `-last`, sized by `--pagination-icon-size`. The two backwards controls reuse their forwards glyph and rotate it:
43
+
44
+ ```css
45
+ .first::after { mask: var(--pagination-icon-first) ...; rotate: 180deg; }
46
+ .previous::after { mask: var(--pagination-icon-previous) ...; rotate: 180deg; }
47
+ ```
48
+
49
+ So a themed arrow only has to exist in one direction. Because the glyph is a mask over `currentColor`, it follows the link colour through hover, active and disabled without a second declaration.
50
+
51
+ The text inside the control (`First`, `Previous`) sits beside the glyph. Replace it with a `.visually-hidden` span if the design wants icons alone; do not remove it, or the control loses its accessible name.
52
+
53
+ ## The responsive collapse is built in
54
+
55
+ ```css
56
+ @media not (--sm) {
57
+ .first, .last,
58
+ li:has(.first), li:has(.last),
59
+ .item:not(.item-adjacent):not(.active) { display: none; }
60
+ }
61
+ ```
62
+
63
+ Below 768px the paginator drops to *previous, the current page, its neighbours, next*. You do not need to write that rule, and you do not need a second markup path for mobile: render the full paginator and let CSS thin it out.
64
+
65
+ This is what `.item-adjacent` is for. It is the only signal the CSS has about which page numbers are near the current one, so a paginator that omits it collapses to the current page alone on a narrow screen.
66
+
67
+ The `li:has(...)` selectors are there so the list item goes with the control it contains, rather than leaving an empty `li` contributing a gap.
68
+
69
+ ## States
70
+
71
+ `.active a` and `.disabled a` both get `pointer-events: none`, so neither is clickable regardless of whether you render an `href`. The active page takes the `-active` colour pair, and a disabled control takes `--pagination-disabled-opacity`.
72
+
73
+ Note that the hover rule here is **not** behind `@media (hover: hover) and (pointer: fine)`, unlike the rest of the package. On a touch device a tapped page number can keep its hover background until the next paint.
74
+
75
+ ## Accessibility
76
+
77
+ The `<nav>` and its `aria-label` are yours to write; the component styles the list only. Mark the current page with `aria-current="page"` on the link as well as `.active` on the item, so the state is announced and not merely coloured.
@@ -0,0 +1,34 @@
1
+ ---
2
+ isIndex: false
3
+ title: Overlays
4
+ description: The shared panel skeleton behind .modal and .drawer, the two opening strategies, and the .dropdown contract.
5
+ weight: 3
6
+ icon: window-stack
7
+ ---
8
+
9
+ Four files, and a design worth understanding before reading any of them.
10
+
11
+ | Page | Covers |
12
+ | --- | --- |
13
+ | [Panel](panel/) | The shared skeleton, `.panel-inline-*`, `.panel-trigger-*`, the backdrop and the JavaScript contract |
14
+ | [Modal](modal/) | `.modal`, the centred configuration |
15
+ | [Drawer](drawer/) | `.drawer` and its four edge variants |
16
+ | [Dropdown](dropdown/) | `.dropdown`, a contextual menu with its own contract |
17
+
18
+ ## `.modal` and `.drawer` are the same panel
19
+
20
+ Same structure, header and content and footer. Same open and closed contract. Same backdrop. They differ only in where they sit: modal centres them, drawer pins them to an edge.
21
+
22
+ `components/panel.css` owns **every rule**. The two variants are pure configuration, declaring nothing but `--panel-*` tokens.
23
+
24
+ There is **no `.panel` class to apply.** The file targets `:where(.modal, .drawer)` directly, so the skeleton arrives with either variant class. `panel` names the shared concept and the token namespace, not a class.
25
+
26
+ Each variant aliases `--panel-*` from its own namespace, so `--modal-*` and `--drawer-*` remain the public API. Override those.
27
+
28
+ ## The tag picks the strategy, not the class
29
+
30
+ Either variant works both ways, and the choice is an accessibility one rather than a styling one. See [Two ways to open a panel](panel/#two-ways-to-open-a-panel).
31
+
32
+ ## No JavaScript ships here
33
+
34
+ `.modal`, `.drawer` and `.dropdown` all expect a script. The CSS defines the classes and attributes that script must toggle; each page documents the contract it needs.