@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.
- package/README.md +28 -0
- package/dist/porchlight.css +3765 -0
- package/dist/porchlight.min.css +1 -0
- package/package.json +59 -0
- package/porchlight.css +62 -0
- package/src/00-layer-order.css +22 -0
- package/src/01-reset.css +53 -0
- package/src/02-tokens.css +254 -0
- package/src/03-themes.css +79 -0
- package/src/04-base.css +78 -0
- package/src/05-layout.css +209 -0
- package/src/06-components/accordion.css +161 -0
- package/src/06-components/alert.css +102 -0
- package/src/06-components/avatar.css +112 -0
- package/src/06-components/badge.css +73 -0
- package/src/06-components/breadcrumb.css +111 -0
- package/src/06-components/button.css +180 -0
- package/src/06-components/card.css +186 -0
- package/src/06-components/chip.css +146 -0
- package/src/06-components/command-palette.css +201 -0
- package/src/06-components/data-table.css +380 -0
- package/src/06-components/dialog.css +148 -0
- package/src/06-components/drawer.css +137 -0
- package/src/06-components/dropdown.css +180 -0
- package/src/06-components/empty-state.css +85 -0
- package/src/06-components/field.css +125 -0
- package/src/06-components/file-upload.css +104 -0
- package/src/06-components/nav.css +185 -0
- package/src/06-components/pagination.css +106 -0
- package/src/06-components/popover-menu.css +146 -0
- package/src/06-components/progress.css +77 -0
- package/src/06-components/reveal.css +73 -0
- package/src/06-components/scroll-progress.css +73 -0
- package/src/06-components/segmented.css +113 -0
- package/src/06-components/skeleton.css +73 -0
- package/src/06-components/stat.css +107 -0
- package/src/06-components/stepper.css +172 -0
- package/src/06-components/switch.css +138 -0
- package/src/06-components/tabs.css +164 -0
- package/src/06-components/tag-input.css +77 -0
- package/src/06-components/textarea-auto.css +77 -0
- package/src/06-components/timeline.css +129 -0
- package/src/06-components/toast.css +175 -0
- package/src/06-components/toolbar.css +87 -0
- package/src/06-components/tooltip.css +104 -0
- package/src/07-utilities.css +77 -0
- 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
|
+
}
|