@cawalch/porchlight 0.1.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 (47) hide show
  1. package/README.md +28 -0
  2. package/dist/porchlight.css +3765 -0
  3. package/dist/porchlight.min.css +1 -0
  4. package/package.json +59 -0
  5. package/porchlight.css +62 -0
  6. package/src/00-layer-order.css +22 -0
  7. package/src/01-reset.css +53 -0
  8. package/src/02-tokens.css +254 -0
  9. package/src/03-themes.css +79 -0
  10. package/src/04-base.css +78 -0
  11. package/src/05-layout.css +209 -0
  12. package/src/06-components/accordion.css +161 -0
  13. package/src/06-components/alert.css +102 -0
  14. package/src/06-components/avatar.css +112 -0
  15. package/src/06-components/badge.css +73 -0
  16. package/src/06-components/breadcrumb.css +111 -0
  17. package/src/06-components/button.css +180 -0
  18. package/src/06-components/card.css +186 -0
  19. package/src/06-components/chip.css +146 -0
  20. package/src/06-components/command-palette.css +201 -0
  21. package/src/06-components/data-table.css +380 -0
  22. package/src/06-components/dialog.css +148 -0
  23. package/src/06-components/drawer.css +137 -0
  24. package/src/06-components/dropdown.css +180 -0
  25. package/src/06-components/empty-state.css +85 -0
  26. package/src/06-components/field.css +125 -0
  27. package/src/06-components/file-upload.css +104 -0
  28. package/src/06-components/nav.css +185 -0
  29. package/src/06-components/pagination.css +106 -0
  30. package/src/06-components/popover-menu.css +146 -0
  31. package/src/06-components/progress.css +77 -0
  32. package/src/06-components/reveal.css +73 -0
  33. package/src/06-components/scroll-progress.css +73 -0
  34. package/src/06-components/segmented.css +113 -0
  35. package/src/06-components/skeleton.css +73 -0
  36. package/src/06-components/stat.css +107 -0
  37. package/src/06-components/stepper.css +172 -0
  38. package/src/06-components/switch.css +138 -0
  39. package/src/06-components/tabs.css +164 -0
  40. package/src/06-components/tag-input.css +77 -0
  41. package/src/06-components/textarea-auto.css +77 -0
  42. package/src/06-components/timeline.css +129 -0
  43. package/src/06-components/toast.css +175 -0
  44. package/src/06-components/toolbar.css +87 -0
  45. package/src/06-components/tooltip.css +104 -0
  46. package/src/07-utilities.css +77 -0
  47. package/src/08-enhancements.css +129 -0
