@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,77 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Porchlight - auto-growing textarea component
|
|
3
|
+
* ===========================================================================
|
|
4
|
+
* A textarea that grows to fit its content automatically using
|
|
5
|
+
* field-sizing: content (Chrome 123+). No JavaScript auto-resize needed.
|
|
6
|
+
*
|
|
7
|
+
* @supports-gated: where field-sizing is not supported, the textarea
|
|
8
|
+
* retains its default rows attribute (graceful degradation to a fixed
|
|
9
|
+
* height).
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* <textarea class="c-textarea-auto" rows="3"></textarea>
|
|
13
|
+
*
|
|
14
|
+
* Set [data-max-rows] to cap the growth (CSS can't count rows, so this is
|
|
15
|
+
* a max-height via the --c-textarea-max-block token).
|
|
16
|
+
*/
|
|
17
|
+
@layer porchlight.components {
|
|
18
|
+
@scope (.c-textarea-auto) {
|
|
19
|
+
:scope {
|
|
20
|
+
--c-textarea-max-block: 24rem;
|
|
21
|
+
|
|
22
|
+
inline-size: 100%;
|
|
23
|
+
padding: var(--pl-space-3);
|
|
24
|
+
border: 1px solid var(--pl-color-border);
|
|
25
|
+
border-radius: var(--pl-radius-md);
|
|
26
|
+
background: var(--pl-color-surface);
|
|
27
|
+
color: var(--pl-color-text);
|
|
28
|
+
font: inherit;
|
|
29
|
+
font-size: var(--pl-text-sm);
|
|
30
|
+
line-height: var(--pl-leading-normal);
|
|
31
|
+
resize: vertical;
|
|
32
|
+
transition:
|
|
33
|
+
border-color var(--pl-duration-1) var(--pl-ease-standard),
|
|
34
|
+
box-shadow var(--pl-duration-1) var(--pl-ease-standard);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
:scope:focus-visible {
|
|
38
|
+
outline: none;
|
|
39
|
+
border-color: var(--pl-focus-color);
|
|
40
|
+
box-shadow: 0 0 0 4px
|
|
41
|
+
color-mix(in oklab, var(--pl-focus-color), transparent var(--pl-focus-glow-opacity));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
:scope:user-invalid {
|
|
45
|
+
border-color: var(--pl-color-danger);
|
|
46
|
+
box-shadow: 0 0 0 4px
|
|
47
|
+
color-mix(in oklab, var(--pl-color-danger), transparent var(--pl-focus-glow-opacity));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
:scope:disabled {
|
|
51
|
+
opacity: var(--pl-opacity-disabled);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Auto-grow: field-sizing: content. */
|
|
55
|
+
@supports (field-sizing: content) {
|
|
56
|
+
:scope {
|
|
57
|
+
field-sizing: content;
|
|
58
|
+
|
|
59
|
+
/* Cap the growth so very long text scrolls instead of growing
|
|
60
|
+
indefinitely. */
|
|
61
|
+
max-block-size: var(--c-textarea-max-block);
|
|
62
|
+
overflow-y: auto;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@media (forced-colors: active) {
|
|
68
|
+
:where(.c-textarea-auto) {
|
|
69
|
+
border-color: ButtonBorder;
|
|
70
|
+
background: Canvas;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
:where(.c-textarea-auto:focus-visible) {
|
|
74
|
+
outline-color: Highlight;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Porchlight - timeline component
|
|
3
|
+
* ===========================================================================
|
|
4
|
+
* A vertical activity feed with markers, connector lines, and per-item
|
|
5
|
+
* content. Each item has a dot/marker on a vertical line, with the content
|
|
6
|
+
* to the side.
|
|
7
|
+
*
|
|
8
|
+
* Structure:
|
|
9
|
+
* <ol class="c-timeline">
|
|
10
|
+
* <li class="c-timeline__item">
|
|
11
|
+
* <span class="c-timeline__dot"></span>
|
|
12
|
+
* <div class="c-timeline__content">
|
|
13
|
+
* <div class="c-timeline__header">
|
|
14
|
+
* <span class="c-timeline__title">Event title</span>
|
|
15
|
+
* <time class="c-timeline__time">2 hours ago</time>
|
|
16
|
+
* </div>
|
|
17
|
+
* <p class="c-timeline__body">Description</p>
|
|
18
|
+
* </div>
|
|
19
|
+
* </li>
|
|
20
|
+
* </ol>
|
|
21
|
+
*
|
|
22
|
+
* Tone variants on the dot: [data-tone] on .c-timeline__item.
|
|
23
|
+
*/
|
|
24
|
+
@layer porchlight.components {
|
|
25
|
+
@scope (.c-timeline) {
|
|
26
|
+
:scope {
|
|
27
|
+
--c-timeline-dot-size: 0.75rem;
|
|
28
|
+
--c-timeline-line-color: var(--pl-color-border);
|
|
29
|
+
--c-timeline-gap: var(--pl-space-4);
|
|
30
|
+
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
margin: 0;
|
|
34
|
+
|
|
35
|
+
/* Inline-start padding accounts for the dot's box-shadow ring which
|
|
36
|
+
extends 2px past the first grid column. Without this, a scrollable
|
|
37
|
+
timeline (overflow-y:auto forces overflow-x:auto per spec) clips
|
|
38
|
+
the ring. Inline-end stays 0 so the scrollbar sits flush. */
|
|
39
|
+
padding-inline: var(--pl-space-1) 0;
|
|
40
|
+
list-style: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.c-timeline__item {
|
|
44
|
+
display: grid;
|
|
45
|
+
grid-template-columns: var(--c-timeline-dot-size) 1fr;
|
|
46
|
+
gap: var(--c-timeline-gap);
|
|
47
|
+
padding-block-end: var(--pl-space-5);
|
|
48
|
+
position: relative;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Vertical connector line: drawn on the dot column. */
|
|
52
|
+
.c-timeline__item:not(:last-child)::before {
|
|
53
|
+
content: "";
|
|
54
|
+
position: absolute;
|
|
55
|
+
inset-block: var(--c-timeline-dot-size) 0;
|
|
56
|
+
inset-inline-start: calc(var(--c-timeline-dot-size) / 2);
|
|
57
|
+
inline-size: 2px;
|
|
58
|
+
background: var(--c-timeline-line-color);
|
|
59
|
+
transform: translateX(-50%);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.c-timeline__dot {
|
|
63
|
+
inline-size: var(--c-timeline-dot-size);
|
|
64
|
+
block-size: var(--c-timeline-dot-size);
|
|
65
|
+
border-radius: 50%;
|
|
66
|
+
background: var(--pl-color-accent);
|
|
67
|
+
border: 2px solid var(--pl-color-surface);
|
|
68
|
+
box-shadow: 0 0 0 2px var(--pl-color-accent);
|
|
69
|
+
margin-block-start: 0.1875rem;
|
|
70
|
+
z-index: var(--pl-z-raised);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Tone variants on the dot. */
|
|
74
|
+
.c-timeline__item[data-tone="success"] .c-timeline__dot {
|
|
75
|
+
background: var(--pl-color-success);
|
|
76
|
+
box-shadow: 0 0 0 2px var(--pl-color-success);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.c-timeline__item[data-tone="warning"] .c-timeline__dot {
|
|
80
|
+
background: var(--pl-color-warning);
|
|
81
|
+
box-shadow: 0 0 0 2px var(--pl-color-warning);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.c-timeline__item[data-tone="danger"] .c-timeline__dot {
|
|
85
|
+
background: var(--pl-color-danger);
|
|
86
|
+
box-shadow: 0 0 0 2px var(--pl-color-danger);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.c-timeline__content {
|
|
90
|
+
min-inline-size: 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.c-timeline__header {
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: baseline;
|
|
96
|
+
justify-content: space-between;
|
|
97
|
+
gap: var(--pl-space-2);
|
|
98
|
+
margin-block-end: var(--pl-space-1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.c-timeline__title {
|
|
102
|
+
font-size: var(--pl-text-sm);
|
|
103
|
+
font-weight: var(--pl-font-weight-semibold);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.c-timeline__time {
|
|
107
|
+
font-size: var(--pl-text-xs);
|
|
108
|
+
color: var(--pl-color-text-muted);
|
|
109
|
+
white-space: nowrap;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.c-timeline__body {
|
|
113
|
+
font-size: var(--pl-text-sm);
|
|
114
|
+
color: var(--pl-color-text-muted);
|
|
115
|
+
line-height: var(--pl-leading-normal);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@media (forced-colors: active) {
|
|
120
|
+
:where(.c-timeline__dot) {
|
|
121
|
+
background: Highlight;
|
|
122
|
+
box-shadow: 0 0 0 2px Highlight;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
:where(.c-timeline__item:not(:last-child))::before {
|
|
126
|
+
background: ButtonBorder;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Porchlight - toast notification component
|
|
3
|
+
* ===========================================================================
|
|
4
|
+
* A transient notification that slides in from a screen edge. Unlike alerts
|
|
5
|
+
* (inline), toasts are overlay-level and auto-dismiss (via JS timer; the CSS
|
|
6
|
+
* handles the visual entrance/exit).
|
|
7
|
+
*
|
|
8
|
+
* Uses @starting-style for the enter animation. The toast stack is a fixed-
|
|
9
|
+
* position flex column at the bottom-corner of the screen.
|
|
10
|
+
*
|
|
11
|
+
* Structure:
|
|
12
|
+
* <div class="c-toast-stack" role="region" aria-label="Notifications">
|
|
13
|
+
* <div class="c-toast" data-tone="success" role="status">
|
|
14
|
+
* <span class="c-toast__icon">...</span>
|
|
15
|
+
* <div class="c-toast__content">
|
|
16
|
+
* <span class="c-toast__title">Saved</span>
|
|
17
|
+
* <span class="c-toast__body">Changes were saved.</span>
|
|
18
|
+
* </div>
|
|
19
|
+
* <button class="c-toast__close" aria-label="Dismiss">x</button>
|
|
20
|
+
* </div>
|
|
21
|
+
* </div>
|
|
22
|
+
*
|
|
23
|
+
* Tones: default (info), success, warning, danger.
|
|
24
|
+
* z-index: --pl-z-toast (1100, above overlays/dialogs at 1000).
|
|
25
|
+
*/
|
|
26
|
+
@layer porchlight.components {
|
|
27
|
+
/* Toast stack: fixed positioning, bottom-corner. */
|
|
28
|
+
:where(.c-toast-stack) {
|
|
29
|
+
position: fixed;
|
|
30
|
+
inset-block-end: var(--pl-space-4);
|
|
31
|
+
inset-inline-end: var(--pl-space-4);
|
|
32
|
+
z-index: var(--pl-z-toast);
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
gap: var(--pl-space-2);
|
|
36
|
+
inline-size: min(24rem, calc(100vi - 2 * var(--pl-space-4)));
|
|
37
|
+
|
|
38
|
+
/* Prevent the stack from stealing pointer events in empty areas. */
|
|
39
|
+
pointer-events: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@scope (.c-toast) {
|
|
43
|
+
:scope {
|
|
44
|
+
--c-toast-tone: var(--pl-color-accent);
|
|
45
|
+
--c-toast-pad: var(--pl-space-3) var(--pl-space-4);
|
|
46
|
+
--c-toast-radius: var(--pl-radius-xl);
|
|
47
|
+
--c-toast-gap: var(--pl-space-3);
|
|
48
|
+
|
|
49
|
+
display: grid;
|
|
50
|
+
grid-template-columns: auto 1fr auto;
|
|
51
|
+
gap: var(--c-toast-gap);
|
|
52
|
+
align-items: start;
|
|
53
|
+
padding: var(--c-toast-pad);
|
|
54
|
+
|
|
55
|
+
/* Floating surface: softened border + Liquid Glass. */
|
|
56
|
+
border: 1px solid color-mix(
|
|
57
|
+
in oklab,
|
|
58
|
+
var(--pl-color-border),
|
|
59
|
+
transparent 25%
|
|
60
|
+
);
|
|
61
|
+
border-inline-start: var(--pl-accent-bar-width) solid var(--c-toast-tone);
|
|
62
|
+
border-radius: var(--c-toast-radius);
|
|
63
|
+
background: light-dark(
|
|
64
|
+
oklch(100% 0 0deg / 96%),
|
|
65
|
+
oklch(17% 0.04 250deg / 94%)
|
|
66
|
+
);
|
|
67
|
+
backdrop-filter: blur(var(--pl-backdrop-blur)) saturate(var(--pl-backdrop-saturate));
|
|
68
|
+
-webkit-backdrop-filter: blur(var(--pl-backdrop-blur)) saturate(var(--pl-backdrop-saturate));
|
|
69
|
+
box-shadow: var(--pl-shadow-3);
|
|
70
|
+
pointer-events: auto;
|
|
71
|
+
|
|
72
|
+
/* Enter animation: slide in from the right + fade.
|
|
73
|
+
Asymmetric: enters deliberately, exits by JS removing [data-visible]. */
|
|
74
|
+
opacity: 0;
|
|
75
|
+
transform: translateX(100%);
|
|
76
|
+
transition:
|
|
77
|
+
opacity var(--pl-duration-enter) var(--pl-ease-decelerate),
|
|
78
|
+
transform var(--pl-duration-enter) var(--pl-ease-decelerate);
|
|
79
|
+
|
|
80
|
+
/* Use @starting-style so the initial render animates from hidden. */
|
|
81
|
+
interpolate-size: allow-keywords;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Settled state (after enter). */
|
|
85
|
+
:scope[data-visible] {
|
|
86
|
+
opacity: 1;
|
|
87
|
+
transform: translateX(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@starting-style {
|
|
91
|
+
:scope[data-visible] {
|
|
92
|
+
opacity: 0;
|
|
93
|
+
transform: translateX(100%);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Tone variants. */
|
|
98
|
+
:scope[data-tone="success"] {
|
|
99
|
+
--c-toast-tone: var(--pl-color-success);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
:scope[data-tone="warning"] {
|
|
103
|
+
--c-toast-tone: var(--pl-color-warning);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
:scope[data-tone="danger"] {
|
|
107
|
+
--c-toast-tone: var(--pl-color-danger);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.c-toast__icon {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: flex-start;
|
|
113
|
+
color: var(--c-toast-tone);
|
|
114
|
+
flex-shrink: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.c-toast__icon svg {
|
|
118
|
+
inline-size: 1.25rem;
|
|
119
|
+
block-size: 1.25rem;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.c-toast__content {
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
gap: var(--pl-space-1);
|
|
126
|
+
min-inline-size: 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.c-toast__title {
|
|
130
|
+
font-weight: var(--pl-font-weight-semibold);
|
|
131
|
+
font-size: var(--pl-text-sm);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.c-toast__body {
|
|
135
|
+
font-size: var(--pl-text-sm);
|
|
136
|
+
color: var(--pl-color-text-muted);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.c-toast__close {
|
|
140
|
+
display: inline-flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
justify-content: center;
|
|
143
|
+
inline-size: 1.5rem;
|
|
144
|
+
block-size: 1.5rem;
|
|
145
|
+
padding: 0;
|
|
146
|
+
border: 0;
|
|
147
|
+
border-radius: var(--pl-radius-sm);
|
|
148
|
+
background: transparent;
|
|
149
|
+
color: var(--pl-color-text-muted);
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
flex-shrink: 0;
|
|
152
|
+
transition: background-color var(--pl-duration-1) var(--pl-ease-standard);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.c-toast__close:hover {
|
|
156
|
+
background: var(--pl-color-surface-2);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.c-toast__close:focus-visible {
|
|
160
|
+
outline: var(--pl-focus-size) solid var(--pl-focus-color);
|
|
161
|
+
outline-offset: var(--pl-focus-offset);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@media (forced-colors: active) {
|
|
166
|
+
:where(.c-toast) {
|
|
167
|
+
border-color: ButtonBorder;
|
|
168
|
+
background: Canvas;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
:where(.c-toast__close:focus-visible) {
|
|
172
|
+
outline-color: Highlight;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Porchlight - toolbar component
|
|
3
|
+
* ===========================================================================
|
|
4
|
+
* A horizontal bar grouping actions above a data region - the SaaS "action bar"
|
|
5
|
+
* on top of tables, lists, and canvases. Pairs a leading cluster (search,
|
|
6
|
+
* filters, bulk actions) with a trailing cluster (primary actions, overflow
|
|
7
|
+
* menu). On narrow containers the two halves wrap independently.
|
|
8
|
+
*
|
|
9
|
+
* Pure structural styling (flex + gap + a divider). It composes .c-button,
|
|
10
|
+
* .c-field, and .c-menu - it is NOT a button system. The toolbar's job is
|
|
11
|
+
* alignment and wrapping; the components inside provide their own visuals.
|
|
12
|
+
*
|
|
13
|
+
* Container-query aware: padding and gap tighten when the toolbar itself is
|
|
14
|
+
* narrow (inside a split pane), not when the viewport changes.
|
|
15
|
+
*/
|
|
16
|
+
@layer porchlight.components {
|
|
17
|
+
@scope (.c-toolbar) {
|
|
18
|
+
:scope {
|
|
19
|
+
--c-toolbar-gap: var(--pl-space-3);
|
|
20
|
+
--c-toolbar-pad: var(--pl-space-3);
|
|
21
|
+
--c-toolbar-min: 10rem;
|
|
22
|
+
|
|
23
|
+
container: c-toolbar / inline-size;
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-wrap: wrap;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: space-between;
|
|
28
|
+
gap: var(--c-toolbar-gap);
|
|
29
|
+
padding: var(--c-toolbar-pad);
|
|
30
|
+
border-block-end: 1px solid color-mix(
|
|
31
|
+
in oklab,
|
|
32
|
+
var(--pl-color-border),
|
|
33
|
+
transparent 20%
|
|
34
|
+
);
|
|
35
|
+
background: var(--pl-color-surface);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Leading group - search, filters, selection count. Grows to fill. */
|
|
39
|
+
.c-toolbar__group {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-wrap: wrap;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: var(--pl-space-2);
|
|
44
|
+
min-inline-size: var(--c-toolbar-min);
|
|
45
|
+
|
|
46
|
+
/* The first group can grow to push the trailing group to the right. */
|
|
47
|
+
flex: 1 1 auto;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Trailing group (last-child) - primary actions, pagination, overflow. */
|
|
51
|
+
.c-toolbar__group:last-child {
|
|
52
|
+
justify-content: flex-end;
|
|
53
|
+
flex: 0 1 auto;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* A divider between toolbar items - a thin vertical rule. */
|
|
57
|
+
.c-toolbar__divider {
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
inline-size: 1px;
|
|
60
|
+
block-size: 1.5rem;
|
|
61
|
+
align-self: center;
|
|
62
|
+
background: color-mix(
|
|
63
|
+
in oklab,
|
|
64
|
+
var(--pl-color-border),
|
|
65
|
+
transparent 20%
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Tighten padding when the toolbar is narrow (container query). */
|
|
70
|
+
@container c-toolbar (inline-size < 36rem) {
|
|
71
|
+
:scope {
|
|
72
|
+
--c-toolbar-pad: var(--pl-space-2);
|
|
73
|
+
--c-toolbar-gap: var(--pl-space-2);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@media (forced-colors: active) {
|
|
79
|
+
:where(.c-toolbar) {
|
|
80
|
+
border-block-end-color: CanvasText;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
:where(.c-toolbar__divider) {
|
|
84
|
+
background: CanvasText;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Porchlight - tooltip component
|
|
3
|
+
* ===========================================================================
|
|
4
|
+
* A hover/focus tooltip anchored to its trigger using CSS Anchor Positioning.
|
|
5
|
+
* Zero JavaScript for positioning. Auto-flips when near viewport edges via
|
|
6
|
+
* position-try-fallbacks.
|
|
7
|
+
*
|
|
8
|
+
* The tooltip appears on :hover and :focus-visible of the trigger. It is
|
|
9
|
+
* tethered via anchor-name/position-anchor. Without anchor positioning
|
|
10
|
+
* support (@supports gate), the tooltip is hidden (no misleading placement).
|
|
11
|
+
*
|
|
12
|
+
* Structure: .c-tooltip wraps a trigger element. The tooltip body is a child
|
|
13
|
+
* with role="tooltip" and an id referenced by aria-describedby on the trigger.
|
|
14
|
+
*
|
|
15
|
+
* <span class="c-tooltip">
|
|
16
|
+
* <span class="c-tooltip__trigger">
|
|
17
|
+
* <button aria-describedby="tip-1">Help</button>
|
|
18
|
+
* </span>
|
|
19
|
+
* <span class="c-tooltip__body" role="tooltip" id="tip-1">Tooltip text</span>
|
|
20
|
+
* </span>
|
|
21
|
+
*
|
|
22
|
+
* Each instance needs a unique anchor name - set --c-tooltip-anchor.
|
|
23
|
+
*/
|
|
24
|
+
@layer porchlight.components {
|
|
25
|
+
@scope (.c-tooltip) {
|
|
26
|
+
:scope {
|
|
27
|
+
--c-tooltip-anchor: --pl-tooltip-anchor;
|
|
28
|
+
--c-tooltip-bg: var(--pl-color-text);
|
|
29
|
+
--c-tooltip-text: var(--pl-color-bg);
|
|
30
|
+
--c-tooltip-pad: var(--pl-space-2) var(--pl-space-3);
|
|
31
|
+
--c-tooltip-radius: var(--pl-radius-lg);
|
|
32
|
+
--c-tooltip-font-size: var(--pl-text-sm);
|
|
33
|
+
--c-tooltip-max-inline: 20rem;
|
|
34
|
+
|
|
35
|
+
display: inline-flex;
|
|
36
|
+
position: relative;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.c-tooltip__trigger {
|
|
40
|
+
anchor-name: var(--c-tooltip-anchor);
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.c-tooltip__body {
|
|
45
|
+
display: none;
|
|
46
|
+
opacity: 0;
|
|
47
|
+
pointer-events: none;
|
|
48
|
+
position: absolute;
|
|
49
|
+
padding: var(--c-tooltip-pad);
|
|
50
|
+
border-radius: var(--c-tooltip-radius);
|
|
51
|
+
background: var(--c-tooltip-bg);
|
|
52
|
+
color: var(--c-tooltip-text);
|
|
53
|
+
font-size: var(--c-tooltip-font-size);
|
|
54
|
+
line-height: var(--pl-leading-tight);
|
|
55
|
+
max-inline-size: var(--c-tooltip-max-inline);
|
|
56
|
+
text-wrap: balance;
|
|
57
|
+
box-shadow: var(--pl-shadow-2);
|
|
58
|
+
z-index: var(--pl-z-overlay);
|
|
59
|
+
transition:
|
|
60
|
+
opacity var(--pl-duration-1) var(--pl-ease-standard),
|
|
61
|
+
display var(--pl-duration-1) var(--pl-ease-standard) allow-discrete;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Anchor positioning: tether above the trigger, try flipping below. */
|
|
65
|
+
@supports (position-anchor: --x) {
|
|
66
|
+
.c-tooltip__body {
|
|
67
|
+
position-anchor: var(--c-tooltip-anchor);
|
|
68
|
+
position-area: block-start span-inline-end;
|
|
69
|
+
margin-block-end: var(--pl-space-2);
|
|
70
|
+
position-try-fallbacks: flip-block;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Show on trigger hover/focus. */
|
|
75
|
+
:scope:has(.c-tooltip__trigger:hover) .c-tooltip__body,
|
|
76
|
+
:scope:has(.c-tooltip__trigger:focus-visible) .c-tooltip__body {
|
|
77
|
+
opacity: 1;
|
|
78
|
+
display: block;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Enter animation. */
|
|
82
|
+
@starting-style {
|
|
83
|
+
:scope:has(.c-tooltip__trigger:hover) .c-tooltip__body,
|
|
84
|
+
:scope:has(.c-tooltip__trigger:focus-visible) .c-tooltip__body {
|
|
85
|
+
opacity: 0;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Without anchor positioning support, hide the tooltip body entirely. */
|
|
91
|
+
@supports not (position-anchor: --x) {
|
|
92
|
+
:where(.c-tooltip__body) {
|
|
93
|
+
display: none;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@media (forced-colors: active) {
|
|
98
|
+
:where(.c-tooltip__body) {
|
|
99
|
+
background: CanvasText;
|
|
100
|
+
color: Canvas;
|
|
101
|
+
border: 1px solid CanvasText;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Porchlight: utilities
|
|
3
|
+
* ===========================================================================
|
|
4
|
+
* A small, finite set of single-purpose helpers for repeated a11y, layout,
|
|
5
|
+
* and typography needs. NOT a utility-first framework: there is deliberately
|
|
6
|
+
* no utility for every CSS property. Each utility earns its place by serving
|
|
7
|
+
* a recurring need a component shouldn't own.
|
|
8
|
+
*
|
|
9
|
+
* Allow-list for !important: utilities is the SECOND place it's permitted
|
|
10
|
+
* (after reset [hidden] and themes reduced-motion). .u-visually-hidden uses it
|
|
11
|
+
* because the rule MUST win over any component/context. Hiding text from
|
|
12
|
+
* sighted users while keeping it for AT is an a11y guarantee that can't be
|
|
13
|
+
* accidentally overridden.
|
|
14
|
+
*
|
|
15
|
+
* Naming: .u-* (utility); --u-* are instance-config tokens (e.g. --u-flow-space).
|
|
16
|
+
*/
|
|
17
|
+
@layer porchlight.utilities {
|
|
18
|
+
/* Visually hide content while keeping it available to assistive tech (the
|
|
19
|
+
canonical sr-only pattern). The element collapses to a 1px box, is clipped,
|
|
20
|
+
and is removed from the flow. Used for icon-only buttons, skip links, etc. */
|
|
21
|
+
:where(.u-visually-hidden) {
|
|
22
|
+
position: absolute !important;
|
|
23
|
+
inline-size: 1px !important;
|
|
24
|
+
block-size: 1px !important;
|
|
25
|
+
padding: 0 !important;
|
|
26
|
+
margin: -1px !important;
|
|
27
|
+
overflow: hidden !important;
|
|
28
|
+
clip-path: inset(50%) !important;
|
|
29
|
+
white-space: nowrap !important;
|
|
30
|
+
border: 0 !important;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Restore visibility (overrides .u-visually-hidden when focused/active). */
|
|
34
|
+
:where(.u-visually-hidden:focus, .u-visually-hidden:active) {
|
|
35
|
+
position: static !important;
|
|
36
|
+
inline-size: auto !important;
|
|
37
|
+
block-size: auto !important;
|
|
38
|
+
clip-path: none !important;
|
|
39
|
+
white-space: normal !important;
|
|
40
|
+
margin: 0 !important;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* One-line truncation: table cells, breadcrumb items, file names. */
|
|
44
|
+
:where(.u-truncate) {
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
text-overflow: ellipsis;
|
|
47
|
+
white-space: nowrap;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Vertical rhythm for prose blocks. The sibling selector adds space
|
|
51
|
+
between top-level children, not before the first / after the last. Tune
|
|
52
|
+
via --u-flow-space. */
|
|
53
|
+
:where(.u-flow) > * + * {
|
|
54
|
+
margin-block-start: var(--u-flow-space, var(--pl-space-4));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* A surface treatment for ad-hoc grouping (not a full .c-card): bg, text,
|
|
58
|
+
border, radius. For when you need panel styling without a component. */
|
|
59
|
+
:where(.u-surface) {
|
|
60
|
+
background: var(--pl-color-surface);
|
|
61
|
+
color: var(--pl-color-text);
|
|
62
|
+
border: 1px solid var(--pl-color-border);
|
|
63
|
+
border-radius: var(--pl-radius-lg);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Muted text: secondary/caption copy. */
|
|
67
|
+
:where(.u-muted) {
|
|
68
|
+
color: var(--pl-color-text-muted);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Full-bleed: break an element out to the viewport edges inside a
|
|
72
|
+
constrained container (e.g. a wide table or image in a prose column). */
|
|
73
|
+
:where(.u-full-bleed) {
|
|
74
|
+
inline-size: 100vi;
|
|
75
|
+
margin-inline: calc(50% - 50vi);
|
|
76
|
+
}
|
|
77
|
+
}
|