@uncinq/css-components 1.8.0 → 1.9.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.
@@ -1,6 +1,9 @@
1
1
  /* components/button.css */
2
2
  @layer components {
3
3
  .btn {
4
+ --icon-margin-inline-start: 0;
5
+ --icon-size: var(--btn-font-size);
6
+
4
7
  align-items: center;
5
8
  appearance: none;
6
9
  background-color: var(--btn-color-background);
@@ -1,106 +1,51 @@
1
1
  /* components/drawer.css */
2
2
  /*
3
- * .drawer — slide-in panel, vanilla JS driven.
3
+ * .drawer — panel sliding in from an edge.
4
4
  *
5
- * JS (components/drawer.js) manages:
6
- * .showing — panel becoming visible (open transition)
7
- * .show — panel fully open
8
- * .hiding — panel becoming hidden (close transition)
9
- * aria-hidden="true" when closed
10
- * body.drawer-open when panel is open
5
+ * A variant is pure configuration: this file declares nothing but --panel-*
6
+ * tokens. components/panel.css owns every rule — structure, placement,
7
+ * open/closed contract and backdrop.
11
8
  *
12
- * Triggers: .js-drawer-toggle[data-target="#id"]
13
- * Close: .js-drawer-close (inside panel)
9
+ * Works on a <dialog> opened with showModal() and on any element carrying
10
+ * popover="auto", opened with showPopover(). Both land in the top layer and
11
+ * get the native ::backdrop. Reach for the second form when the panel is not
12
+ * always modal: combined with .panel-inline-* it becomes a region of the
13
+ * page above a breakpoint, which a <dialog> cannot be without claiming
14
+ * role="dialog" the whole time.
14
15
  *
15
16
  * Position variants: .drawer-start | .drawer-end | .drawer-top | .drawer-bottom
17
+ * Inline variants: .panel-inline-* — see components/panel.css
16
18
  *
17
- * Expected HTML:
18
- * <div id="navigation" class="drawer drawer-start" role="dialog" aria-modal="true" aria-hidden="true">
19
- * <div class="header">
20
- * <span class="title">...</span>
21
- * <button class="js-drawer-close btn">...</button>
22
- * </div>
23
- * <div class="content">...</div>
24
- * </div>
19
+ * Triggers: .js-dialog-toggle[data-target="#id"]
20
+ * Close: .js-dialog-close inside the panel, Escape, or a click outside
25
21
  */
26
22
  @layer components {
27
-
28
- /* ── Panel ───────────────────────────────────────────────────────── */
29
-
30
23
  .drawer {
31
- --color-link: var(--drawer-color-text);
32
- --nav-color-text: var(--color-link);
33
-
34
- background-color: var(--drawer-color-background);
35
- bottom: var(--drawer-bottom, 0);
36
- box-shadow: var(--drawer-shadow);
37
- color: var(--drawer-color-text);
38
- display: flex;
39
- flex-direction: column;
40
- inset-inline-start: var(--drawer-start, auto);
41
- inset-inline-end: var(--drawer-end, 0);
42
- max-width: 100%;
43
- overflow: hidden;
44
- position: fixed;
45
- top: var(--drawer-top, 0);
46
- transform: var(--drawer-translate, translateX(100%));
47
- visibility: hidden;
48
- width: var(--drawer-width);
49
- z-index: var(--drawer-z-index);
50
-
51
- @media (prefers-reduced-motion: no-preference) {
52
- transition-duration: var(--drawer-transition-duration);
53
- transition-property: transform, visibility;
54
- transition-timing-function: var(--drawer-transition-easing);
55
- }
56
-
57
- /* Visible during transitions and when open */
58
- &.show,
59
- &.showing,
60
- &.hiding {
61
- visibility: visible;
62
- }
63
-
64
- /* Fully open — remove transform */
65
- &.show {
66
- transform: none;
67
- }
68
-
69
- /* ── Header ────────────────────────────────────────────────────── */
70
-
71
- & > .header {
72
- align-items: center;
73
- border-block-end: var(--drawer-border-width) var(--drawer-border-style) var(--drawer-border-color);
74
- display: flex;
75
- flex-shrink: 0;
76
- justify-content: space-between;
77
- padding-block: var(--drawer-header-padding-block, var(--drawer-padding-block));
78
- padding-inline: var(--drawer-header-padding-inline, var(--drawer-padding-inline));
79
-
80
- & .title {
81
- font-size: var(--font-size-md);
82
- font-weight: var(--font-weight-heading);
83
- margin: 0;
84
- }
85
- }
86
-
87
- /* ── Body ──────────────────────────────────────────────────────── */
88
-
89
- & > .content {
90
- flex-grow: 1;
91
- overflow-y: auto;
92
- padding-block: var(--drawer-header-padding-block, var(--drawer-padding-block));
93
- padding-inline: var(--drawer-header-padding-inline, var(--drawer-padding-inline));
94
- }
95
-
96
- /* ── Footer ──────────────────────────────────────────────────────── */
97
-
98
- & > .footer {
99
- border-block-start: var(--drawer-border-width) var(--drawer-border-style) var(--drawer-border-color);
100
- flex-shrink: 0;
101
- padding-block: var(--drawer-header-padding-block, var(--drawer-padding-block));
102
- padding-inline: var(--drawer-header-padding-inline, var(--drawer-padding-inline));
103
- }
24
+ /* Structure */
25
+ --panel-backdrop-blur: var(--drawer-backdrop-blur);
26
+ --panel-backdrop-color: var(--drawer-backdrop-color);
27
+ --panel-border-style: var(--drawer-border-style);
28
+ --panel-border-width: var(--drawer-border-width);
29
+ --panel-color-background: var(--drawer-color-background);
30
+ --panel-color-border: var(--drawer-color-border);
31
+ --panel-color-text: var(--drawer-color-text);
32
+ --panel-content-padding-block: var(--drawer-content-padding-block, var(--drawer-padding-block));
33
+ --panel-content-padding-inline: var(--drawer-content-padding-inline, var(--drawer-padding-inline));
34
+ --panel-footer-padding-block: var(--drawer-footer-padding-block, var(--drawer-padding-block));
35
+ --panel-footer-padding-inline: var(--drawer-footer-padding-inline, var(--drawer-padding-inline));
36
+ --panel-header-padding-block: var(--drawer-header-padding-block, var(--drawer-padding-block));
37
+ --panel-header-padding-inline: var(--drawer-header-padding-inline, var(--drawer-padding-inline));
38
+ --panel-shadow: var(--drawer-shadow);
39
+ --panel-transition-duration: var(--drawer-transition-duration);
40
+ --panel-transition-easing: var(--drawer-transition-easing);
41
+
42
+ /* Placement — two edges pinned, the opposite ones auto. The margin stays
43
+ at 0 so the UA rules that centre a modal <dialog> do not apply. */
44
+ --panel-inline-size: var(--drawer-width);
45
+ --panel-inset-block: var(--drawer-top) var(--drawer-bottom);
46
+ --panel-inset-inline: var(--drawer-start) var(--drawer-end);
47
+ --panel-max-inline-size: 100%;
48
+ --panel-translate: var(--drawer-translate);
104
49
  }
105
50
 
106
51
  /* ── Position variants ───────────────────────────────────────────── */
@@ -112,80 +57,28 @@
112
57
  }
113
58
 
114
59
  .drawer-end {
115
- --drawer-start: auto;
116
60
  --drawer-end: 0;
61
+ --drawer-start: auto;
117
62
  --drawer-translate: translateX(100%);
118
63
  }
119
64
 
120
65
  .drawer-top {
66
+ --drawer-bottom: auto;
121
67
  --drawer-end: 0;
122
68
  --drawer-start: 0;
123
69
  --drawer-top: 0;
124
70
  --drawer-translate: translateY(-100%);
125
- height: var(--drawer-height);
71
+ --panel-block-size: var(--drawer-height);
72
+ --panel-inline-size: auto;
126
73
  }
127
74
 
128
75
  .drawer-bottom {
76
+ --drawer-bottom: 0;
129
77
  --drawer-end: 0;
130
78
  --drawer-start: 0;
131
- --drawer-bottom: 0;
79
+ --drawer-top: auto;
132
80
  --drawer-translate: translateY(100%);
133
- height: var(--drawer-height);
134
- }
135
-
136
- /* ── Responsive variants (static above breakpoint) ──────────────── */
137
-
138
- .drawer-tablet {
139
- @media (--tablet) {
140
- box-shadow: none;
141
- overflow: visible;
142
- position: static;
143
- transform: none;
144
- transition: none;
145
- visibility: visible;
146
- width: auto;
147
- z-index: auto;
148
- }
149
- }
150
-
151
- .drawer-laptop {
152
- @media (--laptop) {
153
- box-shadow: none;
154
- overflow: visible;
155
- position: static;
156
- transform: none;
157
- transition: none;
158
- visibility: visible;
159
- width: auto;
160
- z-index: auto;
161
- }
162
- }
163
-
164
- /* ── Backdrop ────────────────────────────────────────────────────── */
165
-
166
- .drawer-backdrop {
167
- backdrop-filter: blur(var(--drawer-backdrop-blur));
168
- background-color: var(--drawer-backdrop-color);
169
- inset: 0;
170
- position: fixed;
171
- z-index: calc(var(--drawer-z-index) - 1);
172
-
173
- &.fade {
174
- opacity: 0;
175
-
176
- @media (prefers-reduced-motion: no-preference) {
177
- transition: opacity var(--drawer-transition-duration) var(--drawer-transition-easing);
178
- }
179
- }
180
-
181
- &.show {
182
- opacity: 1;
183
- }
184
- }
185
-
186
- /* ── Body scroll lock ────────────────────────────────────────────── */
187
-
188
- body.drawer-open {
189
- overflow: hidden;
81
+ --panel-block-size: var(--drawer-height);
82
+ --panel-inline-size: auto;
190
83
  }
191
84
  }
@@ -79,6 +79,7 @@
79
79
  list-style: none;
80
80
  min-width: var(--dropdown-min-width);
81
81
  opacity: 0;
82
+ overflow: hidden;
82
83
  padding: var(--dropdown-padding, 0);
83
84
  pointer-events: none;
84
85
  position: absolute;
@@ -13,7 +13,7 @@
13
13
  margin-block: var(--map-margin-block);
14
14
  overflow: hidden;
15
15
 
16
- @media not (--tablet) {
16
+ @media not (--sm) {
17
17
  --map-ratio: var(--map-ratio-mobile);
18
18
  .container & {
19
19
  margin-inline: calc(-1 * var(--gutter));
@@ -1,151 +1,59 @@
1
1
  /* components/modal.css */
2
2
  /*
3
- * .modal — centered dialog, native <dialog>.
3
+ * .modal — centred panel.
4
4
  *
5
- * JS (components/modal.js) only calls showModal() / close(): [open] and
6
- * ::backdrop drive the styling, and the browser handles Escape, the focus trap
7
- * and making the rest of the page inert. No state class, no backdrop element.
5
+ * A variant is pure configuration: this file declares nothing but --panel-*
6
+ * tokens. components/panel.css owns every rule — structure, placement,
7
+ * open/closed contract and backdrop.
8
8
  *
9
- * The dialog sits in the top layer, so it needs no z-index.
10
- *
11
- * Triggers: .js-dialog-toggle[data-target="#id"]
12
- * Close: .js-dialog-close (inside the panel), click outside the panel
9
+ * Works on a <dialog> opened with showModal() and on any element carrying
10
+ * popover="auto", opened with showPopover(). Both land in the top layer and
11
+ * get the native ::backdrop. Reach for the second form only when the panel is not
12
+ * always modal, typically when .panel-inline-* turns it into a region of the
13
+ * page above a breakpoint.
13
14
  *
14
15
  * Per-instance sizing: override --modal-max-width on the element.
15
16
  *
16
- * Expected HTML:
17
- * <dialog id="navigation" class="modal" aria-labelledby="navigationLabel" aria-modal="true">
18
- * <div class="header">
19
- * <p class="title" id="navigationLabel">...</p>
20
- * <button class="btn-close js-dialog-close">...</button>
21
- * </div>
22
- * <div class="content">...</div>
23
- * <div class="footer">...</div>
24
- * </dialog>
17
+ * Triggers: .js-dialog-toggle[data-target="#id"]
18
+ * Close: .js-dialog-close inside the panel, Escape, or a click outside
25
19
  */
26
20
  @layer components {
27
-
28
- /* ── Panel ───────────────────────────────────────────────────────── */
29
-
30
21
  .modal {
31
- background-color: var(--modal-color-background);
32
- border: 0;
33
- border-radius: var(--modal-radius);
34
- box-shadow: var(--modal-shadow);
35
- color: var(--modal-color-text);
36
- display: flex;
37
- flex-direction: column;
38
- inline-size: 100%;
22
+ /* Structure */
23
+ --panel-backdrop-blur: var(--modal-backdrop-blur);
24
+ --panel-backdrop-color: var(--modal-backdrop-color);
25
+ --panel-border-style: var(--modal-border-style);
26
+ --panel-border-width: var(--modal-border-width);
27
+ --panel-color-background: var(--modal-color-background);
28
+ --panel-color-border: var(--modal-color-border);
29
+ --panel-color-text: var(--modal-color-text);
30
+ --panel-content-padding-block: var(--modal-content-padding-block, var(--modal-padding-block));
31
+ --panel-content-padding-inline: var(--modal-content-padding-inline, var(--modal-padding-inline));
32
+ --panel-footer-padding-block: var(--modal-footer-padding-block, var(--modal-padding-block));
33
+ --panel-footer-padding-inline: var(--modal-footer-padding-inline, var(--modal-padding-inline));
34
+ --panel-header-padding-block: var(--modal-header-padding-block, var(--modal-padding-block));
35
+ --panel-header-padding-inline: var(--modal-header-padding-inline, var(--modal-padding-inline));
36
+ --panel-radius: var(--modal-radius);
37
+ --panel-shadow: var(--modal-shadow);
38
+ --panel-transition-duration: var(--modal-transition-duration);
39
+ --panel-transition-easing: var(--modal-transition-easing);
40
+
41
+ /* Placement — inset 0 with auto margins centres the panel on both axes.
42
+ The height is fit-content and not auto: with both block insets set, an
43
+ auto height is resolved to fill the gap and the auto margins collapse to
44
+ 0, so the panel would neither centre nor shrink to its content. The UA
45
+ dialog stylesheet uses fit-content for that very reason. */
46
+ --panel-block-size: fit-content;
47
+ --panel-inline-size: 100%;
48
+ --panel-inset-block: 0;
49
+ --panel-inset-inline: 0;
50
+ --panel-margin: auto;
39
51
  /* min() replaces the mobile breakpoint: the panel always keeps
40
52
  --modal-margin clear of the viewport edges, whatever the max is set to */
41
- max-block-size: min(var(--modal-max-height), 100% - var(--modal-margin) * 2);
42
- max-inline-size: min(var(--modal-max-width), 100% - var(--modal-margin) * 2);
43
- opacity: 0;
44
- /* padding lives on the sections, so a click on the dialog itself is
45
- always a click on the backdrop */
46
- overflow: hidden;
47
- padding: 0;
48
- transform: var(--modal-translate);
49
-
50
- /* display: flex would override the UA rule that hides a closed dialog */
51
- &:not([open]) {
52
- display: none;
53
- }
54
-
55
- /* Fully open — settle the panel */
56
- &[open] {
57
- opacity: 1;
58
- transform: none;
59
- }
60
-
61
- /* allow-discrete keeps the dialog in the top layer while it fades out */
62
- @media (prefers-reduced-motion: no-preference) {
63
- transition-behavior: allow-discrete;
64
- transition-duration: var(--modal-transition-duration);
65
- transition-property: opacity, transform, display, overlay;
66
- transition-timing-function: var(--modal-transition-easing);
67
- }
68
-
69
- /* ── Header ────────────────────────────────────────────────────── */
70
-
71
- & > .header {
72
- align-items: center;
73
- border-block-end: var(--modal-border-width) var(--modal-border-style) var(--modal-border-color);
74
- display: flex;
75
- flex-shrink: 0;
76
- justify-content: space-between;
77
- padding-block: var(--modal-header-padding-block, var(--modal-padding-block));
78
- padding-inline: var(--modal-header-padding-inline, var(--modal-padding-inline));
79
-
80
- & .title {
81
- font-size: var(--font-size-md);
82
- font-weight: var(--font-weight-heading);
83
- margin: 0;
84
- }
85
- }
86
-
87
- /* ── Body ──────────────────────────────────────────────────────── */
88
-
89
- & > .content {
90
- flex-grow: 1;
91
- overflow-y: auto;
92
- overscroll-behavior: contain;
93
- padding-block: var(--modal-content-padding-block, var(--modal-padding-block));
94
- padding-inline: var(--modal-content-padding-inline, var(--modal-padding-inline));
95
- }
96
-
97
- /* ── Footer ────────────────────────────────────────────────────── */
98
-
99
- & > .footer {
100
- border-block-start:var(--modal-border-width) var(--modal-border-style) var(--modal-border-color);
101
- display: flex;
102
- flex-direction: column;
103
- flex-shrink: 0;
104
- gap: var(--spacing-sm);
105
- padding-block: var(--modal-footer-padding-block, var(--modal-padding-block));
106
- padding-inline: var(--modal-footer-padding-inline, var(--modal-padding-inline));
107
-
108
- @media (--tablet) {
109
- flex-direction: row;
110
- justify-content: flex-end;
111
- }
112
- }
113
- }
114
-
115
- /* ── Backdrop ────────────────────────────────────────────────────── */
116
-
117
- .modal::backdrop {
118
- backdrop-filter: blur(var(--modal-backdrop-blur));
119
- background-color: var(--modal-backdrop-color);
120
- opacity: 0;
121
-
122
- @media (prefers-reduced-motion: no-preference) {
123
- transition-behavior: allow-discrete;
124
- transition-duration: var(--modal-transition-duration);
125
- transition-property: opacity, display, overlay;
126
- transition-timing-function: var(--modal-transition-easing);
127
- }
128
- }
129
-
130
- .modal[open]::backdrop {
131
- opacity: 1;
132
- }
133
-
134
- /* Opening is a discrete display change — both need an explicit start state */
135
- @starting-style {
136
- .modal[open] {
137
- opacity: 0;
138
- transform: var(--modal-translate);
139
- }
140
-
141
- .modal[open]::backdrop {
142
- opacity: 0;
143
- }
144
- }
145
-
146
- /* ── Page scroll lock ────────────────────────────────────────────── */
147
-
148
- body:has(.modal[open]) {
149
- overflow: hidden;
53
+ --panel-max-block-size: min(var(--modal-max-height), 100% - var(--modal-margin) * 2);
54
+ --panel-max-inline-size: min(var(--modal-max-width), 100% - var(--modal-margin) * 2);
55
+ --panel-min-block-size: var(--modal-min-height);
56
+ --panel-opacity: 0;
57
+ --panel-translate: var(--modal-translate);
150
58
  }
151
59
  }
@@ -25,7 +25,9 @@
25
25
  gap: var(--nav-gap);
26
26
  list-style: var(--nav-list-style, none);
27
27
  margin-block: var(--nav-margin-block, 0);
28
- padding-inline-start: var(--nav-padding-inline-start, 0);
28
+ margin-inline: var(--nav-margin-inline);
29
+ padding-block: var(--nav-padding-block);
30
+ padding-inline: var(--nav-padding-inline, 0);
29
31
  }
30
32
 
31
33
  /* ── Nav item ────────────────────────────────────────────────── */
@@ -38,27 +40,22 @@
38
40
 
39
41
  /* ── Links ───────────────────────────────────────────────────── */
40
42
 
41
- a {
43
+ a:where(:not(.btn)),
44
+ li > span {
45
+ color: var(--nav-color-text);
42
46
  display: block;
43
47
  padding-block: var(--nav-link-padding-block);
44
48
  padding-inline: var(--nav-link-padding-inline);
45
49
  text-transform: var(--nav-link-text-transform);
46
- }
47
-
48
- a:not(.btn) {
49
- color: var(--nav-color-text);
50
50
  &.active {
51
51
  --nav-color-text: var(--nav-color-text-active);
52
52
  }
53
53
  }
54
54
 
55
- /* Non-link items (current page rendered as span) */
56
- li > span {
55
+ /* ── Dropdown ───────────────────────────────────────────────────── */
56
+
57
+ .dropdown-toggle {
57
58
  color: var(--nav-color-text);
58
- display: block;
59
- padding-block: var(--nav-link-padding-block);
60
- padding-inline: var(--nav-link-padding-inline);
61
- text-transform: var(--nav-link-text-transform);
62
59
  }
63
60
 
64
61
  /* ── Item highlighted ───────────────────────────────────────────────────── */
@@ -91,7 +91,7 @@
91
91
  }
92
92
 
93
93
  /* On small screens: hide first/last controls and non-adjacent page numbers */
94
- @media not (--tablet) {
94
+ @media not (--sm) {
95
95
  .first,
96
96
  .last,
97
97
  li:has(.first),
@@ -0,0 +1,432 @@
1
+ /* components/panel.css */
2
+ /*
3
+ * Shared skeleton for .modal and .drawer.
4
+ *
5
+ * Both are the same panel — header / content / footer, same open/closed
6
+ * contract — and differ only in where they sit: modal.css centres them,
7
+ * drawer.css sticks them to an edge.
8
+ *
9
+ * The tag picks the strategy, not the class. Either variant can be:
10
+ * - <dialog> opened with showModal() — [open], native ::backdrop, top layer,
11
+ * and the browser handles Escape, the focus trap and inertness;
12
+ * - any element carrying popover="auto", opened by components/dialog.js
13
+ * with showPopover() — same top layer, same ::backdrop, but no role of its
14
+ * own. Use this when the panel becomes a static region of the page above a
15
+ * breakpoint (see .panel-inline-*): a <dialog> is exposed as role="dialog"
16
+ * even when it is only styled as a sidebar, whereas dialog.js sets that
17
+ * role solely while the panel is open.
18
+ *
19
+ * Most selectors here are :where()-wrapped so they carry no specificity: the
20
+ * variants and the host components always win without escalating. The
21
+ * .panel-inline-* variants are the exception — they must beat .modal and
22
+ * .drawer, which they do on source order, so this file is imported last.
23
+ *
24
+ * Structure reads --panel-* tokens; each variant aliases them from its own
25
+ * namespace, so --modal-* and --drawer-* stay the public API.
26
+ *
27
+ * Expected HTML:
28
+ * <dialog class="modal" id="x" aria-labelledby="xLabel">
29
+ * <div class="header">
30
+ * <p class="title" id="xLabel">...</p>
31
+ * <button class="btn-close js-dialog-close"></button>
32
+ * </div>
33
+ * <div class="content">...</div>
34
+ * <div class="footer">...</div>
35
+ * </dialog>
36
+ */
37
+ @layer components {
38
+
39
+ /* ── Open / closed ─────────────────────────────────────────────────
40
+ * `display` is only ever declared on a state selector: declaring it on the
41
+ * panel itself would beat the UA rule that hides a closed <dialog>. Both
42
+ * rules carry no specificity, so .panel-inline-* can put the panel back
43
+ * into the flow with a single class. */
44
+
45
+ :where(.modal, .drawer):not(:where([open], :popover-open)) {
46
+ display: none;
47
+ }
48
+
49
+ /* :is() and not :where() on the state: this has to beat the base rule below,
50
+ which is 0,0,0 and comes later in the file. 0,1,0 wins on specificity
51
+ rather than on source order, and still leaves a host component — .toc
52
+ .drawer is 0,2,0 — room to override without escalating. */
53
+ :where(.modal, .drawer):is([open], :popover-open) {
54
+ display: flex;
55
+ opacity: 1;
56
+ transform: none;
57
+ }
58
+
59
+ /* ── Panel ─────────────────────────────────────────────────────── */
60
+
61
+ :where(.modal, .drawer) {
62
+ --color-link: var(--panel-color-text);
63
+ --color-link-hover: var(--panel-color-text);
64
+ --nav-color-text: var(--color-link);
65
+
66
+ background-color: var(--panel-color-background);
67
+ border: 0;
68
+ border-radius: var(--panel-radius, 0);
69
+ box-shadow: var(--panel-shadow);
70
+ color: var(--panel-color-text);
71
+ flex-direction: column;
72
+ overflow: hidden;
73
+ /* padding lives on the sections, so a click on the panel itself is
74
+ always a click on the backdrop */
75
+ padding: 0;
76
+
77
+ /* Placement — every value comes from the variant. A modal centres itself
78
+ with inset 0 + margin auto; a drawer pins two edges and leaves the
79
+ others auto. Declaring position/inset explicitly also covers <dialog>,
80
+ whose UA rules would otherwise centre an edge panel. */
81
+ block-size: var(--panel-block-size, auto);
82
+ inline-size: var(--panel-inline-size, auto);
83
+ inset-block: var(--panel-inset-block, auto);
84
+ inset-inline: var(--panel-inset-inline, auto);
85
+ margin: var(--panel-margin, 0);
86
+ max-block-size: var(--panel-max-block-size, none);
87
+ max-inline-size: var(--panel-max-inline-size, none);
88
+ min-block-size: var(--panel-min-block-size, auto);
89
+ /* Closed state, so also the values the panel animates FROM — the
90
+ @starting-style block below restates these two. A drawer leaves opacity
91
+ alone and only slides; a modal sets 0 and fades as well. */
92
+ opacity: var(--panel-opacity, 1);
93
+ position: fixed;
94
+ transform: var(--panel-translate, none);
95
+
96
+ /* allow-discrete keeps the panel displayed while it transitions out, and
97
+ keeps a <dialog> in the top layer until the transition ends */
98
+ @media (prefers-reduced-motion: no-preference) {
99
+ transition-behavior: allow-discrete;
100
+ transition-duration: var(--panel-transition-duration);
101
+ transition-property: display, opacity, overlay, transform;
102
+ transition-timing-function: var(--panel-transition-easing);
103
+ }
104
+
105
+ /* ── Header ──────────────────────────────────────────────────── */
106
+
107
+ & > .header {
108
+ align-items: center;
109
+ border-block-end: var(--panel-border-width) var(--panel-border-style) var(--panel-color-border);
110
+ display: flex;
111
+ flex-shrink: 0;
112
+ justify-content: space-between;
113
+ padding-block: var(--panel-header-padding-block);
114
+ padding-inline: var(--panel-header-padding-inline);
115
+
116
+ & .title {
117
+ font-size: var(--font-size-md);
118
+ font-weight: var(--font-weight-heading);
119
+ margin: 0;
120
+ }
121
+ }
122
+
123
+ /* ── Body ────────────────────────────────────────────────────── */
124
+
125
+ & > .content {
126
+ flex-grow: 1;
127
+ overflow-y: auto;
128
+ overscroll-behavior: contain;
129
+ padding-block: var(--panel-content-padding-block);
130
+ padding-inline: var(--panel-content-padding-inline);
131
+ }
132
+
133
+ /* ── Footer ──────────────────────────────────────────────────── */
134
+
135
+ & > .footer {
136
+ border-block-start: var(--panel-border-width) var(--panel-border-style) var(--panel-color-border);
137
+ display: flex;
138
+ flex-direction: column;
139
+ flex-shrink: 0;
140
+ gap: var(--spacing-sm);
141
+ padding-block: var(--panel-footer-padding-block);
142
+ padding-inline: var(--panel-footer-padding-inline);
143
+
144
+ @media (--sm) {
145
+ flex-direction: row;
146
+ justify-content: flex-end;
147
+ }
148
+ }
149
+ }
150
+
151
+ /* ── Inline variants ───────────────────────────────────────────────
152
+ * Above the chosen breakpoint the panel stops being an overlay and becomes
153
+ * a region of the page: the host component then only styles what is its
154
+ * own (sticky behaviour, labels, nav layout). Written once here, so adding
155
+ * a property to .modal or .drawer means neutralising it right below rather
156
+ * than patching every host component.
157
+ *
158
+ * The panel must NOT be a <dialog> when one of these is used — see the
159
+ * strategy note at the top of this file. components/dialog.js also closes
160
+ * an open panel carrying one of these classes when the viewport width
161
+ * changes, since CSS alone cannot release the backdrop, the scroll lock
162
+ * and the inert background. */
163
+
164
+ .panel-inline-sm {
165
+ @media (--sm) {
166
+ /* Colours go too, not just geometry: a region of the page carries none
167
+ of the panel's chrome and takes the page's own text and link colours.
168
+ `inherit` on a custom property hands it back to the parent, which is
169
+ exactly what is wanted here — and it has to be done token by token.
170
+ Resetting --panel-color-text, which the three derive from, would make
171
+ them invalid at computed-value time rather than page-valued, and every
172
+ link would fall back to the surrounding text colour instead of the
173
+ site's link colour. Keep this list in step with the panel rule above. */
174
+ --color-link: inherit;
175
+ --color-link-hover: inherit;
176
+ --nav-color-text: inherit;
177
+
178
+ background-color: transparent;
179
+ block-size: auto;
180
+ border-radius: 0;
181
+ box-shadow: none;
182
+ color: inherit;
183
+ display: flex;
184
+ inline-size: auto;
185
+ inset-block: auto;
186
+ inset-inline: auto;
187
+ margin: 0;
188
+ max-block-size: none;
189
+ max-inline-size: none;
190
+ min-block-size: auto;
191
+ opacity: 1;
192
+ overflow: visible;
193
+ position: static;
194
+ transform: none;
195
+ transition: none;
196
+
197
+ > .header,
198
+ > .footer {
199
+ display: none;
200
+ }
201
+
202
+ > .content {
203
+ flex-grow: 0;
204
+ overflow-y: visible;
205
+ overscroll-behavior: auto;
206
+ padding: 0;
207
+ }
208
+ }
209
+ }
210
+
211
+ .panel-inline-md {
212
+ @media (--md) {
213
+ /* Colours go too, not just geometry: a region of the page carries none
214
+ of the panel's chrome and takes the page's own text and link colours.
215
+ `inherit` on a custom property hands it back to the parent, which is
216
+ exactly what is wanted here — and it has to be done token by token.
217
+ Resetting --panel-color-text, which the three derive from, would make
218
+ them invalid at computed-value time rather than page-valued, and every
219
+ link would fall back to the surrounding text colour instead of the
220
+ site's link colour. Keep this list in step with the panel rule above. */
221
+ --color-link: inherit;
222
+ --color-link-hover: inherit;
223
+ --nav-color-text: inherit;
224
+
225
+ background-color: transparent;
226
+ block-size: auto;
227
+ border-radius: 0;
228
+ box-shadow: none;
229
+ color: inherit;
230
+ display: flex;
231
+ inline-size: auto;
232
+ inset-block: auto;
233
+ inset-inline: auto;
234
+ margin: 0;
235
+ max-block-size: none;
236
+ max-inline-size: none;
237
+ min-block-size: auto;
238
+ opacity: 1;
239
+ overflow: visible;
240
+ position: static;
241
+ transform: none;
242
+ transition: none;
243
+
244
+ > .header,
245
+ > .footer {
246
+ display: none;
247
+ }
248
+
249
+ > .content {
250
+ flex-grow: 0;
251
+ overflow-y: visible;
252
+ overscroll-behavior: auto;
253
+ padding: 0;
254
+ }
255
+ }
256
+ }
257
+
258
+ .panel-inline-lg {
259
+ @media (--lg) {
260
+ /* Colours go too, not just geometry: a region of the page carries none
261
+ of the panel's chrome and takes the page's own text and link colours.
262
+ `inherit` on a custom property hands it back to the parent, which is
263
+ exactly what is wanted here — and it has to be done token by token.
264
+ Resetting --panel-color-text, which the three derive from, would make
265
+ them invalid at computed-value time rather than page-valued, and every
266
+ link would fall back to the surrounding text colour instead of the
267
+ site's link colour. Keep this list in step with the panel rule above. */
268
+ --color-link: inherit;
269
+ --color-link-hover: inherit;
270
+ --nav-color-text: inherit;
271
+
272
+ background-color: transparent;
273
+ block-size: auto;
274
+ border-radius: 0;
275
+ box-shadow: none;
276
+ color: inherit;
277
+ display: flex;
278
+ inline-size: auto;
279
+ inset-block: auto;
280
+ inset-inline: auto;
281
+ margin: 0;
282
+ max-block-size: none;
283
+ max-inline-size: none;
284
+ min-block-size: auto;
285
+ opacity: 1;
286
+ overflow: visible;
287
+ position: static;
288
+ transform: none;
289
+ transition: none;
290
+
291
+ > .header,
292
+ > .footer {
293
+ display: none;
294
+ }
295
+
296
+ > .content {
297
+ flex-grow: 0;
298
+ overflow-y: visible;
299
+ overscroll-behavior: auto;
300
+ padding: 0;
301
+ }
302
+ }
303
+ }
304
+
305
+ .panel-inline-xl {
306
+ @media (--xl) {
307
+ /* Colours go too, not just geometry: a region of the page carries none
308
+ of the panel's chrome and takes the page's own text and link colours.
309
+ `inherit` on a custom property hands it back to the parent, which is
310
+ exactly what is wanted here — and it has to be done token by token.
311
+ Resetting --panel-color-text, which the three derive from, would make
312
+ them invalid at computed-value time rather than page-valued, and every
313
+ link would fall back to the surrounding text colour instead of the
314
+ site's link colour. Keep this list in step with the panel rule above. */
315
+ --color-link: inherit;
316
+ --color-link-hover: inherit;
317
+ --nav-color-text: inherit;
318
+
319
+ background-color: transparent;
320
+ block-size: auto;
321
+ border-radius: 0;
322
+ box-shadow: none;
323
+ color: inherit;
324
+ display: flex;
325
+ inline-size: auto;
326
+ inset-block: auto;
327
+ inset-inline: auto;
328
+ margin: 0;
329
+ max-block-size: none;
330
+ max-inline-size: none;
331
+ min-block-size: auto;
332
+ opacity: 1;
333
+ overflow: visible;
334
+ position: static;
335
+ transform: none;
336
+ transition: none;
337
+
338
+ > .header,
339
+ > .footer {
340
+ display: none;
341
+ }
342
+
343
+ > .content {
344
+ flex-grow: 0;
345
+ overflow-y: visible;
346
+ overscroll-behavior: auto;
347
+ padding: 0;
348
+ }
349
+ }
350
+ }
351
+
352
+ /* ── Trigger ───────────────────────────────────────────────────────
353
+ * Once the panel has gone inline there is nothing left to open, so its
354
+ * trigger goes with it. Kept here rather than restated by every host.
355
+ *
356
+ * Only reaches a trigger that is the panel's own previous sibling: adjacency
357
+ * is what proves intent, so any button there qualifies. A trigger placed
358
+ * further away — inside the heading that precedes the panel, as the filters
359
+ * do — is not covered and stays visible. */
360
+
361
+ @media (--sm) {
362
+ button:has(+ .panel-inline-sm) { display: none; }
363
+ }
364
+
365
+ @media (--md) {
366
+ button:has(+ .panel-inline-md) { display: none; }
367
+ }
368
+
369
+ @media (--lg) {
370
+ button:has(+ .panel-inline-lg) { display: none; }
371
+ }
372
+
373
+ @media (--xl) {
374
+ button:has(+ .panel-inline-xl) { display: none; }
375
+ }
376
+
377
+ /* ── Entry transition ──────────────────────────────────────────────
378
+ * Opening is a discrete display change, so the start state has to be
379
+ * spelled out. It is exactly the closed state, which now lives in tokens —
380
+ * one block covers both variants, panel and ::backdrop alike. */
381
+
382
+ @starting-style {
383
+ :where(.modal, .drawer):is([open], :popover-open) {
384
+ opacity: var(--panel-opacity, 1);
385
+ transform: var(--panel-translate, none);
386
+ }
387
+ }
388
+
389
+ /* ── Backdrop ──────────────────────────────────────────────────────
390
+ * Both paths put the panel in the top layer — showModal() for a <dialog>,
391
+ * showPopover() for anything else — so both get the native ::backdrop, and
392
+ * it inherits from the panel: it reads the very --panel-* tokens the panel
393
+ * declares. No backdrop node is ever injected, and no z-index is involved.
394
+ *
395
+ * This is why the class-driven path uses popover rather than a plain
396
+ * element. A backdrop placed outside the panel sits in the root stacking
397
+ * context, while the panel stays confined to its nearest stacking ancestor
398
+ * — a sticky sidebar wrapper, a sticky header — so the veil paints over the
399
+ * panel and no z-index can lift it out. Placing the backdrop next to the
400
+ * panel only moves the problem: it then inherits the same confinement and
401
+ * anything painted later shows through it. The top layer removes the
402
+ * question entirely, which is exactly what it exists for. */
403
+
404
+ :where(.modal, .drawer)::backdrop {
405
+ backdrop-filter: blur(var(--panel-backdrop-blur));
406
+ background-color: var(--panel-backdrop-color);
407
+ opacity: 0;
408
+
409
+ @media (prefers-reduced-motion: no-preference) {
410
+ transition-behavior: allow-discrete;
411
+ transition-duration: var(--panel-transition-duration);
412
+ transition-property: display, opacity, overlay;
413
+ transition-timing-function: var(--panel-transition-easing);
414
+ }
415
+ }
416
+
417
+ :where(.modal, .drawer):is([open], :popover-open)::backdrop {
418
+ opacity: 1;
419
+ }
420
+
421
+ @starting-style {
422
+ :where(.modal, .drawer):is([open], :popover-open)::backdrop {
423
+ opacity: 0;
424
+ }
425
+ }
426
+
427
+ /* ── Page scroll lock ────────────────────────────────────────────── */
428
+
429
+ body:has(:where(.modal, .drawer):where([open], :popover-open)) {
430
+ overflow: hidden;
431
+ }
432
+ }
package/css/index.css CHANGED
@@ -24,3 +24,4 @@
24
24
  @import 'components/nav-accessibility.css';
25
25
  @import 'components/nav-title.css';
26
26
  @import 'components/pagination.css';
27
+ @import 'components/panel.css';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uncinq/css-components",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Framework-agnostic CSS component implementations.",
5
5
  "license": "MIT",
6
6
  "repository": {