@@ -0,0 +1,380 @@
1
+ /*
2
+ * Porchlight - data table component
3
+ * ===========================================================================
4
+ * An enterprise data table: sticky headers, horizontal scroll with a stable
5
+ * scrollbar gutter, container-query padding, hover/selection affordances,
6
+ * sortable columns, row selection (checkbox column), expandable detail rows,
7
+ * sticky first column, and density modes.
8
+ *
9
+ * Structure: .c-table-wrap (the scroll container + query container) wraps a
10
+ * native <table class="c-table">. The wrap handles overflow + the gutter; the
11
+ * table sets column min-widths and cell geometry. Sticky headers stay pinned
12
+ * while the body scrolls.
13
+ *
14
+ * Density via [data-density] on .c-table-wrap:
15
+ * compact: tighter rows for high information density
16
+ * comfortable (default): standard spacing
17
+ */
18
+ @layer porchlight.components {
19
+ @scope (.c-table-wrap) {
20
+ :scope {
21
+ --c-table-cell-pad: var(--pl-space-3) var(--pl-space-4);
22
+ --c-table-min: 48rem;
23
+
24
+ /* Consistent row height (touch-safe 48px default). Density modes
25
+ shrink this via the compact density override below. */
26
+ --c-table-row-min-block-size: 3rem;
27
+
28
+ /* Expose the wrap radius as a token so corner cells can inherit it
29
+ exactly (minus 1px for the border). */
30
+ --c-table-radius: var(--pl-radius-xl);
31
+
32
+ container: c-table-wrap / inline-size;
33
+ overflow: auto;
34
+
35
+ /* scrollbar-gutter: stable is intentionally omitted here.
36
+ "stable" unconditionally reserves inline-end space for the scrollbar
37
+ even when no scrollbar is present, which shows as a visible gap
38
+ between the last column and the right border. overflow: auto handles
39
+ layout-shift prevention on its own when the scrollbar actually appears. */
40
+ border: 1px solid color-mix(
41
+ in oklab,
42
+ var(--pl-color-border),
43
+ transparent 20%
44
+ );
45
+ border-radius: var(--c-table-radius);
46
+ background: var(--pl-color-surface);
47
+ }
48
+
49
+ /* Density modes. */
50
+ :scope[data-density="compact"] {
51
+ --c-table-cell-pad: var(--pl-space-1) var(--pl-space-3);
52
+ --c-table-row-min-block-size: 2.25rem;
53
+ }
54
+
55
+ .c-table {
56
+ inline-size: 100%;
57
+ min-inline-size: var(--c-table-min);
58
+ border-collapse: separate;
59
+ border-spacing: 0;
60
+ }
61
+
62
+ /* Corner radius: sticky <th> elements paint above the overflow container's
63
+ border-radius clip in WebKit/Blink, so their background bleeds into the
64
+ rounded corner making it appear square. Each corner cell gets an explicit
65
+ border-radius that matches the wrap (minus 1px for the wrap border).
66
+ This is the canonical fix for the "one side rounded, one side square"
67
+ artifact on bordered tables with sticky headers. */
68
+ .c-table thead tr:first-child th:first-child {
69
+ border-start-start-radius: calc(var(--c-table-radius) - 1px);
70
+ }
71
+
72
+ .c-table thead tr:first-child th:last-child {
73
+ border-start-end-radius: calc(var(--c-table-radius) - 1px);
74
+ }
75
+
76
+ .c-table tbody tr:last-child td:first-child {
77
+ border-end-start-radius: calc(var(--c-table-radius) - 1px);
78
+ }
79
+
80
+ .c-table tbody tr:last-child td:last-child {
81
+ border-end-end-radius: calc(var(--c-table-radius) - 1px);
82
+ }
83
+
84
+ .c-table :where(th, td) {
85
+ padding: var(--c-table-cell-pad);
86
+ border-block-end: 1px solid var(--pl-color-border);
87
+ text-align: start;
88
+ vertical-align: middle;
89
+ white-space: nowrap;
90
+
91
+ /* Enforce consistent row height. min-block-size lets content expand
92
+ beyond this (e.g. multi-line cells) without truncation. */
93
+ min-block-size: var(--c-table-row-min-block-size);
94
+ }
95
+
96
+ .c-table :where(td) {
97
+ color: var(--pl-color-text);
98
+ }
99
+
100
+ .c-table :where(th) {
101
+ position: sticky;
102
+ inset-block-start: 0;
103
+ z-index: var(--pl-z-raised);
104
+
105
+ /* surface-2 bg distinguishes the header row from data rows without
106
+ the heavy "band" look of older table designs. */
107
+ background: var(--pl-color-surface-2);
108
+ font-size: var(--pl-text-sm);
109
+ font-weight: var(--pl-font-weight-semibold);
110
+ color: var(--pl-color-text-muted);
111
+
112
+ /* 1px (not 2px) - the surface-2/surface step does the visual separation;
113
+ the border is a hairline accent, not structural weight. */
114
+ border-block-end: 1px solid var(--pl-color-border);
115
+ }
116
+
117
+ /* Tighten padding when the wrap is narrow. */
118
+ @container c-table-wrap (inline-size < 40rem) {
119
+ .c-table :where(th, td) {
120
+ padding-inline: var(--pl-space-2);
121
+ }
122
+ }
123
+
124
+ /* Numeric columns. */
125
+ .c-table :where([data-align="end"]) {
126
+ text-align: end;
127
+ font-variant-numeric: tabular-nums;
128
+ }
129
+
130
+ .c-table :where([data-align="center"]) {
131
+ text-align: center;
132
+ }
133
+
134
+ /* ---------------------------------------------------------------
135
+ * Sortable headers
136
+ * ---------------------------------------------------------------
137
+ * Add [data-sort="asc"|"desc"] to a <th> to show a direction arrow.
138
+ * Unsortable <th> (no data-sort) shows nothing. The arrow is a
139
+ * pure-CSS triangle via border.
140
+ */
141
+ .c-table th[data-sort] {
142
+ cursor: pointer;
143
+ user-select: none;
144
+
145
+ &:hover {
146
+ color: var(--pl-color-text);
147
+ }
148
+ }
149
+
150
+ /* Sort indicator: replaced the CSS border-triangle technique with a
151
+ pseudo-element approach. The neutral state (sortable but unsorted)
152
+ shows ⇅ so users can discover sortability without having to hover.
153
+ Active states use accent color for clear directionality. */
154
+ .c-table__sort-icon {
155
+ display: inline-flex;
156
+ align-items: center;
157
+ margin-inline-start: var(--pl-space-1);
158
+ font-size: 0.7em;
159
+ color: var(--pl-color-text-muted);
160
+ opacity: 0.4;
161
+ transition:
162
+ opacity var(--pl-duration-1) var(--pl-ease-standard),
163
+ color var(--pl-duration-1) var(--pl-ease-standard);
164
+ }
165
+
166
+ /* Neutral (sortable, no active direction): hint the user this is sortable. */
167
+ .c-table__sort-icon::before {
168
+ content: "\21C5"; /* ⇅ updown arrows */
169
+ }
170
+
171
+ /* Ascending: up arrow, full opacity, accent color. */
172
+ .c-table th[data-sort="asc"] .c-table__sort-icon {
173
+ opacity: 1;
174
+ color: var(--pl-color-accent);
175
+ }
176
+
177
+ .c-table th[data-sort="asc"] .c-table__sort-icon::before {
178
+ content: "\2191"; /* ↑ */
179
+ }
180
+
181
+ /* Descending: down arrow, full opacity, accent color. */
182
+ .c-table th[data-sort="desc"] .c-table__sort-icon {
183
+ opacity: 1;
184
+ color: var(--pl-color-accent);
185
+ }
186
+
187
+ .c-table th[data-sort="desc"] .c-table__sort-icon::before {
188
+ content: "\2193"; /* ↓ */
189
+ }
190
+
191
+ /* Active sort column header: full-contrast text so the sorted column
192
+ reads as primary. Unsorted columns stay muted. */
193
+ .c-table th[data-sort="asc"],
194
+ .c-table th[data-sort="desc"] {
195
+ color: var(--pl-color-text);
196
+ }
197
+
198
+ /* ---------------------------------------------------------------
199
+ * Checkbox column (row selection)
200
+ * ---------------------------------------------------------------
201
+ * Add .c-table__check to a <th> or <td> for a narrow centered cell
202
+ * containing a native checkbox. The header checkbox is select-all.
203
+ */
204
+ .c-table .c-table__check {
205
+ inline-size: 2.5rem;
206
+ min-inline-size: 2.5rem;
207
+ text-align: center;
208
+ }
209
+
210
+ .c-table .c-table__check input[type="checkbox"] {
211
+ inline-size: 1rem;
212
+ block-size: 1rem;
213
+ accent-color: var(--pl-color-accent);
214
+ cursor: pointer;
215
+ }
216
+
217
+ /* ---------------------------------------------------------------
218
+ * Expandable detail rows
219
+ * ---------------------------------------------------------------
220
+ * A row with .c-table__detail is collapsed by default. Add [open]
221
+ * (or data-open) to expand. Uses the grid-template-rows technique
222
+ * for smooth animation without interpolate-size.
223
+ */
224
+ .c-table .c-table__detail td {
225
+ padding: 0;
226
+ border-block-end: 1px solid var(--pl-color-border);
227
+
228
+ /* Slightly deeper tint than hover so the detail row reads as
229
+ subordinate content; left accent bar ties it to its parent row. */
230
+ background: color-mix(
231
+ in oklab,
232
+ var(--pl-color-surface-2),
233
+ var(--pl-color-text) 2%
234
+ );
235
+ box-shadow: inset var(--pl-accent-bar-width) 0 0 color-mix(
236
+ in oklab,
237
+ var(--pl-color-accent),
238
+ transparent 70%
239
+ );
240
+ }
241
+
242
+ .c-table__detail-inner {
243
+ display: grid;
244
+ grid-template-rows: 0fr;
245
+ overflow: hidden;
246
+ transition: grid-template-rows var(--pl-duration-3)
247
+ var(--pl-ease-standard);
248
+ }
249
+
250
+ .c-table .c-table__detail[open] .c-table__detail-inner {
251
+ grid-template-rows: 1fr;
252
+ }
253
+
254
+ .c-table__detail-content {
255
+ overflow: hidden;
256
+ padding: var(--pl-space-4);
257
+ }
258
+
259
+ /* The expand/collapse toggle button in a row. */
260
+ .c-table__expand {
261
+ display: inline-flex;
262
+ align-items: center;
263
+ justify-content: center;
264
+ inline-size: 1.5rem;
265
+ block-size: 1.5rem;
266
+ padding: 0;
267
+ border: 0;
268
+ border-radius: var(--pl-radius-sm);
269
+ background: transparent;
270
+ color: var(--pl-color-text-muted);
271
+ cursor: pointer;
272
+ transition: background-color var(--pl-duration-1) var(--pl-ease-standard);
273
+ }
274
+
275
+ .c-table__expand:hover {
276
+ background: var(--pl-color-surface-2);
277
+ color: var(--pl-color-text);
278
+ }
279
+
280
+ .c-table__expand:focus-visible {
281
+ outline: var(--pl-focus-size) solid var(--pl-focus-color);
282
+ outline-offset: var(--pl-focus-offset);
283
+ }
284
+
285
+ .c-table__expand svg {
286
+ inline-size: 0.875rem;
287
+ block-size: 0.875rem;
288
+ transition: rotate var(--pl-duration-2) var(--pl-ease-standard);
289
+ }
290
+
291
+ .c-table__expand[aria-expanded="true"] svg {
292
+ rotate: 180deg;
293
+ }
294
+
295
+ /* Sticky first column - outward drop-shadow communicates "content scrolls
296
+ behind this column" (vs. the old inset-border which pointed inward and
297
+ read as a cell divider, not a depth separator). */
298
+ .c-table .c-table__sticky-col {
299
+ position: sticky;
300
+ inset-inline-start: 0;
301
+ z-index: calc(var(--pl-z-raised) + 1);
302
+ background: var(--pl-color-surface);
303
+ }
304
+
305
+ /* Sticky header + sticky column intersection needs higher z-index. */
306
+ .c-table th.c-table__sticky-col {
307
+ z-index: calc(var(--pl-z-raised) + 2);
308
+
309
+ /* Sticky col header: inherit the surface-2 header background so the
310
+ intersection cell matches the header row, not the body. */
311
+ background: var(--pl-color-surface-2);
312
+ }
313
+
314
+ /* Outward drop-shadow on the trailing edge: fades rightward to show
315
+ that body content scrolls underneath. */
316
+ .c-table .c-table__sticky-col:not(:last-child) {
317
+ box-shadow: 4px 0 8px -2px oklch(0% 0 0deg / 10%);
318
+ }
319
+
320
+ /* Inherit hover/selection background through the sticky column. */
321
+ :where(.c-table) tbody tr:hover .c-table__sticky-col,
322
+ :where(.c-table) tbody tr[aria-selected="true"] .c-table__sticky-col {
323
+ background: inherit;
324
+ }
325
+ }
326
+
327
+ /* Row hover - OUTSIDE @scope (live-state pseudo on tbody tr). */
328
+ :where(.c-table) tbody tr {
329
+ transition: background-color var(--pl-duration-1) var(--pl-ease-standard);
330
+ }
331
+
332
+ /* Stronger tint than plain surface-2 so the hover is clearly visible even
333
+ on high-brightness/low-contrast displays (surface-2 alone is ~3% step). */
334
+ :where(.c-table) tbody tr:hover {
335
+ background: color-mix(
336
+ in oklab,
337
+ var(--pl-color-surface-2),
338
+ var(--pl-color-text) 3.5%
339
+ );
340
+ }
341
+
342
+ /* Selection: left-bar accent (matching the nav active state language) +
343
+ stronger tint so the selected state is unmistakable at a glance. */
344
+ :where(.c-table) tbody tr[aria-selected="true"] {
345
+ background: color-mix(in oklab, var(--pl-color-accent), transparent 84%);
346
+ box-shadow: inset var(--pl-accent-bar-width) 0 0 var(--pl-color-accent);
347
+ }
348
+
349
+ /* Last data row: remove border-block-end so it doesn't double-up with the
350
+ table-wrap's own border on the bottom edge. */
351
+ :where(.c-table) tbody tr:last-child td {
352
+ border-block-end: none;
353
+ }
354
+
355
+ /* Opt-in zebra striping for extreme information density (financial data,
356
+ log tables). Very subtle at 50% transparent surface-2 so it doesn't
357
+ clash with hover/selection states. */
358
+ :where(.c-table-wrap[data-zebra]) .c-table tbody tr:nth-child(even) {
359
+ background: color-mix(
360
+ in oklab,
361
+ var(--pl-color-surface-2),
362
+ transparent 50%
363
+ );
364
+ }
365
+
366
+ /* Loading state: suppress hover effects on skeleton rows. */
367
+ :where(.c-table) tbody[data-loading] tr {
368
+ pointer-events: none;
369
+ }
370
+
371
+ @media (forced-colors: active) {
372
+ :where(.c-table) :where(th, td) {
373
+ border-block-end-color: CanvasText;
374
+ }
375
+
376
+ :where(.c-table__sticky-col) {
377
+ border-inline-end: 1px solid CanvasText;
378
+ }
379
+ }
380
+ }
@@ -0,0 +1,148 @@
1
+ /*
2
+ * Porchlight - dialog component
3
+ * ===========================================================================
4
+ * A modal dialog for confirmations, forms, and detail views. Uses the native
5
+ * <dialog> element with showModal() (top-layer, modal focus trap, Esc to
6
+ * close). The ::backdrop pseudo-element provides the scrim.
7
+ *
8
+ * Open state is styled via [open] (or :modal for showModal). Entry/exit
9
+ * animation uses @starting-style + transition-behavior: allow-discrete on
10
+ * overlay/display - the same pattern as .c-menu, so the EXIT transition runs
11
+ * (display: none normally kills transitions instantly).
12
+ *
13
+ * Header + body + footer slots (like .c-card). The close button in the header
14
+ * is a standard pattern. Open the dialog with el.showModal() in your app JS.
15
+ *
16
+ * Token-driven via --c-dialog-* aliases. Responsive by default:
17
+ * inline-size is min(100% - 2rem, --c-dialog-size) so it never overflows.
18
+ */
19
+ @layer porchlight.components {
20
+ @scope (.c-dialog) {
21
+ :scope {
22
+ --c-dialog-size: 42rem;
23
+
24
+ inline-size: min(100% - 2rem, var(--c-dialog-size));
25
+ max-block-size: min(100dvb - 2rem, 80dvb);
26
+ padding: 0;
27
+ margin: auto;
28
+ overflow: auto;
29
+
30
+ /* Liquid Glass: no border; shadow + backdrop do the structural work. */
31
+ border: none;
32
+ border-radius: var(--pl-radius-2xl);
33
+ background: light-dark(
34
+ oklch(100% 0 0deg / 94%),
35
+ oklch(17% 0.04 250deg / 92%)
36
+ );
37
+ backdrop-filter: blur(var(--pl-backdrop-blur-strong)) saturate(var(--pl-backdrop-saturate-strong));
38
+ -webkit-backdrop-filter: blur(var(--pl-backdrop-blur-strong)) saturate(var(--pl-backdrop-saturate-strong));
39
+ color: var(--pl-color-text);
40
+ box-shadow: var(--pl-shadow-3);
41
+
42
+ /* Entry/exit: asymmetric timing — enter deliberate, exit snappy. */
43
+ opacity: 0;
44
+ transform: scale(0.97) translateY(10px);
45
+ transition:
46
+ opacity var(--pl-duration-enter) var(--pl-ease-decelerate),
47
+ transform var(--pl-duration-enter) var(--pl-ease-decelerate),
48
+ overlay var(--pl-duration-exit) var(--pl-ease-accelerate) allow-discrete,
49
+ display var(--pl-duration-exit) var(--pl-ease-accelerate) allow-discrete;
50
+ }
51
+
52
+ /* Open: settled state. :modal covers showModal(); [open] covers show(). */
53
+ :scope:modal,
54
+ :scope[open] {
55
+ opacity: 1;
56
+ transform: scale(1) translateY(0);
57
+ }
58
+
59
+ /* Enter: the initial state BEFORE the open transition. */
60
+ @starting-style {
61
+ :scope:modal,
62
+ :scope[open] {
63
+ opacity: 0;
64
+ transform: scale(0.97) translateY(10px);
65
+ }
66
+ }
67
+
68
+ /* Scrim - dim the page behind the dialog. Stronger blur + deeper tint
69
+ for the Liquid Glass treatment (the frosted dialog needs more contrast
70
+ against a busy background). */
71
+ :scope::backdrop {
72
+ background: var(--pl-color-scrim);
73
+ backdrop-filter: blur(var(--pl-backdrop-blur-scrim)) saturate(var(--pl-backdrop-saturate-scrim));
74
+ }
75
+
76
+ .c-dialog__header {
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: space-between;
80
+ gap: var(--pl-space-3);
81
+ min-block-size: var(--pl-control-block-size);
82
+ padding: var(--pl-space-5);
83
+
84
+ /* Softer divider — the Liquid Glass surface is already visually separated */
85
+ border-block-end: 1px solid color-mix(
86
+ in oklab,
87
+ var(--pl-color-border),
88
+ transparent 40%
89
+ );
90
+ }
91
+
92
+ .c-dialog__title {
93
+ margin: 0;
94
+ font-size: var(--pl-text-lg);
95
+ font-weight: var(--pl-font-weight-bold);
96
+ line-height: var(--pl-leading-tight);
97
+ text-wrap: balance;
98
+ }
99
+
100
+ /* Close button - a ghost-icon button top-right. */
101
+ .c-dialog__close {
102
+ display: inline-flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ flex-shrink: 0;
106
+ inline-size: var(--pl-control-block-size);
107
+ block-size: var(--pl-control-block-size);
108
+ padding: 0;
109
+ border: 0;
110
+ border-radius: var(--pl-radius-md);
111
+ background: transparent;
112
+ color: var(--pl-color-text-muted);
113
+ font: inherit;
114
+ cursor: pointer;
115
+ transition:
116
+ background-color var(--pl-duration-1) var(--pl-ease-standard),
117
+ color var(--pl-duration-1) var(--pl-ease-standard);
118
+ }
119
+
120
+ .c-dialog__close:hover {
121
+ background: var(--pl-color-surface-2);
122
+ color: var(--pl-color-text);
123
+ }
124
+
125
+ .c-dialog__body {
126
+ padding: var(--pl-space-5);
127
+ }
128
+
129
+ .c-dialog__footer {
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: flex-end;
133
+ gap: var(--pl-space-3);
134
+ padding: var(--pl-space-5);
135
+ border-block-start: 1px solid color-mix(
136
+ in oklab,
137
+ var(--pl-color-border),
138
+ transparent 40%
139
+ );
140
+ }
141
+
142
+ @media (forced-colors: active) {
143
+ :scope {
144
+ border-color: ButtonBorder;
145
+ }
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,137 @@
1
+ /*
2
+ * Porchlight - drawer (slide-over panel) component
3
+ * ===========================================================================
4
+ * An off-canvas panel that slides in from a viewport edge. Uses the native
5
+ * Popover API for top-layer rendering + light-dismiss, combined with
6
+ * @starting-style for a smooth enter/exit animation (mirrors the dialog
7
+ * and popover-menu pattern).
8
+ *
9
+ * The drawer element MUST have the `popover` attribute in HTML:
10
+ * <div popover class="c-drawer" id="my-drawer" data-side="end">
11
+ * <header class="c-drawer__header">...</header>
12
+ * <div class="c-drawer__body">...</div>
13
+ * </div>
14
+ * The trigger uses `popovertarget="my-drawer"` to open it declaratively.
15
+ *
16
+ * [data-side="start"] slides from the inline-start edge (left in LTR).
17
+ * [data-side="end"] slides from the inline-end edge (right in LTR).
18
+ * Default is "end".
19
+ *
20
+ * A backdrop scrim is created via ::backdrop pseudo-element on the popover.
21
+ */
22
+ @layer porchlight.components {
23
+ @scope (.c-drawer) {
24
+ :scope {
25
+ --c-drawer-inline: min(24rem, 100vi);
26
+ --c-drawer-block: 100vb;
27
+ --c-drawer-pad: var(--pl-space-4);
28
+ --c-drawer-gap: var(--pl-space-3);
29
+
30
+ /* Position: stretch from the side edge. */
31
+ inset-inline: auto 0;
32
+ inset-block: 0;
33
+ margin: 0;
34
+ padding: 0;
35
+ border: 0;
36
+
37
+ /* No hard side border - shadow-3 defines the edge cleanly. */
38
+ inline-size: var(--c-drawer-inline);
39
+ block-size: var(--c-drawer-block);
40
+
41
+ /* Liquid Glass: translucent surface with blur. */
42
+ background: light-dark(
43
+ oklch(100% 0 0deg / 96%),
44
+ oklch(17% 0.04 250deg / 94%)
45
+ );
46
+ backdrop-filter: blur(var(--pl-backdrop-blur)) saturate(var(--pl-backdrop-saturate));
47
+ -webkit-backdrop-filter: blur(var(--pl-backdrop-blur)) saturate(var(--pl-backdrop-saturate));
48
+ box-shadow: var(--pl-shadow-3);
49
+
50
+ /* Animation: slide + fade. Asymmetric: enters deliberately, exits snappily. */
51
+ opacity: 0;
52
+ transform: translateX(100%);
53
+ transition:
54
+ opacity var(--pl-duration-enter) var(--pl-ease-decelerate),
55
+ transform var(--pl-duration-enter) var(--pl-ease-decelerate),
56
+ overlay var(--pl-duration-exit) var(--pl-ease-accelerate) allow-discrete,
57
+ display var(--pl-duration-exit) var(--pl-ease-accelerate) allow-discrete;
58
+ }
59
+
60
+ /* Start side: slide from inline-start. */
61
+ :scope[data-side="start"] {
62
+ inset-inline: 0 auto;
63
+ transform: translateX(-100%);
64
+ }
65
+
66
+ /* Open state: visible + settled. */
67
+ :scope:popover-open {
68
+ opacity: 1;
69
+ transform: translateX(0);
70
+ }
71
+
72
+ /* Enter: initial state before the open transition starts. */
73
+ @starting-style {
74
+ :scope:popover-open {
75
+ opacity: 0;
76
+ transform: translateX(100%);
77
+ }
78
+
79
+ :scope[data-side="start"]:popover-open {
80
+ transform: translateX(-100%);
81
+ }
82
+ }
83
+
84
+ /* Backdrop scrim (native ::backdrop on popovers). Stronger tint + blur
85
+ contrasts against the translucent drawer surface. */
86
+ :scope::backdrop {
87
+ background: var(--pl-color-scrim);
88
+ backdrop-filter: blur(var(--pl-backdrop-blur-scrim)) saturate(var(--pl-backdrop-saturate-scrim));
89
+ }
90
+
91
+ /* Layout inside the drawer. */
92
+ .c-drawer__header {
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: space-between;
96
+ gap: var(--c-drawer-gap);
97
+ padding: var(--c-drawer-pad);
98
+
99
+ /* Softer divider - the Liquid Glass surface is already visually separated. */
100
+ border-block-end: 1px solid color-mix(
101
+ in oklab,
102
+ var(--pl-color-border),
103
+ transparent 40%
104
+ );
105
+ }
106
+
107
+ .c-drawer__title {
108
+ font-size: var(--pl-text-lg);
109
+ font-weight: var(--pl-font-weight-semibold);
110
+ }
111
+
112
+ .c-drawer__body {
113
+ padding: var(--c-drawer-pad);
114
+ overflow-y: auto;
115
+ }
116
+
117
+ .c-drawer__footer {
118
+ display: flex;
119
+ gap: var(--c-drawer-gap);
120
+ padding: var(--c-drawer-pad);
121
+
122
+ /* Softer divider. */
123
+ border-block-start: 1px solid color-mix(
124
+ in oklab,
125
+ var(--pl-color-border),
126
+ transparent 40%
127
+ );
128
+ }
129
+ }
130
+
131
+ @media (forced-colors: active) {
132
+ :where(.c-drawer) {
133
+ border-color: ButtonBorder;
134
+ background: Canvas;
135
+ }
136
+ }
137
+ }