a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1/* ========================================================================== */
2/* Components */
3/* ========================================================================== */
4
5/**
6 * Dialog (Modal)
7 *
8 * Native <dialog> element provides semantic modal functionality.
9 * Structure: <dialog><article><header>...</header>content<footer>...</footer></article></dialog>
10 * Close buttons should use aria-label="Close" for targeting.
11 */
12dialog {
13 border: none;
14 padding: 0;
15 margin: auto;
16 max-width: min(90vw, 600px);
17 max-height: 90vh;
18 background-color: var(--color-bg);
19 border-radius: var(--radius-lg);
20 box-shadow: var(--shadow-lg);
21 overflow: hidden;
22}
23
24dialog::backdrop {
25 background-color: rgba(0, 0, 0, 0.5);
26 backdrop-filter: blur(4px);
27}
28
29@media (prefers-color-scheme: dark) {
30 dialog::backdrop {
31 background-color: rgba(0, 0, 0, 0.7);
32 }
33}
34
35dialog article {
36 margin: 0;
37 padding: 0;
38 display: flex;
39 flex-direction: column;
40 max-height: 90vh;
41}
42
43dialog header {
44 position: relative;
45 border-bottom: 1px solid var(--color-border);
46 padding: var(--space-lg);
47 margin: 0;
48}
49
50dialog header h1,
51dialog header h2,
52dialog header h3,
53dialog header h4,
54dialog header h5,
55dialog header h6 {
56 margin-top: 0;
57 margin-bottom: 0;
58 padding-right: var(--space-3xl);
59}
60
61dialog header button[aria-label="Close"],
62dialog header button[aria-label="close"] {
63 position: absolute;
64 top: var(--space-lg);
65 right: var(--space-lg);
66 background: none;
67 border: none;
68 font-size: 1.75rem;
69 line-height: 1;
70 padding: var(--space-xs);
71 margin: 0;
72 width: auto;
73 color: var(--color-text-muted);
74 cursor: pointer;
75 transition: color var(--transition-fast), transform var(--transition-fast);
76}
77
78dialog header button[aria-label="Close"]:hover,
79dialog header button[aria-label="close"]:hover {
80 color: var(--color-text);
81 background: none;
82 transform: scale(1.1);
83}
84
85dialog article > :not(header):not(footer) {
86 padding: var(--space-lg);
87 overflow-y: auto;
88 flex: 1;
89}
90
91dialog form {
92 margin: 0;
93 max-width: none;
94}
95
96dialog footer {
97 border-top: 1px solid var(--color-border);
98 padding: var(--space-lg);
99 margin: 0;
100 display: flex;
101 gap: var(--space-md);
102 justify-content: flex-end;
103 flex-wrap: wrap;
104}
105
106dialog footer button {
107 margin: 0;
108}
109
110@media (max-width: 768px) {
111 dialog {
112 max-width: 95vw;
113 max-height: 95vh;
114 }
115
116 dialog header,
117 dialog article > :not(header):not(footer),
118 dialog footer {
119 padding: var(--space-md);
120 }
121
122
123 dialog header button[aria-label="Close"],
124 dialog header button[aria-label="close"] {
125 top: var(--space-md);
126 right: var(--space-md);
127 }
128}
129
130/**
131 * Accordian
132 *
133 * implemented as details and summary
134 */
135details {
136 margin: var(--space-lg) 0;
137 padding: var(--space-md);
138 border: 1px solid var(--color-border);
139 border-radius: var(--radius-md);
140 max-width: var(--content-width);
141}
142
143summary {
144 font-weight: 700;
145 cursor: pointer;
146 user-select: none;
147 padding: var(--space-sm);
148 margin: calc(-1 * var(--space-sm));
149 transition: background-color var(--transition-fast);
150}
151
152summary:hover {
153 background-color: var(--color-bg-alt);
154}
155
156details[open] summary {
157 margin-bottom: var(--space-md);
158 border-bottom: 1px solid var(--color-border);
159}
160
161/**
162 * Tooltips
163 *
164 * Declarative tooltips using data-vx-tooltip attribute.
165 * Placements: top (default), right, bottom, left
166 * Structure: <element data-vx-tooltip="Tooltip text" data-placement="top">
167 */
168[data-vx-tooltip] {
169 position: relative;
170 cursor: help;
171}
172
173[data-vx-tooltip]::before,
174[data-vx-tooltip]::after {
175 position: absolute;
176 opacity: 0;
177 pointer-events: none;
178 transition: opacity var(--transition-fast), transform var(--transition-fast);
179 z-index: 1000;
180}
181
182[data-vx-tooltip]::before {
183 content: attr(data-vx-tooltip);
184 background: var(--color-text);
185 color: var(--color-bg);
186 padding: var(--space-sm) var(--space-md);
187 border-radius: var(--radius-sm);
188 font-size: 0.875rem;
189 white-space: normal;
190 min-width: 200px;
191 max-width: 300px;
192 width: max-content;
193 text-align: center;
194 display: -webkit-box;
195 -webkit-box-orient: vertical;
196 -webkit-line-clamp: 4;
197 line-clamp: 4;
198 overflow: hidden;
199 text-overflow: ellipsis;
200 line-height: 1.4;
201}
202
203[data-vx-tooltip]::after {
204 content: '';
205 border: 6px solid transparent;
206}
207
208[data-vx-tooltip]:hover::before,
209[data-vx-tooltip]:hover::after,
210[data-vx-tooltip]:focus::before,
211[data-vx-tooltip]:focus::after,
212[data-vx-tooltip]:focus-visible::before,
213[data-vx-tooltip]:focus-visible::after {
214 opacity: 1;
215}
216
217/* Placement: Top (default) */
218[data-vx-tooltip]:not([data-placement])::before,
219[data-vx-tooltip][data-placement="top"]::before {
220 bottom: calc(100% + 12px);
221 left: 50%;
222 transform: translateX(-50%) translateY(4px);
223}
224
225[data-vx-tooltip]:not([data-placement])::after,
226[data-vx-tooltip][data-placement="top"]::after {
227 bottom: calc(100% + 6px);
228 left: 50%;
229 transform: translateX(-50%);
230 border-top-color: var(--color-text);
231}
232
233[data-vx-tooltip]:not([data-placement]):hover::before,
234[data-vx-tooltip][data-placement="top"]:hover::before,
235[data-vx-tooltip]:not([data-placement]):focus::before,
236[data-vx-tooltip][data-placement="top"]:focus::before {
237 transform: translateX(-50%) translateY(0);
238}
239
240/* Placement: Bottom */
241[data-vx-tooltip][data-placement="bottom"]::before {
242 top: calc(100% + 12px);
243 left: 50%;
244 transform: translateX(-50%) translateY(-4px);
245}
246
247[data-vx-tooltip][data-placement="bottom"]::after {
248 top: calc(100% + 6px);
249 left: 50%;
250 transform: translateX(-50%);
251 border-bottom-color: var(--color-text);
252}
253
254[data-vx-tooltip][data-placement="bottom"]:hover::before,
255[data-vx-tooltip][data-placement="bottom"]:focus::before {
256 transform: translateX(-50%) translateY(0);
257}
258
259[data-vx-tooltip][data-placement="right"]::before {
260 left: calc(100% + 12px);
261 top: 50%;
262 transform: translateY(-50%) translateX(-4px);
263}
264
265[data-vx-tooltip][data-placement="right"]::after {
266 left: calc(100% + 6px);
267 top: 50%;
268 transform: translateY(-50%);
269 border-right-color: var(--color-text);
270}
271
272[data-vx-tooltip][data-placement="right"]:hover::before,
273[data-vx-tooltip][data-placement="right"]:focus::before {
274 transform: translateY(-50%) translateX(0);
275}
276
277[data-vx-tooltip][data-placement="left"]::before {
278 right: calc(100% + 12px);
279 top: 50%;
280 transform: translateY(-50%) translateX(4px);
281}
282
283[data-vx-tooltip][data-placement="left"]::after {
284 right: calc(100% + 6px);
285 top: 50%;
286 transform: translateY(-50%);
287 border-left-color: var(--color-text);
288}
289
290[data-vx-tooltip][data-placement="left"]:hover::before,
291[data-vx-tooltip][data-placement="left"]:focus::before {
292 transform: translateY(-50%) translateX(0);
293}
294
295@media (max-width: 768px) {
296 [data-vx-tooltip]::before,
297 [data-vx-tooltip]::after {
298 display: none;
299 }
300}